use std::{collections::HashMap, sync::Arc};
use crate::tools::tool_name::{cap_tool_name, is_capped_tool_name};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum ExportedNameDialect {
Rho,
Conventional,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct McpToolIdentity {
pub(crate) server: String,
pub(crate) tool: String,
}
#[derive(Clone, Debug, Default)]
pub(crate) struct McpToolNames {
registered: Arc<HashMap<String, McpToolIdentity>>,
}
impl McpToolNames {
pub(crate) fn identify(&self, name: &str) -> Option<McpToolIdentity> {
match self.registered.get(name) {
Some(identity) => Some(identity.clone()),
None => parse_exported_name(name, ExportedNameDialect::Rho),
}
}
}
impl FromIterator<(String, McpToolIdentity)> for McpToolNames {
fn from_iter<I: IntoIterator<Item = (String, McpToolIdentity)>>(entries: I) -> Self {
Self {
registered: Arc::new(entries.into_iter().collect()),
}
}
}
pub(crate) fn namespaced_tool_name(server: &str, tool: &str) -> String {
cap_tool_name(format!(
"{}{}",
exported_name_prefix(server),
encode_component(tool)
))
}
pub(crate) fn exported_name_prefix(server: &str) -> String {
format!("mcp__{}__", encode_component(server))
}
fn encode_component(value: &str) -> String {
const ESCAPE_PREFIX: &str = "_rho_";
let already_safe = value
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-'));
if already_safe && !value.contains("__") && !value.starts_with('_') && !value.ends_with('_') {
return value.to_string();
}
let mut encoded = String::with_capacity(ESCAPE_PREFIX.len() + value.len() * 2);
encoded.push_str(ESCAPE_PREFIX);
const HEX: &[u8; 16] = b"0123456789abcdef";
for byte in value.bytes() {
encoded.push(HEX[(byte >> 4) as usize] as char);
encoded.push(HEX[(byte & 0x0f) as usize] as char);
}
encoded
}
pub(crate) fn parse_exported_name(
name: &str,
dialect: ExportedNameDialect,
) -> Option<McpToolIdentity> {
let rest = name.strip_prefix("mcp__")?;
let (server, tool) = rest.split_once("__")?;
if server.is_empty() || tool.is_empty() {
return None;
}
let component = match dialect {
ExportedNameDialect::Rho if is_capped_tool_name(name) => return None,
ExportedNameDialect::Rho => decode_rho_component,
ExportedNameDialect::Conventional => str::to_string,
};
Some(McpToolIdentity {
server: component(server),
tool: component(tool),
})
}
pub(crate) fn resolve_foreign_spelling<'a>(
requested: &str,
registered: impl IntoIterator<Item = &'a str>,
) -> Option<&'a str> {
let registered: Vec<&'a str> = registered.into_iter().collect();
let capped = cap_tool_name(requested.to_owned());
if capped != requested {
if let Some(found) = registered.iter().copied().find(|name| *name == capped) {
return Some(found);
}
}
let key = spelling_key(requested)?;
let mut matches = registered
.into_iter()
.filter(|name| spelling_key(name).as_ref() == Some(&key));
let found = matches.next()?;
matches.next().is_none().then_some(found)
}
fn spelling_key(name: &str) -> Option<String> {
let McpToolIdentity { server, tool } = parse_exported_name(name, ExportedNameDialect::Rho)?;
Some(
format!("mcp__{server}__{tool}")
.replace(|character: char| !character.is_ascii_alphanumeric(), "_"),
)
}
fn decode_rho_component(component: &str) -> String {
let Some(hex) = component.strip_prefix("_rho_") else {
return component.to_string();
};
if hex.is_empty() || hex.len() % 2 != 0 {
return component.to_string();
}
let mut bytes = Vec::with_capacity(hex.len() / 2);
for pair in hex.as_bytes().chunks(2) {
let hi = (pair[0] as char).to_digit(16);
let lo = (pair[1] as char).to_digit(16);
match (hi, lo) {
(Some(hi), Some(lo)) => bytes.push((hi * 16 + lo) as u8),
_ => return component.to_string(),
}
}
String::from_utf8(bytes).unwrap_or_else(|_| component.to_string())
}
#[cfg(test)]
#[path = "exported_name_tests.rs"]
mod tests;