magi-code 0.64.0

Repository-aware CLI coding agent for terminal work
Documentation
use crate::mcp::{
    McpError, McpResult,
    manager::{MCP_QUALIFIED_NAME_MAX_BYTES, qualified_name},
    protocol::Tool,
};
use serde_json::{Value, json};

pub(crate) fn provider_tool_definition(server_name: &str, tool: &Tool) -> McpResult<Value> {
    validate_tool_name(&tool.name)?;
    let name = qualified_name(server_name, &tool.name);
    if name.len() > MCP_QUALIFIED_NAME_MAX_BYTES {
        return Err(McpError::Config(format!(
            "MCP qualified tool name exceeds {MCP_QUALIFIED_NAME_MAX_BYTES} bytes"
        )));
    }
    let description = tool
        .description
        .as_deref()
        .or(tool.title.as_deref())
        .map(str::trim)
        .filter(|value| !value.is_empty())
        .map(ToString::to_string)
        .unwrap_or_else(|| format!("MCP tool from server {server_name}"));
    Ok(json!({
        "type": "function",
        "name": name,
        "description": description,
        "parameters": object_schema_or_default(&tool.input_schema),
    }))
}

pub(crate) fn validate_tool_name(name: &str) -> McpResult<()> {
    if name.is_empty() {
        return Err(McpError::Config(
            "MCP tool name must not be empty".to_string(),
        ));
    }
    if name.contains("__") {
        return Err(McpError::Config(format!(
            "MCP tool name '{name}' must not contain '__'"
        )));
    }
    if !name
        .bytes()
        .all(|byte| byte.is_ascii_alphanumeric() || byte == b'_' || byte == b'-')
    {
        return Err(McpError::Config(format!(
            "MCP tool name '{name}' must contain only ASCII letters, digits, '_' or '-'"
        )));
    }
    Ok(())
}

fn object_schema_or_default(schema: &Value) -> Value {
    if schema.as_object().and_then(|object| object.get("type"))
        == Some(&Value::String("object".to_string()))
    {
        schema.clone()
    } else {
        default_schema()
    }
}

fn default_schema() -> Value {
    json!({"type":"object","properties":{},"additionalProperties":true})
}

#[cfg(test)]
mod tests {
    use super::*;

    fn tool(name: &str, input_schema: Value) -> Tool {
        Tool {
            name: name.to_string(),
            title: Some("Title".to_string()),
            description: None,
            input_schema,
            output_schema: None,
            annotations: None,
        }
    }

    #[test]
    fn converts_tool_to_provider_definition() {
        let definition = provider_tool_definition(
            "mock",
            &Tool {
                description: Some("Echo input".to_string()),
                ..tool(
                    "echo",
                    json!({"type":"object","properties":{"text":{"type":"string"}}}),
                )
            },
        )
        .unwrap();
        assert_eq!(definition["name"], "mcp__mock__echo");
        assert_eq!(definition["description"], "Echo input");
        assert_eq!(definition["parameters"]["type"], "object");
    }

    #[test]
    fn falls_back_for_missing_description_and_non_object_schema() {
        let definition =
            provider_tool_definition("mock", &tool("echo", json!({"type":"string"}))).unwrap();
        assert_eq!(definition["description"], "Title");
        assert_eq!(
            definition["parameters"],
            json!({"type":"object","properties":{},"additionalProperties":true})
        );
    }

    #[test]
    fn rejects_unsafe_tool_names() {
        for name in ["bad/name", "bad__name", ""] {
            assert!(
                provider_tool_definition("mock", &tool(name, json!({"type":"object"}))).is_err()
            );
        }
    }
}