//! MCP Tool Adapter - wraps MCP tools as PicoBot tools use async_trait::async_trait; use std::sync::Arc; use rmcp::model::Tool; use crate::mcp::client::McpClientManager; use crate::tools::traits::{Tool as PicoBotTool, ToolResult}; /// Wrapper that adapts an MCP tool to PicoBot's Tool trait pub struct McpToolWrapper { /// The MCP client manager manager: Arc, /// The server name this tool belongs to server_name: String, /// The original tool name on the MCP server tool_name: String, /// The full tool name with namespace (mcp_{server}_{tool}) full_name: String, /// Tool information from MCP server tool_info: Tool, } impl McpToolWrapper { /// Create a new tool wrapper pub fn new( manager: Arc, server_name: String, tool_info: Tool, ) -> Self { let tool_name = tool_info.name.clone().into_owned(); let full_name = format!("mcp_{}_{}", server_name, tool_name); Self { manager, server_name, tool_name, full_name, tool_info, } } /// Get the server name pub fn server_name(&self) -> &str { &self.server_name } /// Get the original tool name pub fn original_name(&self) -> &str { &self.tool_name } } #[async_trait] impl PicoBotTool for McpToolWrapper { fn name(&self) -> &str { &self.full_name } fn description(&self) -> &str { self.tool_info.description.as_deref().unwrap_or("MCP tool") } fn parameters_schema(&self) -> serde_json::Value { // Convert Arc to serde_json::Value let schema = (*self.tool_info.input_schema).clone(); serde_json::Value::Object(schema) } async fn execute(&self, args: serde_json::Value) -> anyhow::Result { tracing::debug!( server = %self.server_name, tool = %self.tool_name, "Calling MCP tool" ); let result = self .manager .call_tool(&self.server_name, &self.tool_name, args) .await?; // Convert MCP CallToolResult to PicoBot ToolResult let output = extract_text_content(&result); let is_error = result.is_error.unwrap_or(false); Ok(ToolResult { success: !is_error, output, error: if is_error { Some("MCP tool returned error".to_string()) } else { None }, }) } fn read_only(&self) -> bool { // MCP tools may or may not be read-only; we default to false // This could be enhanced if MCP servers provide this info via annotations false } } /// Extract text content from MCP CallToolResult fn extract_text_content(result: &rmcp::model::CallToolResult) -> String { let mut text_parts = Vec::new(); for content in &result.content { if let Some(text) = content.as_text() { text_parts.push(text.text.clone()); } } if text_parts.is_empty() { // No text content found, try to serialize the whole result serde_json::to_string_pretty(&result).unwrap_or_else(|_| "Empty result".to_string()) } else { text_parts.join("\n") } } /// Register all MCP tools from connected servers into a tool registry pub async fn register_mcp_tools( manager: Arc, registry: &mut crate::tools::registry::ToolRegistry, ) -> anyhow::Result<()> { let all_tools = manager.all_tools().await; for (server_name, tool_info) in all_tools { let wrapper = McpToolWrapper::new( manager.clone(), server_name.clone(), tool_info, ); tracing::info!( name = %wrapper.name(), server = %server_name, "Registering MCP tool" ); registry.register(wrapper); } Ok(()) } #[cfg(test)] mod tests { use super::*; use rmcp::model::{CallToolResult, Content}; #[test] fn test_extract_text_content_from_text() { let result = CallToolResult::success(vec![ Content::text("Hello"), Content::text("World"), ]); let text = extract_text_content(&result); assert_eq!(text, "Hello\nWorld"); } #[test] fn test_extract_text_content_empty() { let result = CallToolResult::success(vec![]); let text = extract_text_content(&result); assert!(text.contains("Empty result")); } #[test] fn test_mcp_tool_wrapper_name() { let manager = Arc::new(McpClientManager::new()); let tool_info = Tool { name: "echo".into(), description: Some("Echo tool".into()), input_schema: serde_json::json!({"type": "object"}).as_object().unwrap().clone(), ..Default::default() }; let wrapper = McpToolWrapper::new(manager, "filesystem".to_string(), tool_info); assert_eq!(wrapper.name(), "mcp_filesystem_echo"); assert_eq!(wrapper.original_name(), "echo"); assert_eq!(wrapper.server_name(), "filesystem"); } }