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