codetether_agent/tool/
mcp_bridge.rs1use super::{Tool, ToolResult};
7use anyhow::Result;
8use async_trait::async_trait;
9use serde_json::{Value, json};
10
11pub 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 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}