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