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}