Skip to main content

oxios_mcp/
lib.rs

1//! oxios-mcp — Model Context Protocol client library.
2//!
3//! Implements MCP over JSON-RPC 2.0 via stdio transport.
4//! Independent of the Oxios kernel — usable as a standalone MCP client.
5//!
6//! # Protocol Overview
7//!
8//! MCP defines several message types:
9//! - `initialize` - Establish connection with capabilities negotiation
10//! - `tools/list` - List available tools from a server
11//! - `tools/call` - Execute a tool with arguments
12//! - `resources/list` - List available resources
13//! - `resources/read` - Read a resource by URI
14//!
15//! # Architecture
16//!
17//! ```text
18//! Consumer → McpBridge → McpClient (per server)
19//!                         ↓
20//!              tokio::process::Command (stdio)
21//!                         ↓
22//!              JSON-RPC 2.0 (stdin/stdout)
23//!                         ↓
24//!              MCP Server Process
25//! ```
26
27pub mod client;
28pub mod protocol;
29
30pub use client::McpClient;
31pub use protocol::*;
32
33use std::collections::HashMap;
34use std::sync::Arc;
35
36use anyhow::{Result, anyhow};
37use tokio::sync::RwLock;
38
39// ---------------------------------------------------------------------------
40// McpBridge — manages multiple MCP server clients
41// ---------------------------------------------------------------------------
42
43/// MCP bridge — connects multiple MCP servers to the tool system.
44///
45/// `McpBridge` owns the collection of registered MCP server configurations
46/// and manages `McpClient` instances for active servers.
47///
48/// # Example
49///
50/// ```ignore
51/// let mut bridge = McpBridge::new();
52/// bridge.register_server(McpServer::new("files", "npx")
53///     .with_args(vec!["-y", "@anthropic/mcp-server-filesystem"]));
54///
55/// // Initialize all servers
56/// bridge.initialize_all().await?;
57///
58/// // List all tools across all servers
59/// let tools = bridge.list_tools().await?;
60/// ```
61pub struct McpBridge {
62    /// Registered MCP server configurations
63    servers: parking_lot::RwLock<Vec<McpServer>>,
64    /// Active MCP clients (keyed by server name)
65    clients: RwLock<HashMap<String, Arc<McpClient>>>,
66    /// Tool cache: server_name → cached tools
67    tool_cache: RwLock<HashMap<String, Vec<McpTool>>>,
68}
69
70impl McpBridge {
71    /// Create a new empty MCP bridge
72    pub fn new() -> Self {
73        Self {
74            servers: parking_lot::RwLock::new(Vec::new()),
75            clients: RwLock::new(HashMap::new()),
76            tool_cache: RwLock::new(HashMap::new()),
77        }
78    }
79
80    /// Register an MCP server configuration (does not start the process).
81    ///
82    /// If a server with the same name is already registered, it is replaced
83    /// in-place (F7) rather than appended — `initialize_all()` would
84    /// otherwise spawn both and orphan the first child when the second
85    /// overwrites the entry in the clients map.
86    pub fn register_server(&self, server: McpServer) {
87        let mut servers = self.servers.write();
88        let name = server.name.clone();
89        if let Some(existing) = servers.iter_mut().find(|s| s.name == name) {
90            tracing::warn!(
91                server = %name,
92                "Overwriting duplicate MCP server registration"
93            );
94            *existing = server;
95        } else {
96            servers.push(server);
97        }
98    }
99
100    /// Get all registered server configurations (names only).
101    pub fn servers(&self) -> Vec<String> {
102        self.servers.read().iter().map(|s| s.name.clone()).collect()
103    }
104
105    /// Get a server configuration by name.
106    pub fn get_server(&self, name: &str) -> Option<McpServer> {
107        self.servers.read().iter().find(|s| s.name == name).cloned()
108    }
109
110    /// Initialize all enabled MCP servers.
111    ///
112    /// Each server is spawned as a child process and receives the initialize request.
113    /// Servers that fail to initialize are logged but do not cause a total failure.
114    pub async fn initialize_all(&self) -> Result<()> {
115        let mut errors = Vec::new();
116
117        let server_list: Vec<McpServer> = self.servers.read().iter().cloned().collect();
118        for server in server_list {
119            if !server.enabled {
120                tracing::debug!(server = %server.name, "Skipping disabled MCP server");
121                continue;
122            }
123
124            let client = Arc::new(McpClient::new(server.clone()));
125            match client.initialize().await {
126                Ok(()) => {
127                    self.clients
128                        .write()
129                        .await
130                        .insert(server.name.clone(), client);
131                    tracing::info!(server = %server.name, "MCP server started");
132                }
133                Err(e) => {
134                    tracing::error!(server = %server.name, error = %e, "Failed to initialize MCP server");
135                    errors.push(format!("{}: {}", server.name, e));
136                }
137            }
138        }
139
140        if errors.is_empty() {
141            Ok(())
142        } else {
143            Err(anyhow!("MCP initialization failed: {}", errors.join("; ")))
144        }
145    }
146
147    /// Initialize a specific server by name.
148    pub async fn initialize_server(&self, name: &str) -> Result<()> {
149        let server = self
150            .servers
151            .read()
152            .iter()
153            .find(|s| s.name == name)
154            .cloned()
155            .ok_or_else(|| anyhow!("MCP server '{name}' not found"))?;
156
157        let client = Arc::new(McpClient::new(server));
158        client.initialize().await?;
159
160        self.clients.write().await.insert(name.to_string(), client);
161        Ok(())
162    }
163
164    /// Get a client by server name.
165    pub async fn client(&self, name: &str) -> Option<Arc<McpClient>> {
166        self.clients.read().await.get(name).cloned()
167    }
168
169    /// List all available tools from all initialized MCP servers.
170    ///
171    /// Tools are collected from each server's cache (refreshed on demand).
172    ///
173    /// F6: clones the `Arc<McpClient>` handles out of the lock before issuing
174    /// remote calls so register/unregister/toggle aren't blocked for the
175    /// full round-trip duration.
176    pub async fn list_tools(&self) -> Result<Vec<McpTool>> {
177        let clients: Vec<(String, Arc<McpClient>)> = self
178            .clients
179            .read()
180            .await
181            .iter()
182            .map(|(k, v)| (k.clone(), v.clone()))
183            .collect();
184
185        let mut all_tools = Vec::new();
186        for (name, client) in &clients {
187            if let Ok(mcp_tools) = client.list_tools().await {
188                let start = all_tools.len();
189                all_tools.extend(mcp_tools);
190                *self
191                    .tool_cache
192                    .write()
193                    .await
194                    .entry(name.clone())
195                    .or_insert_with(Vec::new) = all_tools[start..].to_vec();
196            }
197        }
198
199        Ok(all_tools)
200    }
201
202    /// Get cached tools for a specific server.
203    pub async fn cached_tools(&self, server_name: &str) -> Option<Vec<McpTool>> {
204        self.tool_cache.read().await.get(server_name).cloned()
205    }
206
207    /// Call an MCP tool on a specific server.
208    ///
209    /// F6: releases the clients read lock before the remote `call_tool`.
210    pub async fn call_tool(
211        &self,
212        server_name: &str,
213        tool_name: &str,
214        args: serde_json::Value,
215    ) -> Result<McpToolCallResult> {
216        let client = {
217            let clients = self.clients.read().await;
218            clients
219                .get(server_name)
220                .cloned()
221                .ok_or_else(|| anyhow!("MCP server '{server_name}' not connected"))?
222        };
223
224        client.call_tool(tool_name, args).await
225    }
226
227    /// Shutdown all connected MCP server processes.
228    pub async fn shutdown_all(&self) -> Result<()> {
229        let mut clients = self.clients.write().await;
230
231        for (name, client) in clients.drain() {
232            if let Err(e) = client.shutdown().await {
233                tracing::warn!(server = %name, error = %e, "Error shutting down MCP server");
234            }
235        }
236
237        self.tool_cache.write().await.clear();
238        Ok(())
239    }
240
241    /// Refresh tools from a specific server.
242    ///
243    /// F6: releases the clients read lock before the remote `refresh_tools`.
244    pub async fn refresh_tools(&self, server_name: &str) -> Result<Vec<McpTool>> {
245        let client = {
246            let clients = self.clients.read().await;
247            clients
248                .get(server_name)
249                .cloned()
250                .ok_or_else(|| anyhow!("MCP server '{server_name}' not connected"))?
251        };
252
253        let mcp_tools = client.refresh_tools().await?;
254
255        *self
256            .tool_cache
257            .write()
258            .await
259            .entry(server_name.to_string())
260            .or_insert_with(Vec::new) = mcp_tools.clone();
261
262        Ok(mcp_tools)
263    }
264
265    /// Clear the tool cache for a server.
266    pub async fn clear_cache(&self, server_name: &str) {
267        self.tool_cache.write().await.remove(server_name);
268    }
269
270    /// Remove a server by name (disconnects client if active, removes config).
271    pub async fn remove_server(&self, name: &str) -> Result<()> {
272        // Shut down client if active.
273        if let Some(client) = self.clients.write().await.remove(name)
274            && let Err(e) = client.shutdown().await
275        {
276            tracing::warn!(server = %name, error = %e, "Error shutting down MCP server during removal");
277        }
278        // Remove from config list.
279        let found = {
280            let mut servers = self.servers.write();
281            let len_before = servers.len();
282            servers.retain(|s| s.name != name);
283            servers.len() != len_before
284        };
285        if !found {
286            return Err(anyhow!("MCP server '{name}' not found"));
287        }
288        // Clear cache.
289        self.tool_cache.write().await.remove(name);
290        Ok(())
291    }
292    /// Update a server's configuration in place and restart it.
293    ///
294    /// Replaces the config entry, disconnects the old client if active, and
295    /// re-initializes. The `name` is the key and cannot be changed via this
296    /// method — callers wanting a new name should register a new server and
297    /// remove the old one. Returns the updated server config.
298    pub async fn update_server(
299        &self,
300        name: &str,
301        command: String,
302        args: Vec<String>,
303        env: HashMap<String, String>,
304        enabled: bool,
305    ) -> Result<McpServer> {
306        // Stop the existing client (if any) before mutating the config so
307        // the new spawn does not race the old one.
308        if let Some(client) = self.clients.write().await.remove(name)
309            && let Err(e) = client.shutdown().await
310        {
311            tracing::warn!(server = %name, error = %e, "Error shutting down MCP server during update");
312        }
313        self.tool_cache.write().await.remove(name);
314
315        // Overwrite the config entry (register_server already handles
316        // in-place replacement for duplicate names).
317        let server = McpServer {
318            name: name.to_string(),
319            command,
320            args,
321            env,
322            enabled,
323        };
324        self.register_server(server.clone());
325        Ok(server)
326    }
327
328    /// Toggle a server's enabled flag. Returns the new enabled state.
329    pub async fn toggle_server(&self, name: &str) -> Result<bool> {
330        // Extract new_state inside a block scope so the parking_lot lock
331        // is released before any .await points in the async block below.
332        let new_state = {
333            let mut servers = self.servers.write();
334            let server = servers
335                .iter_mut()
336                .find(|s| s.name == name)
337                .ok_or_else(|| anyhow!("MCP server '{name}' not found"))?;
338            server.enabled = !server.enabled;
339            server.enabled
340        };
341
342        // If disabled, disconnect the client (now fully outside the lock).
343        if !new_state {
344            if let Some(client) = self.clients.write().await.remove(name)
345                && let Err(e) = client.shutdown().await
346            {
347                tracing::warn!(server = %name, error = %e, "Error shutting down MCP server on disable");
348            }
349            self.tool_cache.write().await.remove(name);
350        }
351
352        Ok(new_state)
353    }
354
355    /// Clear all caches.
356    pub async fn clear_all_caches(&self) {
357        self.tool_cache.write().await.clear();
358    }
359}
360
361impl Default for McpBridge {
362    fn default() -> Self {
363        Self::new()
364    }
365}
366
367// ============================================================================
368// Tests
369// ============================================================================
370
371#[cfg(test)]
372mod tests {
373    use super::*;
374    use tokio::time::Duration;
375
376    // --- McpServer tests ---
377
378    #[test]
379    fn test_mcp_server_builder() {
380        let server = McpServer::new("test-server", "npx")
381            .with_args(vec!["-y".to_string(), "@anthropic/mcp-server".to_string()])
382            .with_env("DEBUG", "true");
383
384        assert_eq!(server.name, "test-server");
385        assert_eq!(server.command, "npx");
386        assert_eq!(server.args, vec!["-y", "@anthropic/mcp-server"]);
387        assert_eq!(server.env.get("DEBUG"), Some(&"true".to_string()));
388        assert!(server.enabled);
389    }
390
391    // --- McpBridge::update_server tests ---
392
393    #[tokio::test]
394    async fn test_update_server_replaces_config() {
395        let bridge = McpBridge::new();
396        bridge.register_server(
397            McpServer::new("fs", "npx")
398                .with_args(vec!["old".to_string()])
399                .with_env("OLD", "1"),
400        );
401
402        // Update with new command/args/env/enabled=false (no init).
403        let mut env = HashMap::new();
404        env.insert("NEW".to_string(), "2".to_string());
405        let updated = bridge
406            .update_server(
407                "fs",
408                "node".to_string(),
409                vec!["new.js".to_string()],
410                env,
411                false,
412            )
413            .await
414            .expect("update");
415
416        assert_eq!(updated.command, "node");
417        assert_eq!(updated.args, vec!["new.js"]);
418        assert_eq!(updated.env.get("NEW"), Some(&"2".to_string()));
419        assert!(!updated.enabled);
420
421        let stored = bridge.get_server("fs").expect("get_server");
422        assert_eq!(stored.command, "node");
423        assert!(!stored.enabled);
424        // Old env entry must be gone (config replaced, not merged).
425        assert!(!stored.env.contains_key("OLD"));
426    }
427
428    #[tokio::test]
429    async fn test_update_server_unknown_name_returns_error() {
430        let bridge = McpBridge::new();
431        let result = bridge
432            .update_server("nope", "x".to_string(), vec![], HashMap::new(), true)
433            .await;
434        // No prior registration — `register_server` appends, so this succeeds
435        // but creates a new entry. The method is update-in-place, but the
436        // bridge accepts "update a non-existent server" as a create-via-update.
437        // The route layer is the one that enforces existence (it checks
438        // `previous` for rollback); the bridge stays simple.
439        assert!(result.is_ok());
440    }
441
442    // --- JSON-RPC request/response tests ---
443
444    #[test]
445    fn test_mcp_request_serialization() {
446        let request = McpRequest::new("tools/list");
447        let json = serde_json::to_string(&request).unwrap();
448
449        assert!(json.contains(r#""method":"tools/list""#));
450        assert!(json.contains(r#""jsonrpc":"2.0""#));
451    }
452
453    #[test]
454    fn test_mcp_request_with_params() {
455        let request = McpRequest::new("tools/call").with_params(serde_json::json!({
456            "name": "my_tool",
457            "arguments": {"arg1": "value1"}
458        }));
459
460        let json = serde_json::to_string(&request).unwrap();
461        assert!(json.contains("my_tool"));
462        assert!(json.contains("arg1"));
463    }
464
465    #[test]
466    fn test_mcp_request_to_jsonl() {
467        let request = McpRequest::new("initialize");
468        let jsonl = request.to_jsonl().unwrap();
469
470        // Should end with newline
471        assert_eq!(jsonl.last(), Some(&b'\n'));
472
473        // Should parse back
474        let json_str = String::from_utf8_lossy(&jsonl[..jsonl.len() - 1]);
475        let parsed: McpRequest = serde_json::from_str(&json_str).unwrap();
476        assert_eq!(parsed.method, "initialize");
477    }
478
479    #[test]
480    fn test_mcp_response_result() {
481        let response = McpResponse {
482            jsonrpc: "2.0".to_string(),
483            id: serde_json::json!(1),
484            result: Some(serde_json::json!({"tools": []})),
485            error: None,
486        };
487
488        assert!(!response.is_error());
489        let result = response.clone().into_result().unwrap();
490        assert!(result.get("tools").is_some());
491    }
492
493    #[test]
494    fn test_mcp_response_error() {
495        let response = McpResponse {
496            jsonrpc: "2.0".to_string(),
497            id: serde_json::json!(2),
498            result: None,
499            error: Some(McpError::internal_error("Something went wrong")),
500        };
501
502        assert!(response.is_error());
503        let err = response.into_result().unwrap_err();
504        assert!(err.to_string().contains("internal error"));
505    }
506
507    #[test]
508    fn test_mcp_error_codes() {
509        assert_eq!(McpError::parse_error().code, -32700);
510        assert_eq!(McpError::invalid_request("test").code, -32600);
511        assert_eq!(McpError::method_not_found().code, -32601);
512        assert_eq!(McpError::invalid_params().code, -32602);
513        assert_eq!(McpError::internal_error("x").code, -32603);
514        assert_eq!(McpError::server_error("x").code, -32000);
515    }
516
517    // --- McpBridge registration tests ---
518
519    #[test]
520    fn test_bridge_registration() {
521        let bridge = McpBridge::new();
522
523        bridge.register_server(McpServer::new("test", "echo"));
524
525        assert_eq!(bridge.servers(), vec!["test"]);
526        assert!(bridge.get_server("test").is_some());
527        assert!(bridge.get_server("missing").is_none());
528    }
529
530    // --- McpClient lifecycle tests ---
531
532    #[tokio::test]
533    async fn test_mcp_client_non_existent_command() {
534        let server = McpServer::new("ghost", "nonexistent-binary-xyz");
535        let client = McpClient::new(server);
536
537        let result = client.initialize().await;
538        assert!(result.is_err());
539        assert!(result.unwrap_err().to_string().contains("Failed to spawn"));
540    }
541
542    #[tokio::test]
543    async fn test_mcp_client_shutdown_no_panic() {
544        let server = McpServer::new("test-shutdown", "echo");
545        let client = McpClient::new(server);
546
547        // Shutting down without initializing should not panic
548        client.shutdown().await.expect("shutdown should succeed");
549        assert!(!client.is_initialized().await);
550    }
551
552    #[tokio::test]
553    async fn test_mcp_client_with_timeout() {
554        let server = McpServer::new("test", "sleep").with_args(vec!["999".to_string()]);
555        let client = McpClient::new(server).with_timeout(Duration::from_millis(100));
556
557        // This will spawn a sleep process that hangs
558        let result = client.initialize().await;
559        // Should timeout, not panic
560        assert!(result.is_err());
561    }
562
563    // --- McpBridge lifecycle tests ---
564
565    #[tokio::test]
566    async fn test_bridge_initialize_all_empty() {
567        let bridge = McpBridge::new();
568        bridge
569            .initialize_all()
570            .await
571            .expect("empty bridge should initialize");
572    }
573
574    #[tokio::test]
575    async fn test_bridge_initialize_all_fails_gracefully() {
576        let bridge = McpBridge::new();
577        bridge.register_server(McpServer::new("ghost", "nonexistent-cmd-xyz"));
578        bridge.register_server(McpServer::new("ghost2", "nonexistent-cmd-abc"));
579
580        let result = bridge.initialize_all().await;
581        // Should fail because all servers fail to spawn
582        assert!(result.is_err());
583    }
584
585    #[tokio::test]
586    async fn test_bridge_shutdown_all_empty() {
587        let bridge = McpBridge::new();
588        bridge
589            .shutdown_all()
590            .await
591            .expect("empty bridge shutdown should succeed");
592    }
593
594    #[tokio::test]
595    async fn test_bridge_call_tool_no_server() {
596        let bridge = McpBridge::new();
597        let result = bridge
598            .call_tool("ghost", "tool", serde_json::json!({}))
599            .await;
600        assert!(result.is_err());
601        assert!(result.unwrap_err().to_string().contains("not connected"));
602    }
603
604    #[tokio::test]
605    async fn test_bridge_initialize_server_not_found() {
606        let bridge = McpBridge::new();
607        let result = bridge.initialize_server("missing").await;
608        assert!(result.is_err());
609    }
610
611    #[test]
612    fn test_mcp_client_debug() {
613        let server = McpServer::new("debug-test", "echo");
614        let client = McpClient::new(server);
615        let debug = format!("{client:?}");
616        assert!(debug.contains("debug-test"));
617    }
618
619    // --- Double-init guard ---
620
621    #[tokio::test]
622    async fn test_mcp_client_double_init_ignored() {
623        let server = McpServer::new("echo", "echo");
624        let client = McpClient::new(server);
625
626        // First init will fail because "echo" doesn't speak MCP protocol
627        let _ = client.initialize().await;
628        // But calling is_initialized() should not panic
629        let _ = client.is_initialized().await;
630    }
631}