1use super::client::McpClient;
35use super::types::{McpContent, McpError, McpToolInfo};
36use crate::types::{AgentTool, Content, ToolContext, ToolError, ToolResult};
37use async_trait::async_trait;
38use std::sync::Arc;
39use tokio::sync::Mutex;
40
41pub struct McpToolAdapter {
43 client: Arc<Mutex<McpClient>>, tool: McpToolInfo, prefix: Option<String>,
47}
48
49impl McpToolAdapter {
50 pub fn new(client: Arc<Mutex<McpClient>>, tool: McpToolInfo) -> Self {
52 Self {
53 client,
54 tool,
55 prefix: None,
56 }
57 }
58
59 pub fn with_prefix(mut self, prefix: impl Into<String>) -> Self {
61 self.prefix = Some(prefix.into());
62 self
63 }
64
65 pub async fn from_client(
81 client: Arc<Mutex<McpClient>>, ) -> Result<Vec<Self>, McpError> {
83 let tools = client.lock().await.list_tools().await?;
85 Ok(tools
86 .into_iter()
87 .map(|tool| McpToolAdapter::new(client.clone(), tool)) .collect())
89 }
90
91 pub async fn from_client_with_prefix(
93 client: Arc<Mutex<McpClient>>, prefix: impl Into<String>, ) -> Result<Vec<Self>, McpError> {
96 let prefix = prefix.into();
97 let tools = client.lock().await.list_tools().await?;
98 Ok(tools
99 .into_iter()
100 .map(|tool| McpToolAdapter::new(client.clone(), tool).with_prefix(prefix.clone()))
101 .collect())
102 }
103}
104
105#[async_trait]
106impl AgentTool for McpToolAdapter {
107 fn name(&self) -> &str {
108 &self.tool.name
111 }
112
113 fn label(&self) -> &str {
114 &self.tool.name
115 }
116
117 fn description(&self) -> &str {
118 self.tool
119 .description
120 .as_deref()
121 .unwrap_or("MCP tool (no description)")
122 }
123
124 fn parameters_schema(&self) -> serde_json::Value {
125 if self.tool.input_schema.is_null() {
126 serde_json::json!({"type": "object", "properties": {}})
127 } else {
128 self.tool.input_schema.clone()
129 }
130 }
131
132 async fn execute(
133 &self,
134 params: serde_json::Value, _ctx: ToolContext, ) -> Result<ToolResult, ToolError> {
137 let client = self.client.lock().await;
138 let result = client
139 .call_tool(&self.tool.name, params)
140 .await
141 .map_err(|e| ToolError::Failed(format!("MCP call failed: {}", e)))?;
142
143 if result.is_error {
144 let error_text = result
145 .content
146 .iter()
147 .filter_map(|c| match c {
148 McpContent::Text { text } => Some(text.as_str()),
149 _ => None,
150 })
151 .collect::<Vec<_>>()
152 .join("\n");
153 return Err(ToolError::Failed(error_text));
154 }
155
156 let content: Vec<Content> = result
157 .content
158 .into_iter()
159 .map(|c| match c {
160 McpContent::Text { text } => Content::Text { text },
161 McpContent::Image { data, mime_type } => Content::Image { data, mime_type },
162 })
163 .collect();
164
165 Ok(ToolResult {
166 content,
167 details: serde_json::Value::Null,
168 child_loop_id: None,
169 })
170 }
171}
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176 use crate::mcp::transport::McpTransport;
177 use crate::mcp::types::*;
178
179 struct MockTransport {
181 responses: std::sync::Mutex<Vec<JsonRpcResponse>>,
182 }
183
184 impl MockTransport {
185 fn new(responses: Vec<JsonRpcResponse>) -> Self {
186 Self {
187 responses: std::sync::Mutex::new(responses),
188 }
189 }
190 }
191
192 #[async_trait]
193 impl McpTransport for MockTransport {
194 async fn send(&self, _request: JsonRpcRequest) -> Result<JsonRpcResponse, McpError> {
195 let mut responses = self.responses.lock().unwrap();
196 if responses.is_empty() {
197 Err(McpError::ConnectionClosed)
198 } else {
199 Ok(responses.remove(0))
200 }
201 }
202
203 async fn close(&self) -> Result<(), McpError> {
204 Ok(())
205 }
206 }
207
208 fn ok_response(id: u64, result: serde_json::Value) -> JsonRpcResponse {
209 JsonRpcResponse {
210 jsonrpc: "2.0".into(),
211 id: Some(id),
212 result: Some(result),
213 error: None,
214 }
215 }
216
217 #[tokio::test]
218 async fn test_tool_adapter_wraps_mcp_tool() {
219 let tool_info = McpToolInfo {
220 name: "read_file".into(),
221 description: Some("Read a file from disk".into()),
222 input_schema: serde_json::json!({
223 "type": "object",
224 "properties": {
225 "path": {"type": "string"}
226 },
227 "required": ["path"]
228 }),
229 };
230
231 let transport = MockTransport::new(vec![
233 ok_response(
235 1,
236 serde_json::json!({
237 "content": [{"type": "text", "text": "file contents"}],
238 "isError": false
239 }),
240 ),
241 ]);
242
243 let client = McpClient::from_transport(Box::new(transport));
244 let client = Arc::new(Mutex::new(client));
245
246 let adapter = McpToolAdapter::new(client, tool_info);
247
248 assert_eq!(adapter.name(), "read_file");
249 assert_eq!(adapter.description(), "Read a file from disk");
250
251 let schema = adapter.parameters_schema();
252 assert_eq!(schema["type"], "object");
253
254 let result = adapter
255 .execute(
256 serde_json::json!({"path": "/tmp/test"}),
257 ToolContext {
258 tool_call_id: "tc-1".into(),
259 tool_name: "read_file".into(),
260 cancel: tokio_util::sync::CancellationToken::new(),
261 on_update: None,
262 on_progress: None,
263 },
264 )
265 .await
266 .unwrap();
267
268 assert_eq!(result.content.len(), 1);
269 if let Content::Text { text } = &result.content[0] {
270 assert_eq!(text, "file contents");
271 } else {
272 panic!("Expected text content");
273 }
274 }
275
276 #[tokio::test]
277 async fn test_tool_adapter_handles_error() {
278 let tool_info = McpToolInfo {
279 name: "fail_tool".into(),
280 description: None,
281 input_schema: serde_json::Value::Null,
282 };
283
284 let transport = MockTransport::new(vec![ok_response(
285 1,
286 serde_json::json!({
287 "content": [{"type": "text", "text": "something went wrong"}],
288 "isError": true
289 }),
290 )]);
291
292 let client = McpClient::from_transport(Box::new(transport));
293 let client = Arc::new(Mutex::new(client));
294
295 let adapter = McpToolAdapter::new(client, tool_info);
296 assert_eq!(adapter.description(), "MCP tool (no description)");
297
298 let result = adapter
299 .execute(
300 serde_json::json!({}),
301 ToolContext {
302 tool_call_id: "tc-1".into(),
303 tool_name: "fail_tool".into(),
304 cancel: tokio_util::sync::CancellationToken::new(),
305 on_update: None,
306 on_progress: None,
307 },
308 )
309 .await;
310 assert!(result.is_err());
311 }
312
313 #[tokio::test]
314 async fn test_from_client_creates_adapters() {
315 let transport = MockTransport::new(vec![ok_response(
317 1,
318 serde_json::json!({
319 "tools": [
320 {"name": "tool_a", "description": "Tool A", "inputSchema": {"type": "object"}},
321 {"name": "tool_b", "description": "Tool B", "inputSchema": {"type": "object"}}
322 ]
323 }),
324 )]);
325
326 let client = McpClient::from_transport(Box::new(transport));
327 let client = Arc::new(Mutex::new(client));
328
329 let adapters = McpToolAdapter::from_client(client).await.unwrap();
330 assert_eq!(adapters.len(), 2);
331 assert_eq!(adapters[0].name(), "tool_a");
332 assert_eq!(adapters[1].name(), "tool_b");
333 }
334}