mcp_runner/
client.rs

1//! Client module for interacting with MCP servers.
2//!
3//! This module provides the `McpClient` class which serves as the main interface
4//! for communicating with Model Context Protocol servers. It allows applications to:
5//! - List available tools provided by an MCP server
6//! - Call tools with arguments
7//! - List available resources
8//! - Retrieve resource content
9//!
10//! The client is transport-agnostic and can work with any implementation of the
11//! `Transport` trait, though the library primarily focuses on StdioTransport.
12use crate::error::{Error, Result};
13use crate::transport::Transport;
14use serde::{Deserialize, Serialize};
15use serde_json::Value;
16use std::sync::Arc;
17use tracing; // Import tracing
18
19/// Represents an MCP tool with its metadata.
20///
21/// Tools are the primary way to interact with MCP servers. Each tool has
22/// a name, description, and optional schema information for its inputs and outputs.
23///
24/// # Examples
25///
26/// ```
27/// # use serde_json::json;
28/// use mcp_runner::client::Tool;
29///
30/// let tool = Tool {
31///     name: "fetch".to_string(),
32///     description: "Fetch data from a URL".to_string(),
33///     input_schema: Some(json!({
34///         "type": "object",
35///         "properties": {
36///             "url": {
37///                 "type": "string",
38///                 "description": "The URL to fetch data from"
39///             }
40///         },
41///         "required": ["url"]
42///     })),
43///     output_schema: Some(json!({
44///         "type": "object",
45///         "properties": {
46///             "content": {
47///                 "type": "string"
48///             }
49///         }
50///     })),
51/// };
52/// ```
53#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct Tool {
55    /// Tool name used when calling the tool.
56    pub name: String,
57    /// Human-readable description of the tool's purpose and functionality.
58    pub description: String,
59    /// JSON Schema defining the expected format of tool inputs.
60    #[serde(rename = "inputSchema")]
61    pub input_schema: Option<Value>,
62    /// JSON Schema defining the expected format of tool outputs.
63    #[serde(rename = "outputSchema")]
64    pub output_schema: Option<Value>,
65}
66
67/// Represents an MCP resource with its metadata.
68///
69/// Resources are data exposed by the MCP server that can be retrieved by clients.
70/// Each resource has a URI that uniquely identifies it, along with metadata.
71///
72/// # Examples
73///
74/// ```
75/// use mcp_runner::client::Resource;
76///
77/// let resource = Resource {
78///     uri: "res:fetch/settings".to_string(),
79///     name: "Fetch Settings".to_string(),
80///     description: Some("Configuration settings for fetch operations".to_string()),
81///     resource_type: Some("application/json".to_string()),
82/// };
83/// ```
84#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct Resource {
86    /// Unique URI identifying the resource.
87    pub uri: String,
88    /// Human-readable name of the resource.
89    pub name: String,
90    /// Optional description of the resource's content or purpose.
91    pub description: Option<String>,
92    /// Optional MIME type or format of the resource.
93    #[serde(rename = "type")]
94    pub resource_type: Option<String>,
95}
96
97/// A client for interacting with an MCP server.
98///
99/// The `McpClient` provides a high-level interface for communicating with
100/// Model Context Protocol servers. It abstracts away the details of the
101/// transport layer and JSON-RPC protocol, offering a simple API for listing
102/// and calling tools, and accessing resources.
103/// All public methods are instrumented with `tracing` spans.
104///
105/// # Examples
106///
107/// Basic usage:
108///
109/// ```no_run
110/// # // This example is marked no_run because it doesn't actually run the code,
111/// # // it just verifies that it compiles correctly.
112/// use mcp_runner::{McpClient, transport::StdioTransport, error::Result};
113/// use serde_json::{json, Value};
114/// use async_process::{ChildStdin, ChildStdout};
115///
116/// # // Mock implementation for the example
117/// # fn get_mock_stdin_stdout() -> (ChildStdin, ChildStdout) {
118/// #     unimplemented!("This is just for doctest and won't be called")
119/// # }
120///
121/// # async fn example() -> Result<()> {
122/// # // In a real app, you would get these from a server process
123/// # // Here we just declare them but don't initialize to make the doctest pass
124/// # let (stdin, stdout) = get_mock_stdin_stdout();
125///
126/// // Create a transport
127/// let transport = StdioTransport::new("fetch-server".to_string(), stdin, stdout);
128///
129/// // Create a client
130/// let client = McpClient::new("fetch-server".to_string(), transport);
131///
132/// // Initialize
133/// client.initialize().await?;
134///
135/// // List tools
136/// let tools = client.list_tools().await?;
137/// for tool in tools {
138///     println!("Tool: {} - {}", tool.name, tool.description);
139/// }
140///
141/// // Call the fetch tool
142/// #[derive(serde::Serialize, Debug)]
143/// struct FetchInput {
144///     url: String,
145/// }
146///
147/// let input = FetchInput {
148///     url: "https://modelcontextprotocol.io".to_string(),
149/// };
150///
151/// let output: Value = client.call_tool("fetch", &input).await?;
152/// println!("Fetch result: {}", output);
153/// # Ok(())
154/// # }
155/// ```
156#[derive(Clone)]
157pub struct McpClient {
158    /// Server name for identification.
159    name: String,
160    /// Transport implementation for communication.
161    transport: Arc<dyn Transport>,
162}
163
164impl std::fmt::Debug for McpClient {
165    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
166        f.debug_struct("McpClient")
167            .field("name", &self.name)
168            .field("transport", &"<dyn Transport>")
169            .finish()
170    }
171}
172
173impl McpClient {
174    /// Creates a new MCP client with the specified name and transport.
175    ///
176    /// This method is instrumented with `tracing`.
177    ///
178    /// # Arguments
179    ///
180    /// * `name` - A name for this client, typically the server name
181    /// * `transport` - The transport implementation to use for communication
182    ///
183    /// # Returns
184    ///
185    /// A new `McpClient` instance
186    #[tracing::instrument(skip(transport), fields(client_name = %name))]
187    pub fn new(name: String, transport: impl Transport + 'static) -> Self {
188        tracing::info!("Creating new McpClient");
189        Self {
190            name,
191            transport: Arc::new(transport),
192        }
193    }
194
195    /// Creates a new McpClient by connecting to a server using the provided configuration
196    ///
197    /// This is a utility method that creates a new client for an MCP server
198    /// by using the server's configuration to establish a new connection.
199    ///
200    /// # Arguments
201    ///
202    /// * `server_name` - The name of the server to connect to
203    /// * `config` - The configuration containing server connection details
204    ///
205    /// # Returns
206    ///
207    /// A `Result<McpClient>` with a new client or an error if connection fails
208    #[tracing::instrument(skip(config), fields(server = %server_name))]
209    pub fn connect(server_name: &str, config: &crate::Config) -> Result<Self> {
210        tracing::info!("Connecting to server {}", server_name);
211
212        // Check if the server exists in the config
213        let server_config = config.mcp_servers.get(server_name).ok_or_else(|| {
214            tracing::error!("Server '{}' not found in configuration", server_name);
215            Error::ServerNotFound(server_name.to_string())
216        })?;
217
218        // Create a transport to connect to the server
219        let transport = crate::transport::create_transport_for_config(server_name, server_config)?;
220
221        // Return a new client with the transport
222        Ok(Self::new(server_name.to_string(), transport))
223    }
224
225    /// Gets the name of the client (usually the same as the server name).
226    ///
227    /// # Returns
228    ///
229    /// A string slice containing the client name
230    pub fn name(&self) -> &str {
231        &self.name
232    }
233
234    /// Initializes the connection to the MCP server.
235    ///
236    /// This method should be called before any other methods to ensure
237    /// the server is ready to accept requests.
238    /// This method is instrumented with `tracing`.
239    ///
240    /// # Returns
241    ///
242    /// A `Result<()>` indicating success or failure
243    #[tracing::instrument(skip(self), fields(client_name = %self.name))]
244    pub async fn initialize(&self) -> Result<()> {
245        tracing::info!("Initializing client connection");
246        self.transport.initialize().await.map_err(|e| {
247            tracing::error!(error = %e, "Failed to initialize transport");
248            e
249        })
250    }
251
252    /// Lists all available tools provided by the MCP server.
253    ///
254    /// This method is instrumented with `tracing`.
255    ///
256    /// # Returns
257    ///
258    /// A `Result<Vec<Tool>>` containing descriptions of available tools if successful
259    #[tracing::instrument(skip(self), fields(client_name = %self.name))]
260    pub async fn list_tools(&self) -> Result<Vec<Tool>> {
261        tracing::debug!("Listing tools via transport");
262        let tools_json = self.transport.list_tools().await?;
263        tracing::trace!(raw_tools = ?tools_json, "Received raw tools list");
264
265        let mut tools = Vec::new();
266        for tool_value in tools_json {
267            match serde_json::from_value::<Tool>(tool_value.clone()) {
268                Ok(tool) => {
269                    tracing::trace!(tool_name = %tool.name, "Successfully deserialized tool");
270                    tools.push(tool);
271                }
272                Err(e) => {
273                    tracing::warn!(error = %e, value = ?tool_value, "Failed to deserialize tool from value");
274                }
275            }
276        }
277        tracing::debug!(num_tools = tools.len(), "Finished listing tools");
278        Ok(tools)
279    }
280
281    /// Calls a tool on the MCP server with the given arguments.
282    ///
283    /// This method provides a strongly-typed interface for tool calls,
284    /// where the input and output types are specified as generic parameters.
285    /// This method is instrumented with `tracing`.
286    ///
287    /// # Type Parameters
288    ///
289    /// * `T` - The input type, which must be serializable to JSON and implement `Debug`
290    /// * `R` - The output type, which must be deserializable from JSON
291    ///
292    /// # Arguments
293    ///
294    /// * `name` - The name of the tool to call
295    /// * `args` - The arguments to pass to the tool
296    ///
297    /// # Returns
298    ///
299    /// A `Result<R>` containing the tool's response if successful
300    ///
301    /// # Errors
302    ///
303    /// Returns an error if:
304    /// * The tool call fails
305    /// * The arguments cannot be serialized
306    /// * The result cannot be deserialized to type R
307    #[tracing::instrument(skip(self, args), fields(client_name = %self.name, tool_name = %name))]
308    pub async fn call_tool<T, R>(&self, name: &str, args: &T) -> Result<R>
309    where
310        T: Serialize + std::fmt::Debug,
311        R: for<'de> Deserialize<'de>,
312    {
313        tracing::debug!(args = ?args, "Calling tool");
314        let args_value = serde_json::to_value(args).map_err(|e| {
315            tracing::error!(error = %e, "Failed to serialize tool arguments");
316            Error::Serialization(format!("Failed to serialize tool arguments: {}", e))
317        })?;
318        tracing::trace!(args_json = ?args_value, "Serialized arguments");
319
320        let result_value = self.transport.call_tool(name, args_value).await?;
321        tracing::trace!(result_json = ?result_value, "Received raw tool result");
322
323        serde_json::from_value(result_value.clone()).map_err(|e| {
324            tracing::error!(error = %e, value = ?result_value, "Failed to deserialize tool result");
325            Error::Serialization(format!("Failed to deserialize tool result: {}", e))
326        })
327    }
328
329    /// Lists all available resources provided by the MCP server.
330    ///
331    /// This method is instrumented with `tracing`.
332    ///
333    /// # Returns
334    ///
335    /// A `Result<Vec<Resource>>` containing descriptions of available resources if successful
336    #[tracing::instrument(skip(self), fields(client_name = %self.name))]
337    pub async fn list_resources(&self) -> Result<Vec<Resource>> {
338        tracing::debug!("Listing resources via transport");
339        let resources_json = self.transport.list_resources().await?;
340        tracing::trace!(raw_resources = ?resources_json, "Received raw resources list");
341
342        let mut resources = Vec::new();
343        for resource_value in resources_json {
344            match serde_json::from_value::<Resource>(resource_value.clone()) {
345                Ok(resource) => {
346                    tracing::trace!(resource_uri = %resource.uri, "Successfully deserialized resource");
347                    resources.push(resource);
348                }
349                Err(e) => {
350                    tracing::warn!(error = %e, value = ?resource_value, "Failed to deserialize resource from value");
351                }
352            }
353        }
354        tracing::debug!(
355            num_resources = resources.len(),
356            "Finished listing resources"
357        );
358        Ok(resources)
359    }
360
361    /// Gets a specific resource from the MCP server.
362    ///
363    /// This method provides a strongly-typed interface for resource retrieval,
364    /// where the expected resource type is specified as a generic parameter.
365    /// This method is instrumented with `tracing`.
366    ///
367    /// # Type Parameters
368    ///
369    /// * `R` - The resource type, which must be deserializable from JSON
370    ///
371    /// # Arguments
372    ///
373    /// * `uri` - The URI of the resource to retrieve
374    ///
375    /// # Returns
376    ///
377    /// A `Result<R>` containing the resource data if successful
378    ///
379    /// # Errors
380    ///
381    /// Returns an error if:
382    /// * The resource retrieval fails
383    /// * The result cannot be deserialized to type R
384    #[tracing::instrument(skip(self), fields(client_name = %self.name, resource_uri = %uri))]
385    pub async fn get_resource<R>(&self, uri: &str) -> Result<R>
386    where
387        R: for<'de> Deserialize<'de>,
388    {
389        tracing::debug!("Getting resource via transport");
390        let resource_value = self.transport.get_resource(uri).await?;
391        tracing::trace!(raw_resource = ?resource_value, "Received raw resource value");
392
393        serde_json::from_value(resource_value.clone()).map_err(|e| {
394            tracing::error!(error = %e, value = ?resource_value, "Failed to deserialize resource");
395            Error::Serialization(format!("Failed to deserialize resource: {}", e))
396        })
397    }
398
399    /// Closes the client connection.
400    ///
401    /// This is a placeholder method since the transport is behind an Arc and can't actually
402    /// be closed by the client directly. Users should drop all references to the client
403    /// to properly clean up resources.
404    /// This method is instrumented with `tracing`.
405    ///
406    /// # Returns
407    ///
408    /// A `Result<()>` that is always Ok
409    #[tracing::instrument(skip(self), fields(client_name = %self.name))]
410    pub async fn close(&self) -> Result<()> {
411        tracing::info!(
412            "Close called on McpClient (Note: Transport closure depends on Arc references)"
413        );
414        Ok(())
415    }
416}