use super::{McpError, McpResult};
use std::{
borrow::Borrow,
cmp::Ordering,
fmt,
hash::{Hash, Hasher},
};
pub(crate) const MCP_QUALIFIED_NAME_MAX_BYTES: usize = 64;
const PREFIX: &str = "mcp__";
const SEPARATOR: &str = "__";
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct QualifiedMcpToolName {
server: String,
tool: String,
qualified: String,
}
impl QualifiedMcpToolName {
pub(crate) fn new(server: &str, tool: &str) -> McpResult<Self> {
validate_component("server", server)?;
if server.ends_with('_') {
return Err(McpError::Config(format!(
"MCP server name '{server}' must not end with '_' because mcp__<server>__<tool> would be ambiguous; choose a name that does not end with '_'"
)));
}
validate_component("tool", tool)?;
let qualified = format!("{PREFIX}{server}{SEPARATOR}{tool}");
if qualified.len() > MCP_QUALIFIED_NAME_MAX_BYTES {
return Err(McpError::Config(format!(
"MCP qualified tool name exceeds {MCP_QUALIFIED_NAME_MAX_BYTES} bytes"
)));
}
Ok(Self {
server: server.to_string(),
tool: tool.to_string(),
qualified,
})
}
pub(crate) fn parse(name: &str) -> McpResult<Self> {
let rest = name.strip_prefix(PREFIX).ok_or_else(|| invalid(name))?;
let (server, tool) = rest.split_once(SEPARATOR).ok_or_else(|| invalid(name))?;
if tool.contains(SEPARATOR) {
return Err(invalid(name));
}
let parsed = Self::new(server, tool).map_err(|error| invalid_with_detail(name, error))?;
if parsed.as_str() != name {
return Err(invalid(name));
}
Ok(parsed)
}
pub(crate) fn server(&self) -> &str {
&self.server
}
pub(crate) fn tool(&self) -> &str {
&self.tool
}
pub(crate) fn as_str(&self) -> &str {
&self.qualified
}
}
impl Hash for QualifiedMcpToolName {
fn hash<H: Hasher>(&self, state: &mut H) {
self.as_str().hash(state);
}
}
impl Borrow<str> for QualifiedMcpToolName {
fn borrow(&self) -> &str {
self.as_str()
}
}
impl Ord for QualifiedMcpToolName {
fn cmp(&self, other: &Self) -> Ordering {
self.as_str().cmp(other.as_str())
}
}
impl PartialOrd for QualifiedMcpToolName {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl fmt::Display for QualifiedMcpToolName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
fn validate_component(kind: &str, value: &str) -> McpResult<()> {
if value.is_empty() {
return Err(McpError::Config(format!(
"MCP {kind} name must not be empty"
)));
}
if value.contains(SEPARATOR) {
return Err(McpError::Config(format!(
"MCP {kind} name '{value}' must not contain '__'"
)));
}
if !value
.bytes()
.all(|b| b.is_ascii_alphanumeric() || b == b'_' || b == b'-')
{
return Err(McpError::Config(format!(
"MCP {kind} name '{value}' must contain only ASCII letters, digits, '_' or '-'"
)));
}
Ok(())
}
fn invalid(name: &str) -> McpError {
McpError::Config(format!(
"invalid MCP qualified tool name '{name}'; expected exactly mcp__<server>__<tool>"
))
}
fn invalid_with_detail(name: &str, error: McpError) -> McpError {
McpError::Config(format!("invalid MCP qualified tool name '{name}': {error}"))
}