Skip to main content

agent_code_lib/tools/
mcp_proxy.rs

1//! MCP proxy tool: bridges MCP server tools into the local tool system.
2//!
3//! Each MCP tool discovered from a server is wrapped as a local `Tool`
4//! implementation that proxies calls through the MCP client.
5
6use async_trait::async_trait;
7use std::sync::Arc;
8use tokio::sync::Mutex;
9
10use super::{Tool, ToolContext, ToolResult};
11use crate::error::ToolError;
12use crate::services::mcp::{McpClient, McpTool};
13
14/// A tool backed by an MCP server. Proxies `call()` to the server
15/// via `tools/call` JSON-RPC and converts the response.
16pub struct McpProxyTool {
17    /// The MCP tool metadata (name, description, schema).
18    definition: McpTool,
19    /// Qualified name: `mcp__{server}__{tool}` for uniqueness.
20    qualified_name: String,
21    /// The MCP client connection (shared across all tools from this server).
22    client: Arc<Mutex<McpClient>>,
23    /// Original server name for display.
24    server_name: String,
25}
26
27impl McpProxyTool {
28    pub fn new(definition: McpTool, server_name: &str, client: Arc<Mutex<McpClient>>) -> Self {
29        let qualified_name = format!(
30            "mcp__{}__{}",
31            normalize_name(server_name),
32            normalize_name(&definition.name),
33        );
34        Self {
35            definition,
36            qualified_name,
37            client,
38            server_name: server_name.to_string(),
39        }
40    }
41}
42
43#[async_trait]
44impl Tool for McpProxyTool {
45    fn name(&self) -> &'static str {
46        // Leak the string to get a &'static str. This is fine because
47        // MCP tools live for the duration of the session.
48        Box::leak(self.qualified_name.clone().into_boxed_str())
49    }
50
51    fn description(&self) -> &'static str {
52        let desc = self
53            .definition
54            .description
55            .clone()
56            .unwrap_or_else(|| format!("MCP tool from {}", self.server_name));
57        Box::leak(desc.into_boxed_str())
58    }
59
60    fn input_schema(&self) -> serde_json::Value {
61        self.definition.input_schema.clone()
62    }
63
64    fn is_read_only(&self) -> bool {
65        false // We can't know — assume mutation is possible.
66    }
67
68    fn is_concurrency_safe(&self) -> bool {
69        false // MCP servers may have internal state.
70    }
71
72    async fn call(
73        &self,
74        input: serde_json::Value,
75        _ctx: &ToolContext,
76    ) -> Result<ToolResult, ToolError> {
77        let client = self.client.lock().await;
78
79        let result = client
80            .call_tool(&self.definition.name, input)
81            .await
82            .map_err(|e| ToolError::ExecutionFailed(format!("MCP call failed: {e}")))?;
83
84        // Convert MCP response to our ToolResult.
85        let content = result
86            .content
87            .iter()
88            .filter_map(|c| match c {
89                crate::services::mcp::McpContent::Text { text } => Some(text.as_str()),
90                _ => None,
91            })
92            .collect::<Vec<_>>()
93            .join("\n");
94
95        Ok(ToolResult {
96            content: if content.is_empty() {
97                "(no output)".to_string()
98            } else {
99                content
100            },
101            is_error: result.is_error,
102        })
103    }
104}
105
106/// Normalize a name for use in qualified tool names (lowercase, replace spaces/special chars).
107fn normalize_name(s: &str) -> String {
108    s.chars()
109        .map(|c| {
110            if c.is_alphanumeric() {
111                c.to_ascii_lowercase()
112            } else {
113                '_'
114            }
115        })
116        .collect()
117}
118
119/// Create proxy tools from all tools discovered on an MCP server.
120pub fn create_proxy_tools(
121    server_name: &str,
122    mcp_tools: &[McpTool],
123    client: Arc<Mutex<McpClient>>,
124) -> Vec<Arc<dyn Tool>> {
125    mcp_tools
126        .iter()
127        .map(|t| {
128            Arc::new(McpProxyTool::new(t.clone(), server_name, client.clone())) as Arc<dyn Tool>
129        })
130        .collect()
131}