magi-code 0.77.1

Repository-aware CLI coding agent for terminal work
Documentation
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}"))
}
#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn round_trip() {
        for (server, tool) in [
            ("server", "search"),
            ("server", "_search"),
            ("server-name", "tool_name"),
        ] {
            let n = QualifiedMcpToolName::new(server, tool).unwrap();
            let parsed = QualifiedMcpToolName::parse(n.as_str()).unwrap();
            assert_eq!(parsed, n);
            assert_eq!(parsed.server(), server);
            assert_eq!(parsed.tool(), tool);
        }
    }

    #[test]
    fn rejects_ambiguous_trailing_server_underscore() {
        let tool_with_leading_underscore = QualifiedMcpToolName::new("a", "_tool").unwrap();
        assert_eq!(tool_with_leading_underscore.as_str(), "mcp__a___tool");
        assert_eq!(
            QualifiedMcpToolName::parse(tool_with_leading_underscore.as_str()).unwrap(),
            tool_with_leading_underscore
        );

        let error = QualifiedMcpToolName::new("bad_", "tool")
            .unwrap_err()
            .to_string();
        assert!(error.contains("must not end with '_'"), "{error}");
        assert!(error.contains("ambiguous"), "{error}");
    }

    #[test]
    fn rejects_bad_names() {
        for n in [
            "mcp__server",
            "mcp__server__tool__extra",
            "mcp__bad/name__tool",
            "mcp____tool",
        ] {
            let error = QualifiedMcpToolName::parse(n).unwrap_err().to_string();
            assert!(error.contains(n), "{error}");
        }
    }
    #[test]
    fn rejects_unicode_components_with_detail() {
        let error = QualifiedMcpToolName::parse("mcp__server__é")
            .unwrap_err()
            .to_string();
        assert!(
            error.contains("mcp__server__é") && error.contains("ASCII"),
            "{error}"
        );
    }
    #[test]
    fn accepts_exact_byte_cap_and_rejects_one_over() {
        let tool = "x".repeat(64 - "mcp__s__".len());
        assert!(QualifiedMcpToolName::new("s", &tool).is_ok());
        assert!(QualifiedMcpToolName::new("s", &(tool + "x")).is_err());
    }
    #[test]
    fn ordering_uses_full_qualified_text() {
        let mut names = [
            QualifiedMcpToolName::new("a-", "tool").unwrap(),
            QualifiedMcpToolName::new("a", "tool").unwrap(),
        ];
        names.sort();
        assert_eq!(
            names.iter().map(|name| name.as_str()).collect::<Vec<_>>(),
            vec!["mcp__a-__tool", "mcp__a__tool"]
        );
    }
}