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 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}