Skip to main content

recursive/
mcp_server.rs

1//! MCP server lifecycle management.
2//!
3//! This module provides [`McpServerManager`], which handles spawning MCP
4//! servers (stdio or HTTP+SSE), discovering their tools, and registering
5//! them into the agent's [`ToolRegistry`]. Each MCP tool is wrapped in a
6//! thin adapter that delegates execution to the corresponding server.
7//!
8//! # Architecture
9//!
10//! ```text
11//! ┌─────────────────────────────────────────────┐
12//! │              McpServerManager               │
13//! │  ┌──────────┐  ┌──────────┐  ┌──────────┐  │
14//! │  │ Server A │  │ Server B │  │ Server C │  │
15//! │  │ (stdio)  │  │ (SSE)    │  │ (stdio)  │  │
16//! │  └────┬─────┘  └────┬─────┘  └────┬─────┘  │
17//! │       │              │              │        │
18//! │  ┌────▼──────────────▼──────────────▼────┐  │
19//! │  │          ToolRegistry                 │  │
20//! │  │  mcp__A__tool1  mcp__B__tool2  ...    │  │
21//! │  └───────────────────────────────────────┘  │
22//! └─────────────────────────────────────────────┘
23//! ```
24
25use std::collections::HashMap;
26use std::sync::Arc;
27
28use tokio::sync::Mutex;
29use tracing::{info, instrument};
30
31use crate::error::{Error, Result};
32use crate::llm::ToolSpec;
33use crate::mcp::{McpClient, McpServer, McpTool};
34use crate::tools::ToolRegistry;
35use serde::{Deserialize, Serialize};
36use serde_json::Value;
37use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
38
39// ---------------------------------------------------------------------------
40// JSON-RPC 2.0 protocol types (MCP server side)
41// ---------------------------------------------------------------------------
42
43/// A JSON-RPC 2.0 request.
44#[derive(Debug, Deserialize)]
45pub struct JsonRpcRequest {
46    pub jsonrpc: String,
47    #[serde(default)]
48    pub id: Option<Value>,
49    pub method: String,
50    #[serde(default)]
51    pub params: Value,
52}
53
54/// A JSON-RPC 2.0 response.
55#[derive(Debug, Serialize)]
56pub struct JsonRpcResponse {
57    pub jsonrpc: String,
58    pub id: Value,
59    #[serde(skip_serializing_if = "Option::is_none")]
60    pub result: Option<Value>,
61    #[serde(skip_serializing_if = "Option::is_none")]
62    pub error: Option<JsonRpcError>,
63}
64
65/// A JSON-RPC 2.0 error object.
66#[derive(Debug, Serialize)]
67pub struct JsonRpcError {
68    pub code: i32,
69    pub message: String,
70}
71
72impl JsonRpcResponse {
73    /// Create a success response.
74    pub fn success(id: Value, result: Value) -> Self {
75        Self {
76            jsonrpc: "2.0".to_string(),
77            id,
78            result: Some(result),
79            error: None,
80        }
81    }
82
83    /// Create an error response.
84    pub fn error(id: Value, code: i32, message: String) -> Self {
85        Self {
86            jsonrpc: "2.0".to_string(),
87            id,
88            result: None,
89            error: Some(JsonRpcError { code, message }),
90        }
91    }
92
93    /// Create a parse error response (-32700).
94    pub fn parse_error() -> Self {
95        Self::error(Value::Null, -32700, "Parse error".to_string())
96    }
97
98    /// Create a method not found error response (-32601).
99    pub fn method_not_found(id: Value) -> Self {
100        Self::error(id, -32601, "Method not found".to_string())
101    }
102}
103
104// ---------------------------------------------------------------------------
105// Dispatch
106// ---------------------------------------------------------------------------
107
108/// Dispatch a parsed JSON-RPC request and return a response.
109/// Returns `None` for notifications (no response needed).
110pub async fn dispatch_request(
111    request: &JsonRpcRequest,
112    tool_specs: &[ToolSpec],
113    tools: &ToolRegistry,
114) -> Option<JsonRpcResponse> {
115    match request.method.as_str() {
116        "initialize" => Some(handle_initialize(request)),
117        "notifications/initialized" => None,
118        "tools/list" => Some(handle_tools_list(request, tool_specs)),
119        "tools/call" => Some(handle_tools_call(request, tools).await),
120        _ => {
121            let id = request.id.clone().unwrap_or(Value::Null);
122            Some(JsonRpcResponse::method_not_found(id))
123        }
124    }
125}
126
127/// Handle `initialize` — return server capabilities and info.
128fn handle_initialize(request: &JsonRpcRequest) -> JsonRpcResponse {
129    let id = request.id.clone().unwrap_or(Value::Null);
130    JsonRpcResponse::success(
131        id,
132        serde_json::json!({
133            "protocolVersion": "2024-11-05",
134            "capabilities": {
135                "tools": {}
136            },
137            "serverInfo": {
138                "name": "recursive-mcp-server",
139                "version": "0.1.0"
140            }
141        }),
142    )
143}
144
145/// Handle `tools/list` — return the list of tool specs in MCP format.
146fn handle_tools_list(request: &JsonRpcRequest, tool_specs: &[ToolSpec]) -> JsonRpcResponse {
147    let id = request.id.clone().unwrap_or(Value::Null);
148    let tools: Vec<Value> = tool_specs
149        .iter()
150        .map(|spec| {
151            serde_json::json!({
152                "name": spec.name,
153                "description": spec.description,
154                "inputSchema": spec.parameters,
155            })
156        })
157        .collect();
158    JsonRpcResponse::success(id, serde_json::json!({ "tools": tools }))
159}
160
161/// Handle `tools/call` — execute a tool and return the result.
162async fn handle_tools_call(request: &JsonRpcRequest, tools: &ToolRegistry) -> JsonRpcResponse {
163    let id = request.id.clone().unwrap_or(Value::Null);
164    let tool_name = request
165        .params
166        .get("name")
167        .and_then(|v| v.as_str())
168        .unwrap_or("");
169    let arguments = request
170        .params
171        .get("arguments")
172        .cloned()
173        .unwrap_or(serde_json::json!({}));
174
175    if tool_name.is_empty() {
176        return JsonRpcResponse::error(id, -32602, "Missing tool name".to_string());
177    }
178
179    match tools.get(tool_name) {
180        Some(tool) => match tool.execute(arguments).await {
181            Ok(text) => JsonRpcResponse::success(
182                id,
183                serde_json::json!({
184                    "content": [{"type": "text", "text": text}]
185                }),
186            ),
187            Err(e) => JsonRpcResponse::success(
188                id,
189                serde_json::json!({
190                    "isError": true,
191                    "content": [{"type": "text", "text": e.to_string()}]
192                }),
193            ),
194        },
195        None => JsonRpcResponse::error(id, -32602, format!("Tool not found: {tool_name}")),
196    }
197}
198
199// ---------------------------------------------------------------------------
200// McpServerRunner — stdio transport loop
201// ---------------------------------------------------------------------------
202
203/// Runs the MCP server stdio loop: reads JSON-RPC from stdin, dispatches,
204/// and writes responses to stdout.
205pub struct McpServerRunner {
206    tool_specs: Vec<ToolSpec>,
207    tools: ToolRegistry,
208}
209
210impl McpServerRunner {
211    /// Create a new runner from a [`ToolRegistry`].
212    ///
213    /// The tool specs are extracted immediately so they can be served
214    /// without holding a borrow on the registry.
215    pub fn new(tools: ToolRegistry) -> Self {
216        let tool_specs = tools.specs();
217        Self { tool_specs, tools }
218    }
219
220    /// Run the stdio server loop until EOF on stdin.
221    pub async fn run(&self) -> Result<()> {
222        let stdin = tokio::io::stdin();
223        let stdout = tokio::io::stdout();
224        self.run_on(BufReader::new(stdin), stdout).await
225    }
226
227    /// Run the server loop on generic reader/writer (testable).
228    pub async fn run_on<R, W>(&self, reader: R, writer: W) -> Result<()>
229    where
230        R: tokio::io::AsyncBufRead + Unpin,
231        W: tokio::io::AsyncWrite + Unpin,
232    {
233        let mut lines = BufReader::new(reader).lines();
234        let mut out = writer;
235
236        while let Some(line) = lines.next_line().await? {
237            let line = line.trim().to_string();
238            if line.is_empty() {
239                continue;
240            }
241
242            // Parse the request
243            let request: JsonRpcRequest = match serde_json::from_str(&line) {
244                Ok(req) => req,
245                Err(_) => {
246                    let resp = JsonRpcResponse::parse_error();
247                    let json = serde_json::to_string(&resp)?;
248                    out.write_all(json.as_bytes()).await?;
249                    out.write_all(b"\n").await?;
250                    out.flush().await?;
251                    continue;
252                }
253            };
254
255            // Dispatch
256            if let Some(response) = dispatch_request(&request, &self.tool_specs, &self.tools).await
257            {
258                let json = serde_json::to_string(&response)?;
259                out.write_all(json.as_bytes()).await?;
260                out.write_all(b"\n").await?;
261                out.flush().await?;
262            }
263        }
264
265        Ok(())
266    }
267}
268
269/// Manages the lifecycle of one or more MCP servers and their tools.
270///
271/// Call [`McpServerManager::register_all`] to spawn every configured server,
272/// discover its tools, and register them into a [`ToolRegistry`]. The manager
273/// keeps the underlying [`McpClient`]s alive so they can handle tool calls.
274pub struct McpServerManager {
275    /// Configured servers.
276    servers: Vec<McpServer>,
277    /// Running clients, keyed by server name.
278    clients: Mutex<HashMap<String, Arc<Mutex<McpClient>>>>,
279}
280
281impl McpServerManager {
282    /// Create a new manager from a list of server configurations.
283    ///
284    /// The servers are not spawned until [`register_all`](Self::register_all) is called.
285    pub fn new(servers: Vec<McpServer>) -> Self {
286        Self {
287            servers,
288            clients: Mutex::new(HashMap::new()),
289        }
290    }
291
292    /// Spawn all configured servers, discover their tools, and register them
293    /// into the given [`ToolRegistry`].
294    ///
295    /// Returns a list of `(server_name, tool_count)` pairs for logging.
296    ///
297    /// # Errors
298    ///
299    /// Returns an error if any server fails to start or if tool discovery
300    /// fails. Servers are started sequentially; a failure stops the process.
301    #[instrument(skip_all, name = "mcp.register_all")]
302    pub async fn register_all(&self, registry: &mut ToolRegistry) -> Result<Vec<(String, usize)>> {
303        let mut results = Vec::new();
304
305        for server in &self.servers {
306            let name = server.name.clone();
307            info!(server = %name, "Starting MCP server");
308
309            let client = McpClient::spawn(server).await.map_err(|e| Error::Tool {
310                name: format!("mcp_server:{name}"),
311                message: format!("Failed to start MCP server: {e}"),
312            })?;
313
314            let client = Arc::new(Mutex::new(client));
315
316            // Discover tools from this server.
317            let tool_specs = client
318                .lock()
319                .await
320                .list_tools()
321                .await
322                .map_err(|e| Error::Tool {
323                    name: format!("mcp_server:{name}"),
324                    message: format!("Failed to discover tools from MCP server: {e}"),
325                })?;
326
327            let tool_count = tool_specs.len();
328            info!(
329                server = %name,
330                count = tool_count,
331                "Discovered MCP tools"
332            );
333
334            // Wrap each tool spec in an McpTool and register it.
335            for spec in &tool_specs {
336                let tool = McpTool::new(client.clone(), spec.clone(), &name);
337                registry.register_mut(Arc::new(tool));
338                info!(
339                    server = %name,
340                    tool = %spec.name,
341                    "Registered MCP tool"
342                );
343            }
344
345            // Store the client so it stays alive.
346            self.clients.lock().await.insert(name.clone(), client);
347            results.push((name, tool_count));
348        }
349
350        Ok(results)
351    }
352
353    /// Shut down all running MCP clients.
354    ///
355    /// This drops the clients, which causes their background tasks (stdio
356    /// reader/writer, SSE listener) to be cancelled.
357    pub async fn shutdown(&self) {
358        let mut clients = self.clients.lock().await;
359        let names: Vec<String> = clients.keys().cloned().collect();
360        for name in &names {
361            info!(server = %name, "Shutting down MCP server");
362            clients.remove(name);
363        }
364    }
365
366    /// Return the number of currently running clients.
367    pub async fn running_count(&self) -> usize {
368        self.clients.lock().await.len()
369    }
370}
371
372#[cfg(test)]
373mod tests {
374    use super::*;
375    use crate::mcp::McpServer;
376
377    /// Test that an empty server list produces no registrations.
378    #[tokio::test]
379    async fn empty_servers_registers_nothing() {
380        let manager = McpServerManager::new(vec![]);
381        let mut registry = ToolRegistry::local();
382        let results = manager.register_all(&mut registry).await.unwrap();
383        assert!(results.is_empty());
384        assert!(registry.names().is_empty());
385    }
386
387    /// Test that tool names are correctly namespaced.
388    #[test]
389    fn tool_name_format() {
390        let server = "my-server";
391        let tool = "read_file";
392        let namespaced = format!("mcp__{}__{}", server, tool);
393        assert_eq!(namespaced, "mcp__my-server__read_file");
394    }
395
396    /// Test that a server config with a URL creates an SSE-based client
397    /// (as opposed to stdio). This is a config-level test only.
398    #[test]
399    fn sse_server_config_detection() {
400        let server = McpServer {
401            name: "test-sse".into(),
402            command: String::new(),
403            args: vec![],
404            url: Some("http://localhost:8080/sse".into()),
405        };
406        // The McpClient::spawn method checks server.url.is_some()
407        // to decide transport. We verify the config is wired correctly.
408        assert!(server.url.is_some());
409        assert!(server.command.is_empty());
410    }
411
412    /// Test that a server config with a command creates a stdio-based client.
413    #[test]
414    fn stdio_server_config_detection() {
415        let server = McpServer {
416            name: "test-stdio".into(),
417            command: "echo".into(),
418            args: vec!["hello".into()],
419            url: None,
420        };
421        assert!(!server.command.is_empty());
422        assert!(server.url.is_none());
423    }
424}