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, obj).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(
232 &self,
233 action: &str,
234 obj: &serde_json::Map<String, Value>,
235 ) -> Result<AgentToolResult, String> {
236 let server = obj.get("server").and_then(|v| v.as_str()).unwrap_or("");
237 match action {
238 "ui-messages" => Ok(AgentToolResult::success(
239 "No UI session messages available.",
240 )),
241 "list-resources" => {
242 if server.is_empty() {
243 return Ok(AgentToolResult::error(String::from(
244 "list-resources requires 'server'",
245 )));
246 }
247 match self.manager.list_resources(server).await {
248 Ok(resources) => Ok(AgentToolResult::success(
249 serde_json::to_string_pretty(&resources).unwrap_or_default(),
250 )),
251 Err(e) => Ok(AgentToolResult::error(format!(
252 "list_resources('{}') failed: {}",
253 server, e
254 ))),
255 }
256 }
257 "read-resource" => {
258 let uri = obj.get("uri").and_then(|v| v.as_str()).unwrap_or("");
259 if server.is_empty() || uri.is_empty() {
260 return Ok(AgentToolResult::error(String::from(
261 "read-resource requires 'server' and 'uri'",
262 )));
263 }
264 match self.manager.read_resource(server, uri).await {
265 Ok(content) => Ok(AgentToolResult::success(content::transform_mcp_content(
266 &content,
267 ))),
268 Err(e) => Ok(AgentToolResult::error(format!(
269 "read_resource('{}','{}') failed: {}",
270 server, uri, e
271 ))),
272 }
273 }
274 "list-prompts" => {
275 if server.is_empty() {
276 return Ok(AgentToolResult::error(String::from(
277 "list-prompts requires 'server'",
278 )));
279 }
280 match self.manager.list_prompts(server).await {
281 Ok(prompts) => {
282 let summary: Vec<String> = prompts
283 .iter()
284 .map(|p| {
285 format!(
286 "- {}{}",
287 p.name,
288 p.description
289 .as_deref()
290 .map(|d| format!(" — {}", d))
291 .unwrap_or_default()
292 )
293 })
294 .collect();
295 Ok(AgentToolResult::success(summary.join("\n")))
296 }
297 Err(e) => Ok(AgentToolResult::error(format!(
298 "list_prompts('{}') failed: {}",
299 server, e
300 ))),
301 }
302 }
303 "get-prompt" => {
304 let name = obj.get("name").and_then(|v| v.as_str()).unwrap_or("");
305 let arguments = obj
306 .get("arguments")
307 .and_then(|v| v.as_object())
308 .map(|m| {
309 m.iter()
310 .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
311 .collect::<std::collections::HashMap<_, _>>()
312 })
313 .unwrap_or_default();
314 if server.is_empty() || name.is_empty() {
315 return Ok(AgentToolResult::error(String::from(
316 "get-prompt requires 'server' and 'name'",
317 )));
318 }
319 match self.manager.get_prompt(server, name, arguments).await {
320 Ok(messages) => Ok(AgentToolResult::success(
321 serde_json::to_string_pretty(&messages).unwrap_or_default(),
322 )),
323 Err(e) => Ok(AgentToolResult::error(format!(
324 "get_prompt('{}','{}') failed: {}",
325 server, name, e
326 ))),
327 }
328 }
329 _ => Ok(AgentToolResult::error(format!(
330 "Unknown action: '{}'. Supported: 'ui-messages', 'list-resources', 'read-resource', 'list-prompts', 'get-prompt'",
331 action
332 ))),
333 }
334 }
335}