Skip to main content

codetether_agent/tool/
mcp_bridge.rs

1//! MCP Bridge Tool: Connect to and invoke tools from external MCP servers
2//!
3//! This tool enables agents (including the A2A worker) to connect to external
4//! MCP (Model Context Protocol) servers and invoke their tools.
5
6use super::{Tool, ToolResult};
7use anyhow::Result;
8use async_trait::async_trait;
9use serde_json::{Value, json};
10
11/// MCP Bridge Tool - Connect to external MCP servers and call their tools
12pub struct McpBridgeTool;
13
14impl Default for McpBridgeTool {
15    fn default() -> Self {
16        Self::new()
17    }
18}
19
20impl McpBridgeTool {
21    pub fn new() -> Self {
22        Self
23    }
24}
25
26#[async_trait]
27impl Tool for McpBridgeTool {
28    fn id(&self) -> &str {
29        "mcp"
30    }
31
32    fn name(&self) -> &str {
33        "MCP Bridge"
34    }
35
36    fn description(&self) -> &str {
37        "Connect to an MCP (Model Context Protocol) server and invoke its tools. \
38         Actions: 'list_tools' to discover available tools from an MCP server, \
39         'call_tool' to invoke a specific tool, 'list_resources' to list available resources, \
40         'read_resource' to read a resource by URI."
41    }
42
43    fn parameters(&self) -> Value {
44        json!({
45            "type": "object",
46            "properties": {
47                "action": {
48                    "type": "string",
49                    "description": "Action to perform: list_tools, call_tool, list_resources, read_resource",
50                    "enum": ["list_tools", "call_tool", "list_resources", "read_resource"]
51                },
52                "command": {
53                    "type": "string",
54                    "description": "Command to spawn the MCP server process (e.g. 'npx -y @modelcontextprotocol/server-filesystem /path'). Required for list_tools and list_resources."
55                },
56                "tool_name": {
57                    "type": "string",
58                    "description": "Name of the MCP tool to call (required for call_tool)"
59                },
60                "arguments": {
61                    "type": "object",
62                    "description": "Arguments to pass to the MCP tool (for call_tool)"
63                },
64                "resource_uri": {
65                    "type": "string",
66                    "description": "URI of the resource to read (for read_resource)"
67                }
68            },
69            "required": ["action", "command"]
70        })
71    }
72
73    async fn execute(&self, args: Value) -> Result<ToolResult> {
74        let action = args["action"]
75            .as_str()
76            .ok_or_else(|| anyhow::anyhow!("Missing 'action' parameter"))?;
77        let command = args["command"]
78            .as_str()
79            .ok_or_else(|| anyhow::anyhow!("Missing 'command' parameter"))?;
80
81        // Parse command into parts
82        let parts: Vec<&str> = command.split_whitespace().collect();
83        if parts.is_empty() {
84            return Ok(ToolResult::error("Empty command"));
85        }
86
87        let cmd = parts[0];
88        let cmd_args: Vec<&str> = parts[1..].to_vec();
89
90        match action {
91            "list_tools" => {
92                let manager =
93                    super::mcp_tools::McpToolManager::connect_subprocess(cmd, &cmd_args).await?;
94                let wrappers = manager.wrappers().await;
95                let result: Vec<Value> = wrappers
96                    .iter()
97                    .map(|t| {
98                        json!({
99                            "name": t.name(),
100                            "description": t.description(),
101                            "input_schema": t.parameters(),
102                        })
103                    })
104                    .collect();
105                manager.client().close().await?;
106                Ok(ToolResult::success(serde_json::to_string_pretty(&result)?))
107            }
108            "call_tool" => {
109                let tool_name = args["tool_name"]
110                    .as_str()
111                    .ok_or_else(|| anyhow::anyhow!("Missing 'tool_name' for call_tool"))?;
112                let arguments = args["arguments"].clone();
113                let arguments = if arguments.is_null() {
114                    json!({})
115                } else {
116                    arguments
117                };
118
119                let manager =
120                    super::mcp_tools::McpToolManager::connect_subprocess(cmd, &cmd_args).await?;
121                let client = manager.client();
122                let result = client.call_tool(tool_name, arguments).await?;
123                client.close().await?;
124
125                let output: String = result
126                    .content
127                    .iter()
128                    .map(|c| match c {
129                        crate::mcp::ToolContent::Text { text } => text.clone(),
130                        crate::mcp::ToolContent::Image { data, mime_type } => {
131                            format!("[image: {} ({} bytes)]", mime_type, data.len())
132                        }
133                        crate::mcp::ToolContent::Resource { resource } => {
134                            serde_json::to_string(resource).unwrap_or_default()
135                        }
136                    })
137                    .collect::<Vec<_>>()
138                    .join("\n");
139
140                if result.is_error {
141                    Ok(ToolResult::error(output))
142                } else {
143                    Ok(ToolResult::success(output))
144                }
145            }
146            "list_resources" => {
147                let manager =
148                    super::mcp_tools::McpToolManager::connect_subprocess(cmd, &cmd_args).await?;
149                let client = manager.client();
150                let resources = client.list_resources().await?;
151                let result: Vec<Value> = resources
152                    .iter()
153                    .map(|r| {
154                        json!({
155                            "uri": r.uri,
156                            "name": r.name,
157                            "description": r.description,
158                            "mime_type": r.mime_type,
159                        })
160                    })
161                    .collect();
162                client.close().await?;
163                Ok(ToolResult::success(serde_json::to_string_pretty(&result)?))
164            }
165            "read_resource" => {
166                let uri = args["resource_uri"]
167                    .as_str()
168                    .ok_or_else(|| anyhow::anyhow!("Missing 'resource_uri' for read_resource"))?;
169
170                let manager =
171                    super::mcp_tools::McpToolManager::connect_subprocess(cmd, &cmd_args).await?;
172                let client = manager.client();
173                let result = client.read_resource(uri).await?;
174                client.close().await?;
175                Ok(ToolResult::success(serde_json::to_string_pretty(&result)?))
176            }
177            _ => Ok(ToolResult::error(format!("Unknown action: {}", action))),
178        }
179    }
180}