mcp_runner/
lib.rs

1/*!
2 # MCP Runner
3
4 A Rust library for running and interacting with Model Context Protocol (MCP) servers.
5
6 ## Overview
7
8 MCP Runner provides functionality to:
9 - Start and manage MCP server processes
10 - Communicate with MCP servers using JSON-RPC
11 - Configure MCP servers through config files
12 - List and call tools provided by MCP servers
13 - Access resources exposed by MCP servers
14
15 ## Basic Usage
16
17 ```no_run
18 use mcp_runner::{McpRunner, Result};
19 use serde_json::{json, Value};
20
21 #[tokio::main]
22 async fn main() -> Result<()> {
23     // Create a runner from config file
24     let mut runner = McpRunner::from_config_file("config.json")?;
25
26     // Start all configured servers
27     let server_ids = runner.start_all_servers().await?;
28
29     // Or start a specific server
30     let server_id = runner.start_server("fetch").await?;
31
32     // Get a client to interact with the server
33     let client = runner.get_client(server_id)?;
34
35     // Initialize the client
36     client.initialize().await?;
37
38     // List available tools
39     let tools = client.list_tools().await?;
40     println!("Available tools: {:?}", tools);
41
42     // Call a tool
43     let args = json!({
44         "url": "https://modelcontextprotocol.io"
45     });
46     let result: Value = client.call_tool("fetch", &args).await?;
47
48     println!("Result: {:?}", result);
49
50     Ok(())
51 }
52 ```
53
54 ## Features
55
56 - **Server Management**: Start, stop, and monitor MCP servers
57 - **JSON-RPC Communication**: Communicate with MCP servers using JSON-RPC
58 - **Configuration**: Configure servers through JSON config files
59 - **Error Handling**: Comprehensive error handling
60 - **Async Support**: Full async/await support
61
62 ## License
63
64 This project is licensed under the terms in the LICENSE file.
65*/
66
67pub mod client;
68pub mod config;
69pub mod error;
70pub mod server;
71pub mod transport;
72
73// Re-export key types for better API ergonomics
74pub use client::McpClient;
75pub use config::Config;
76pub use error::{Error, Result};
77pub use server::{ServerId, ServerProcess, ServerStatus};
78
79use std::collections::HashMap;
80use std::path::Path;
81use tracing;
82use transport::StdioTransport; // Import tracing
83
84/// Configure and run MCP servers
85///
86/// This struct is the main entry point for managing MCP server lifecycles
87/// and obtaining clients to interact with them.
88/// All public methods are instrumented with `tracing` spans.
89pub struct McpRunner {
90    /// Configuration
91    config: Config,
92    /// Running server processes
93    servers: HashMap<ServerId, ServerProcess>,
94    /// Map of server names to server IDs
95    server_names: HashMap<String, ServerId>,
96}
97
98impl McpRunner {
99    /// Create a new MCP runner from a configuration file path
100    ///
101    /// This method is instrumented with `tracing`.
102    #[tracing::instrument(skip(path), fields(config_path = ?path.as_ref()))]
103    pub fn from_config_file(path: impl AsRef<Path>) -> Result<Self> {
104        tracing::info!("Loading configuration from file");
105        let config = Config::from_file(path)?;
106        Ok(Self::new(config))
107    }
108
109    /// Create a new MCP runner from a configuration string
110    ///
111    /// This method is instrumented with `tracing`.
112    #[tracing::instrument(skip(config))]
113    pub fn from_config_str(config: &str) -> Result<Self> {
114        tracing::info!("Loading configuration from string");
115        let config = Config::parse_from_str(config)?;
116        Ok(Self::new(config))
117    }
118
119    /// Create a new MCP runner from a configuration
120    ///
121    /// This method is instrumented with `tracing`.
122    #[tracing::instrument(skip(config), fields(num_servers = config.mcp_servers.len()))]
123    pub fn new(config: Config) -> Self {
124        tracing::info!("Creating new McpRunner");
125        Self {
126            config,
127            servers: HashMap::new(),
128            server_names: HashMap::new(),
129        }
130    }
131
132    /// Start a specific MCP server
133    ///
134    /// This method is instrumented with `tracing`.
135    #[tracing::instrument(skip(self), fields(server_name = %name))]
136    pub async fn start_server(&mut self, name: &str) -> Result<ServerId> {
137        // Check if server is already running
138        if let Some(id) = self.server_names.get(name) {
139            tracing::debug!(server_id = %id, "Server already running");
140            return Ok(*id);
141        }
142
143        tracing::info!("Attempting to start server");
144        // Get server configuration
145        let config = self
146            .config
147            .mcp_servers
148            .get(name)
149            .ok_or_else(|| {
150                tracing::error!("Configuration not found for server");
151                Error::ServerNotFound(name.to_string())
152            })?
153            .clone();
154
155        // Create and start server process
156        let mut server = ServerProcess::new(name.to_string(), config);
157        let id = server.id();
158        tracing::debug!(server_id = %id, "Created ServerProcess instance");
159
160        server.start().await.map_err(|e| {
161            tracing::error!(error = %e, "Failed to start server process");
162            e
163        })?;
164
165        // Store server
166        tracing::debug!(server_id = %id, "Storing running server process");
167        self.servers.insert(id, server);
168        self.server_names.insert(name.to_string(), id);
169
170        tracing::info!(server_id = %id, "Server started successfully");
171        Ok(id)
172    }
173
174    /// Start all configured servers
175    ///
176    /// This method is instrumented with `tracing`.
177    #[tracing::instrument(skip(self))]
178    pub async fn start_all_servers(&mut self) -> Result<Vec<ServerId>> {
179        tracing::info!("Starting all configured servers");
180        // Collect server names first to avoid borrowing issues
181        let server_names: Vec<String> = self
182            .config
183            .mcp_servers
184            .keys()
185            .map(|k| k.to_string())
186            .collect();
187        tracing::debug!(servers_to_start = ?server_names);
188
189        let mut ids = Vec::new();
190        let mut errors = Vec::new();
191
192        for name in server_names {
193            match self.start_server(&name).await {
194                Ok(id) => ids.push(id),
195                Err(e) => {
196                    tracing::error!(server_name = %name, error = %e, "Failed to start server");
197                    errors.push((name, e));
198                }
199            }
200        }
201
202        if !errors.is_empty() {
203            tracing::warn!(num_failed = errors.len(), "Some servers failed to start");
204            return Err(errors.remove(0).1);
205        }
206
207        tracing::info!(num_started = ids.len(), "Finished starting all servers");
208        Ok(ids)
209    }
210
211    /// Stop a running server
212    ///
213    /// This method is instrumented with `tracing`.
214    #[tracing::instrument(skip(self), fields(server_id = %id))]
215    pub async fn stop_server(&mut self, id: ServerId) -> Result<()> {
216        tracing::info!("Attempting to stop server");
217        if let Some(mut server) = self.servers.remove(&id) {
218            let name = server.name().to_string();
219            tracing::debug!(server_name = %name, "Found server process to stop");
220            self.server_names.remove(&name);
221
222            server.stop().await.map_err(|e| {
223                tracing::error!(error = %e, "Failed to stop server process");
224                e
225            })?;
226
227            tracing::info!("Server stopped successfully");
228            Ok(())
229        } else {
230            tracing::warn!("Attempted to stop a server that was not found or not running");
231            Err(Error::ServerNotFound(format!("{:?}", id)))
232        }
233    }
234
235    /// Get server status
236    ///
237    /// This method is instrumented with `tracing`.
238    #[tracing::instrument(skip(self), fields(server_id = %id))]
239    pub fn server_status(&self, id: ServerId) -> Result<ServerStatus> {
240        tracing::debug!("Getting server status");
241        self.servers
242            .get(&id)
243            .map(|server| {
244                let status = server.status();
245                tracing::trace!(status = ?status);
246                status
247            })
248            .ok_or_else(|| {
249                tracing::warn!("Status requested for unknown server");
250                Error::ServerNotFound(format!("{:?}", id))
251            })
252    }
253
254    /// Get server ID by name
255    ///
256    /// This method is instrumented with `tracing`.
257    #[tracing::instrument(skip(self), fields(server_name = %name))]
258    pub fn get_server_id(&self, name: &str) -> Result<ServerId> {
259        tracing::debug!("Getting server ID by name");
260        self.server_names.get(name).copied().ok_or_else(|| {
261            tracing::warn!("Server ID requested for unknown server name");
262            Error::ServerNotFound(name.to_string())
263        })
264    }
265
266    /// Get a client for a server
267    ///
268    /// This method is instrumented with `tracing`.
269    #[tracing::instrument(skip(self), fields(server_id = %id))]
270    pub fn get_client(&mut self, id: ServerId) -> Result<McpClient> {
271        tracing::info!("Getting client for server");
272        let server = self.servers.get_mut(&id).ok_or_else(|| {
273            tracing::error!("Client requested for unknown or stopped server");
274            Error::ServerNotFound(format!("{:?}", id))
275        })?;
276        let server_name = server.name().to_string();
277        tracing::debug!(server_name = %server_name, "Found server process");
278
279        tracing::debug!("Taking stdin/stdout from server process");
280        let stdin = server.take_stdin().map_err(|e| {
281            tracing::error!(error = %e, "Failed to take stdin from server");
282            e
283        })?;
284        let stdout = server.take_stdout().map_err(|e| {
285            tracing::error!(error = %e, "Failed to take stdout from server");
286            e
287        })?;
288
289        tracing::debug!("Creating StdioTransport and McpClient");
290        let transport = StdioTransport::new(server_name.clone(), stdin, stdout);
291        let client = McpClient::new(server_name, transport);
292
293        tracing::info!("Client created successfully");
294        Ok(client)
295    }
296}