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