1use crate::tools::{AgentTool, AgentToolResult, ToolContext};
12use async_trait::async_trait;
13use serde_json::Value;
14use std::sync::Arc;
15use tokio::sync::oneshot;
16
17use super::McpManager;
18use super::content;
19
20pub struct McpTool {
28 manager: Arc<McpManager>,
29}
30
31impl std::fmt::Debug for McpTool {
32 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33 f.debug_struct("McpTool").finish()
34 }
35}
36
37impl McpTool {
38 pub fn new(manager: Arc<McpManager>) -> Self {
40 Self { manager }
41 }
42
43 pub fn manager(&self) -> Arc<McpManager> {
46 Arc::clone(&self.manager)
47 }
48}
49
50#[async_trait]
51impl AgentTool for McpTool {
52 fn name(&self) -> &str {
53 "mcp"
54 }
55
56 fn label(&self) -> &str {
57 "MCP"
58 }
59
60 fn description(&self) -> &str {
61 "MCP gateway - connect to MCP servers and call their tools. Non-MCP Pi tools should be called directly, not through mcp.\n\nUsage:\n mcp({ }) → status\n mcp({ tool: \"name\", args: '{}' }) → call tool\n mcp({ connect: \"server\" }) → connect\n mcp({ search: \"query\" }) → search\n mcp({ describe: \"tool\" }) → describe\n mcp({ server: \"name\" }) → list tools\n\nMode: tool > connect > describe > search > server > status"
62 }
63
64 fn parameters_schema(&self) -> Value {
65 serde_json::json!({
66 "type": "object",
67 "properties": {
68 "tool": {
69 "type": "string",
70 "description": "Tool name to call (e.g. 'xcodebuild_list_sims')"
71 },
72 "args": {
73 "type": "string",
74 "description": "Arguments as JSON string (e.g. '{\"key\": \"value\"}')"
75 },
76 "connect": {
77 "type": "string",
78 "description": "Server name to connect (lazy connect + metadata refresh)"
79 },
80 "describe": {
81 "type": "string",
82 "description": "Tool name to describe (shows parameters)"
83 },
84 "search": {
85 "type": "string",
86 "description": "Search tools by name/description"
87 },
88 "regex": {
89 "type": "boolean",
90 "description": "Treat search as regex (default: substring match)"
91 },
92 "server": {
93 "type": "string",
94 "description": "Filter to specific server (also disambiguates tool calls)"
95 },
96 "action": {
97 "type": "string",
98 "description": "Action: 'ui-messages' to retrieve prompts/intents from UI sessions"
99 }
100 },
101 "additionalProperties": false
102 })
103 }
104
105 fn essential(&self) -> bool {
106 false
107 }
108
109 async fn execute(
110 &self,
111 _tool_call_id: &str,
112 params: Value,
113 _signal: Option<oneshot::Receiver<()>>,
114 _ctx: &ToolContext,
115 ) -> Result<AgentToolResult, String> {
116 let obj = params
117 .as_object()
118 .ok_or("Parameters must be a JSON object")?;
119
120 let parsed_args = if let Some(args_val) = obj.get("args").and_then(|v| v.as_str()) {
122 serde_json::from_str::<Value>(args_val)
123 .map_err(|e| format!("Invalid args JSON: {}", e))?
124 } else {
125 Value::Object(serde_json::Map::new())
126 };
127
128 if let Some(action) = obj.get("action").and_then(|v| v.as_str()) {
130 return self.handle_action(action).await;
131 }
132
133 if let Some(tool_name) = obj.get("tool").and_then(|v| v.as_str()) {
134 let server = obj.get("server").and_then(|v| v.as_str());
135 return self.handle_call(tool_name, parsed_args, server).await;
136 }
137
138 if let Some(server_name) = obj.get("connect").and_then(|v| v.as_str()) {
139 return self.handle_connect(server_name).await;
140 }
141
142 if let Some(tool_name) = obj.get("describe").and_then(|v| v.as_str()) {
143 return self.handle_describe(tool_name).await;
144 }
145
146 if let Some(query) = obj.get("search").and_then(|v| v.as_str()) {
147 let regex = obj.get("regex").and_then(|v| v.as_bool()).unwrap_or(false);
148 let server = obj.get("server").and_then(|v| v.as_str());
149 return self.handle_search(query, regex, server).await;
150 }
151
152 if let Some(server_name) = obj.get("server").and_then(|v| v.as_str()) {
153 return self.handle_list(server_name).await;
154 }
155
156 self.handle_status().await
158 }
159}
160
161impl McpTool {
164 async fn handle_status(&self) -> Result<AgentToolResult, String> {
165 let status = self.manager.status().await;
166 Ok(AgentToolResult::success(status))
167 }
168
169 async fn handle_connect(&self, server_name: &str) -> Result<AgentToolResult, String> {
170 let result = self
171 .manager
172 .connect(server_name)
173 .await
174 .map_err(|e| e.to_string())?;
175 Ok(AgentToolResult::success(result))
176 }
177
178 async fn handle_describe(&self, tool_name: &str) -> Result<AgentToolResult, String> {
179 let result = self
180 .manager
181 .describe(tool_name)
182 .await
183 .map_err(|e| e.to_string())?;
184 Ok(AgentToolResult::success(result))
185 }
186
187 async fn handle_search(
188 &self,
189 query: &str,
190 regex: bool,
191 server: Option<&str>,
192 ) -> Result<AgentToolResult, String> {
193 let result = self
194 .manager
195 .search(query, regex, server)
196 .await
197 .map_err(|e| e.to_string())?;
198 Ok(AgentToolResult::success(result))
199 }
200
201 async fn handle_list(&self, server_name: &str) -> Result<AgentToolResult, String> {
202 let result = self
203 .manager
204 .list_tools(server_name)
205 .await
206 .map_err(|e| e.to_string())?;
207 Ok(AgentToolResult::success(result))
208 }
209
210 async fn handle_call(
211 &self,
212 tool_name: &str,
213 args: Value,
214 server: Option<&str>,
215 ) -> Result<AgentToolResult, String> {
216 let result = self
217 .manager
218 .call_tool(tool_name, args, server)
219 .await
220 .map_err(|e| e.to_string())?;
221
222 if result.is_error {
223 let text = content::transform_mcp_content(&result.content);
224 Ok(AgentToolResult::error(format!("Error: {}", text)))
225 } else {
226 let text = content::transform_mcp_content(&result.content);
227 Ok(AgentToolResult::success(text))
228 }
229 }
230
231 async fn handle_action(&self, action: &str) -> Result<AgentToolResult, String> {
232 match action {
233 "ui-messages" => Ok(AgentToolResult::success(
234 "No UI session messages available.",
235 )),
236 _ => Ok(AgentToolResult::error(format!(
237 "Unknown action: '{}'. Supported: 'ui-messages'",
238 action
239 ))),
240 }
241 }
242}