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}