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