Skip to main content

agent_runtime/tools/
mcp_client.rs

1// MCP Client implementation using rust-mcp-sdk
2//
3// The Model Context Protocol (MCP) is a protocol for AI assistants to interact
4// with external tools and data sources. This module provides integration with
5// MCP servers via the rust-mcp-sdk.
6//
7// Current Status: PLACEHOLDER IMPLEMENTATION
8// The rust-mcp-sdk API structure is not yet fully understood. This module provides
9// a complete API surface that compiles, but the actual MCP protocol communication
10// needs to be implemented.
11//
12// To complete this implementation, we need to:
13// 1. Study rust-mcp-sdk documentation and examples
14// 2. Understand how to create clients and transports
15// 3. Implement the MCP protocol request/response cycle
16// 4. Handle tool discovery and execution
17//
18// For now, this provides a working skeleton that integrates with our tool system.
19
20use crate::tool::Tool;
21use crate::types::{JsonValue, ToolError, ToolResult};
22use async_trait::async_trait;
23use rust_mcp_sdk::{
24    mcp_client::{
25        client_runtime_core, ClientHandlerCore, McpClientOptions, ToMcpClientHandlerCore,
26    },
27    schema::{
28        CallToolRequestParams, ClientCapabilities, Implementation, InitializeRequestParams,
29        LATEST_PROTOCOL_VERSION,
30    },
31    McpClient as SdkMcpClient, StdioTransport, TransportOptions,
32};
33use std::collections::HashMap;
34use std::sync::Arc;
35
36/// MCP Client wrapper for connecting to MCP servers
37///
38/// Manages a connection to an MCP server and provides methods to:
39/// - Discover available tools
40/// - Execute tools remotely
41///
42/// # Example (when implemented)
43/// ```no_run
44/// # use agent_runtime::McpClient;
45/// # async fn example() -> Result<(), String> {
46/// // Connect to an MCP server via stdio
47/// let client = McpClient::new_stdio("npx", &["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]).await?;
48///
49/// // Discover tools
50/// let tools = client.list_tools().await?;
51/// println!("Found {} tools", tools.len());
52/// # Ok(())
53/// # }
54/// ```
55pub struct McpClient {
56    inner: Arc<dyn SdkMcpClient>,
57}
58
59impl McpClient {
60    /// Create a new MCP client connected to a server via stdio
61    ///
62    /// # Arguments
63    /// * `command` - The command to run (e.g., "npx", "python", "node")
64    /// * `args` - Arguments to pass (e.g., ["-y", "@modelcontextprotocol/server-filesystem", "/path"])
65    ///
66    /// # Example MCP Servers
67    /// - Filesystem: `npx -y @modelcontextprotocol/server-filesystem /tmp`
68    /// - SQLite: `npx -y @modelcontextprotocol/server-sqlite --db-path ./data.db`
69    /// - Web: `npx -y @modelcontextprotocol/server-fetch`
70    pub async fn new_stdio(command: &str, args: &[&str]) -> Result<Arc<Self>, String> {
71        // Create client details
72        let client_details = InitializeRequestParams {
73            capabilities: ClientCapabilities::default(),
74            client_info: Implementation {
75                name: "agent-runtime-mcp-client".into(),
76                version: env!("CARGO_PKG_VERSION").into(),
77                description: Some("MCP client for agent-runtime framework".into()),
78                title: None,
79                icons: vec![],
80                website_url: None,
81            },
82            protocol_version: LATEST_PROTOCOL_VERSION.into(),
83            meta: None,
84        };
85
86        // Create transport that launches the MCP server
87        let transport = StdioTransport::create_with_server_launch(
88            command,
89            args.iter().map(|s| s.to_string()).collect(),
90            None,
91            TransportOptions::default(),
92        )
93        .map_err(|e| format!("Failed to create transport: {}", e))?;
94
95        // Create a minimal handler
96        let handler = MinimalClientHandler {};
97
98        // Create and start the MCP client
99        let client = client_runtime_core::create_client(McpClientOptions {
100            client_details,
101            transport,
102            handler: handler.to_mcp_client_handler(),
103            task_store: None,
104            server_task_store: None,
105        });
106
107        client
108            .clone()
109            .start()
110            .await
111            .map_err(|e| format!("Failed to start MCP client: {}", e))?;
112
113        Ok(Arc::new(Self { inner: client }))
114    }
115
116    /// Discover all tools available on the connected MCP server
117    ///
118    /// Sends a `tools/list` request to the MCP server and parses the response.
119    pub async fn list_tools(&self) -> Result<Vec<McpToolInfo>, String> {
120        let response = self
121            .inner
122            .request_tool_list(None)
123            .await
124            .map_err(|e| format!("Failed to list tools: {}", e))?;
125
126        Ok(response
127            .tools
128            .into_iter()
129            .map(|tool| McpToolInfo {
130                name: tool.name,
131                description: tool.description.unwrap_or_default(),
132                input_schema: serde_json::to_value(&tool.input_schema).unwrap_or(JsonValue::Null),
133            })
134            .collect())
135    }
136
137    /// Call a tool on the MCP server
138    ///
139    /// Sends a `tools/call` request with the given arguments and waits for the result.
140    pub async fn call_tool(
141        &self,
142        name: &str,
143        arguments: HashMap<String, JsonValue>,
144    ) -> Result<JsonValue, String> {
145        let params = CallToolRequestParams {
146            name: name.to_string(),
147            arguments: Some(
148                arguments
149                    .into_iter()
150                    .collect::<serde_json::Map<String, JsonValue>>(),
151            ),
152            meta: None,
153            task: None,
154        };
155
156        let result = self
157            .inner
158            .request_tool_call(params)
159            .await
160            .map_err(|e| format!("MCP tool call failed: {}", e))?;
161
162        // Convert the result content to a JSON value
163        if let Some(content) = result.content.first() {
164            if let Ok(text_content) = content.as_text_content() {
165                Ok(JsonValue::String(text_content.text.clone()))
166            } else {
167                Ok(serde_json::to_value(content)
168                    .map_err(|e| format!("Failed to serialize result: {}", e))?)
169            }
170        } else {
171            Ok(JsonValue::Null)
172        }
173    }
174}
175
176/// Information about a tool discovered from an MCP server
177#[derive(Debug, Clone)]
178pub struct McpToolInfo {
179    pub name: String,
180    pub description: String,
181    pub input_schema: JsonValue,
182}
183
184/// Minimal handler for MCP client (we don't need custom message handling)
185struct MinimalClientHandler;
186
187use rust_mcp_sdk::schema::{
188    NotificationFromServer, ResultFromClient, RpcError, ServerJsonrpcRequest,
189};
190
191#[async_trait]
192impl ClientHandlerCore for MinimalClientHandler {
193    async fn handle_request(
194        &self,
195        _request: ServerJsonrpcRequest,
196        _runtime: &dyn SdkMcpClient,
197    ) -> Result<ResultFromClient, RpcError> {
198        Err(RpcError::method_not_found())
199    }
200
201    async fn handle_notification(
202        &self,
203        _notification: NotificationFromServer,
204        _runtime: &dyn SdkMcpClient,
205    ) -> Result<(), RpcError> {
206        Ok(())
207    }
208
209    async fn handle_error(
210        &self,
211        _error: &RpcError,
212        _runtime: &dyn SdkMcpClient,
213    ) -> Result<(), RpcError> {
214        Ok(())
215    }
216}
217
218/// A tool that wraps an MCP server tool
219///
220/// This implements our `Tool` trait so it can be used alongside native tools
221/// in the `ToolRegistry`.
222pub struct McpTool {
223    name: String,
224    description: String,
225    input_schema: JsonValue,
226    // Reference to the MCP client for making calls
227    client: Arc<McpClient>,
228}
229
230impl McpTool {
231    /// Create a new MCP tool wrapper
232    pub fn new(
233        name: String,
234        description: String,
235        input_schema: JsonValue,
236        client: Arc<McpClient>,
237    ) -> Self {
238        Self {
239            name,
240            description,
241            input_schema,
242            client,
243        }
244    }
245
246    /// Create from McpToolInfo (convenience method)
247    pub fn from_info(info: McpToolInfo, client: Arc<McpClient>) -> Self {
248        Self::new(info.name, info.description, info.input_schema, client)
249    }
250}
251
252#[async_trait]
253impl Tool for McpTool {
254    fn name(&self) -> &str {
255        &self.name
256    }
257
258    fn description(&self) -> &str {
259        &self.description
260    }
261
262    fn input_schema(&self) -> JsonValue {
263        self.input_schema.clone()
264    }
265
266    async fn execute(&self, params: HashMap<String, JsonValue>) -> Result<ToolResult, ToolError> {
267        let start = std::time::Instant::now();
268
269        // Call through to MCP server
270        match self.client.call_tool(&self.name, params).await {
271            Ok(output) => Ok(ToolResult::success(
272                output,
273                start.elapsed().as_secs_f64() * 1000.0,
274            )),
275            Err(e) => Err(ToolError::ExecutionFailed(format!("MCP error: {}", e))),
276        }
277    }
278}