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/// ```
156pub struct McpClient {
157    /// Server name for identification.
158    name: String,
159    /// Transport implementation for communication.
160    transport: Arc<dyn Transport>,
161}
162
163impl McpClient {
164    /// Creates a new MCP client with the specified name and transport.
165    ///
166    /// This method is instrumented with `tracing`.
167    ///
168    /// # Arguments
169    ///
170    /// * `name` - A name for this client, typically the server name
171    /// * `transport` - The transport implementation to use for communication
172    ///
173    /// # Returns
174    ///
175    /// A new `McpClient` instance
176    #[tracing::instrument(skip(transport), fields(client_name = %name))]
177    pub fn new(name: String, transport: impl Transport + 'static) -> Self {
178        tracing::info!("Creating new McpClient");
179        Self {
180            name,
181            transport: Arc::new(transport),
182        }
183    }
184
185    /// Gets the name of the client (usually the same as the server name).
186    ///
187    /// # Returns
188    ///
189    /// A string slice containing the client name
190    pub fn name(&self) -> &str {
191        &self.name
192    }
193
194    /// Initializes the connection to the MCP server.
195    ///
196    /// This method should be called before any other methods to ensure
197    /// the server is ready to accept requests.
198    /// This method is instrumented with `tracing`.
199    ///
200    /// # Returns
201    ///
202    /// A `Result<()>` indicating success or failure
203    #[tracing::instrument(skip(self), fields(client_name = %self.name))]
204    pub async fn initialize(&self) -> Result<()> {
205        tracing::info!("Initializing client connection");
206        self.transport.initialize().await.map_err(|e| {
207            tracing::error!(error = %e, "Failed to initialize transport");
208            e
209        })
210    }
211
212    /// Lists all available tools provided by the MCP server.
213    ///
214    /// This method is instrumented with `tracing`.
215    ///
216    /// # Returns
217    ///
218    /// A `Result<Vec<Tool>>` containing descriptions of available tools if successful
219    #[tracing::instrument(skip(self), fields(client_name = %self.name))]
220    pub async fn list_tools(&self) -> Result<Vec<Tool>> {
221        tracing::debug!("Listing tools via transport");
222        let tools_json = self.transport.list_tools().await?;
223        tracing::trace!(raw_tools = ?tools_json, "Received raw tools list");
224
225        let mut tools = Vec::new();
226        for tool_value in tools_json {
227            match serde_json::from_value::<Tool>(tool_value.clone()) {
228                Ok(tool) => {
229                    tracing::trace!(tool_name = %tool.name, "Successfully deserialized tool");
230                    tools.push(tool);
231                }
232                Err(e) => {
233                    tracing::warn!(error = %e, value = ?tool_value, "Failed to deserialize tool from value");
234                }
235            }
236        }
237        tracing::debug!(num_tools = tools.len(), "Finished listing tools");
238        Ok(tools)
239    }
240
241    /// Calls a tool on the MCP server with the given arguments.
242    ///
243    /// This method provides a strongly-typed interface for tool calls,
244    /// where the input and output types are specified as generic parameters.
245    /// This method is instrumented with `tracing`.
246    ///
247    /// # Type Parameters
248    ///
249    /// * `T` - The input type, which must be serializable to JSON and implement `Debug`
250    /// * `R` - The output type, which must be deserializable from JSON
251    ///
252    /// # Arguments
253    ///
254    /// * `name` - The name of the tool to call
255    /// * `args` - The arguments to pass to the tool
256    ///
257    /// # Returns
258    ///
259    /// A `Result<R>` containing the tool's response if successful
260    ///
261    /// # Errors
262    ///
263    /// Returns an error if:
264    /// * The tool call fails
265    /// * The arguments cannot be serialized
266    /// * The result cannot be deserialized to type R
267    #[tracing::instrument(skip(self, args), fields(client_name = %self.name, tool_name = %name))]
268    pub async fn call_tool<T, R>(&self, name: &str, args: &T) -> Result<R>
269    where
270        T: Serialize + std::fmt::Debug,
271        R: for<'de> Deserialize<'de>,
272    {
273        tracing::debug!(args = ?args, "Calling tool");
274        let args_value = serde_json::to_value(args).map_err(|e| {
275            tracing::error!(error = %e, "Failed to serialize tool arguments");
276            Error::Serialization(format!("Failed to serialize tool arguments: {}", e))
277        })?;
278        tracing::trace!(args_json = ?args_value, "Serialized arguments");
279
280        let result_value = self.transport.call_tool(name, args_value).await?;
281        tracing::trace!(result_json = ?result_value, "Received raw tool result");
282
283        serde_json::from_value(result_value.clone()).map_err(|e| {
284            tracing::error!(error = %e, value = ?result_value, "Failed to deserialize tool result");
285            Error::Serialization(format!("Failed to deserialize tool result: {}", e))
286        })
287    }
288
289    /// Lists all available resources provided by the MCP server.
290    ///
291    /// This method is instrumented with `tracing`.
292    ///
293    /// # Returns
294    ///
295    /// A `Result<Vec<Resource>>` containing descriptions of available resources if successful
296    #[tracing::instrument(skip(self), fields(client_name = %self.name))]
297    pub async fn list_resources(&self) -> Result<Vec<Resource>> {
298        tracing::debug!("Listing resources via transport");
299        let resources_json = self.transport.list_resources().await?;
300        tracing::trace!(raw_resources = ?resources_json, "Received raw resources list");
301
302        let mut resources = Vec::new();
303        for resource_value in resources_json {
304            match serde_json::from_value::<Resource>(resource_value.clone()) {
305                Ok(resource) => {
306                    tracing::trace!(resource_uri = %resource.uri, "Successfully deserialized resource");
307                    resources.push(resource);
308                }
309                Err(e) => {
310                    tracing::warn!(error = %e, value = ?resource_value, "Failed to deserialize resource from value");
311                }
312            }
313        }
314        tracing::debug!(
315            num_resources = resources.len(),
316            "Finished listing resources"
317        );
318        Ok(resources)
319    }
320
321    /// Gets a specific resource from the MCP server.
322    ///
323    /// This method provides a strongly-typed interface for resource retrieval,
324    /// where the expected resource type is specified as a generic parameter.
325    /// This method is instrumented with `tracing`.
326    ///
327    /// # Type Parameters
328    ///
329    /// * `R` - The resource type, which must be deserializable from JSON
330    ///
331    /// # Arguments
332    ///
333    /// * `uri` - The URI of the resource to retrieve
334    ///
335    /// # Returns
336    ///
337    /// A `Result<R>` containing the resource data if successful
338    ///
339    /// # Errors
340    ///
341    /// Returns an error if:
342    /// * The resource retrieval fails
343    /// * The result cannot be deserialized to type R
344    #[tracing::instrument(skip(self), fields(client_name = %self.name, resource_uri = %uri))]
345    pub async fn get_resource<R>(&self, uri: &str) -> Result<R>
346    where
347        R: for<'de> Deserialize<'de>,
348    {
349        tracing::debug!("Getting resource via transport");
350        let resource_value = self.transport.get_resource(uri).await?;
351        tracing::trace!(raw_resource = ?resource_value, "Received raw resource value");
352
353        serde_json::from_value(resource_value.clone()).map_err(|e| {
354            tracing::error!(error = %e, value = ?resource_value, "Failed to deserialize resource");
355            Error::Serialization(format!("Failed to deserialize resource: {}", e))
356        })
357    }
358
359    /// Closes the client connection.
360    ///
361    /// This is a placeholder method since the transport is behind an Arc and can't actually
362    /// be closed by the client directly. Users should drop all references to the client
363    /// to properly clean up resources.
364    /// This method is instrumented with `tracing`.
365    ///
366    /// # Returns
367    ///
368    /// A `Result<()>` that is always Ok
369    #[tracing::instrument(skip(self), fields(client_name = %self.name))]
370    pub async fn close(&self) -> Result<()> {
371        tracing::info!(
372            "Close called on McpClient (Note: Transport closure depends on Arc references)"
373        );
374        Ok(())
375    }
376}