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 client =
93                    crate::mcp::McpClient::connect_subprocess(cmd, &cmd_args).await?;
94                let tools = client.tools().await;
95                let result: Vec<Value> = tools
96                    .iter()
97                    .map(|t| {
98                        json!({
99                            "name": t.name,
100                            "description": t.description,
101                            "input_schema": t.input_schema,
102                        })
103                    })
104                    .collect();
105                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 client =
120                    crate::mcp::McpClient::connect_subprocess(cmd, &cmd_args).await?;
121                let result = client.call_tool(tool_name, arguments).await?;
122                client.close().await?;
123
124                let output: String = result
125                    .content
126                    .iter()
127                    .map(|c| match c {
128                        crate::mcp::ToolContent::Text { text } => text.clone(),
129                        crate::mcp::ToolContent::Image { data, mime_type } => {
130                            format!("[image: {} ({} bytes)]", mime_type, data.len())
131                        }
132                        crate::mcp::ToolContent::Resource { resource } => {
133                            serde_json::to_string(resource).unwrap_or_default()
134                        }
135                    })
136                    .collect::<Vec<_>>()
137                    .join("\n");
138
139                if result.is_error {
140                    Ok(ToolResult::error(output))
141                } else {
142                    Ok(ToolResult::success(output))
143                }
144            }
145            "list_resources" => {
146                let client =
147                    crate::mcp::McpClient::connect_subprocess(cmd, &cmd_args).await?;
148                let resources = client.list_resources().await?;
149                let result: Vec<Value> = resources
150                    .iter()
151                    .map(|r| {
152                        json!({
153                            "uri": r.uri,
154                            "name": r.name,
155                            "description": r.description,
156                            "mime_type": r.mime_type,
157                        })
158                    })
159                    .collect();
160                client.close().await?;
161                Ok(ToolResult::success(serde_json::to_string_pretty(&result)?))
162            }
163            "read_resource" => {
164                let uri = args["resource_uri"]
165                    .as_str()
166                    .ok_or_else(|| anyhow::anyhow!("Missing 'resource_uri' for read_resource"))?;
167
168                let client =
169                    crate::mcp::McpClient::connect_subprocess(cmd, &cmd_args).await?;
170                let result = client.read_resource(uri).await?;
171                client.close().await?;
172                Ok(ToolResult::success(serde_json::to_string_pretty(&result)?))
173            }
174            _ => Ok(ToolResult::error(format!("Unknown action: {}", action))),
175        }
176    }
177}