Skip to main content

turbomcp_core/
router.rs

1//! Shared MCP request router for all platforms.
2//!
3//! This module provides the core routing logic that maps JSON-RPC requests to
4//! `McpHandler` methods. It is designed to work on both native and WASM targets.
5//!
6//! # Design Philosophy
7//!
8//! - **Unified**: Single router implementation for native and WASM
9//! - **no_std Compatible**: Works in `no_std` environments with `alloc`
10//! - **Extensible**: Native can layer additional validation (protocol, capabilities)
11//!
12//! # Example
13//!
14//! ```rust,ignore
15//! use turbomcp_core::router::{route_request, RouteConfig};
16//! use turbomcp_core::jsonrpc::{JsonRpcIncoming, JsonRpcOutgoing};
17//! use turbomcp_core::context::RequestContext;
18//!
19//! // Basic routing (WASM-friendly)
20//! let response = route_request(&handler, request, &ctx, &RouteConfig::default()).await;
21//!
22//! // With protocol version override
23//! let config = RouteConfig {
24//!     protocol_version: Some("2025-11-25"),
25//! };
26//! let response = route_request(&handler, request, &ctx, &config).await;
27//! ```
28
29use alloc::string::ToString;
30use serde_json::Value;
31
32use crate::PROTOCOL_VERSION;
33use crate::context::RequestContext;
34use crate::error::McpError;
35use crate::handler::McpHandler;
36use crate::jsonrpc::{JsonRpcIncoming, JsonRpcOutgoing};
37use turbomcp_types::ServerInfo;
38
39/// Configuration for request routing.
40///
41/// This provides minimal configuration that works on all platforms.
42/// Native platforms can provide additional validation through middleware.
43#[derive(Debug, Clone, Default)]
44pub struct RouteConfig<'a> {
45    /// Override protocol version in initialize response.
46    /// If None, uses `PROTOCOL_VERSION` constant.
47    pub protocol_version: Option<&'a str>,
48}
49
50/// Route a JSON-RPC request to the appropriate handler method.
51///
52/// This is the core routing function used by both native and WASM platforms.
53/// It dispatches MCP methods to the corresponding `McpHandler` methods.
54///
55/// # Supported Methods
56///
57/// - `initialize` - Returns server info and capabilities
58/// - `initialized` / `notifications/initialized` - Acknowledges initialization
59/// - `tools/list` - Lists available tools
60/// - `tools/call` - Calls a tool by name
61/// - `resources/list` - Lists available resources
62/// - `resources/read` - Reads a resource by URI
63/// - `prompts/list` - Lists available prompts
64/// - `prompts/get` - Gets a prompt by name
65/// - `ping` - Health check
66///
67/// # Example
68///
69/// ```rust,ignore
70/// use turbomcp_core::router::{route_request, RouteConfig};
71///
72/// let config = RouteConfig::default();
73/// let response = route_request(&handler, request, &ctx, &config).await;
74///
75/// if response.should_send() {
76///     // Send response to client
77/// }
78/// ```
79pub async fn route_request<H: McpHandler>(
80    handler: &H,
81    request: JsonRpcIncoming,
82    ctx: &RequestContext,
83    config: &RouteConfig<'_>,
84) -> JsonRpcOutgoing {
85    let id = request.id.clone();
86
87    match request.method.as_str() {
88        // Initialize handshake
89        "initialize" => {
90            let params = request.params.clone().unwrap_or_default();
91
92            // Validate clientInfo is present (MCP spec requirement)
93            let Some(client_info) = params.get("clientInfo") else {
94                return JsonRpcOutgoing::error(
95                    id,
96                    McpError::invalid_params("Missing required field: clientInfo"),
97                );
98            };
99
100            // Validate clientInfo has required fields
101            let client_name = client_info.get("name").and_then(|v| v.as_str());
102            let client_version = client_info.get("version").and_then(|v| v.as_str());
103            if client_name.is_none() || client_version.is_none() {
104                return JsonRpcOutgoing::error(
105                    id,
106                    McpError::invalid_params("clientInfo must contain 'name' and 'version' fields"),
107                );
108            }
109
110            let protocol_version = config.protocol_version.unwrap_or(PROTOCOL_VERSION);
111            let info = handler.server_info();
112            let result = build_initialize_result(&info, handler, protocol_version);
113            JsonRpcOutgoing::success(id, result)
114        }
115
116        // Handle both "initialized" and "notifications/initialized"
117        // Per JSON-RPC 2.0, notifications (no id) should not receive responses
118        "initialized" | "notifications/initialized" => {
119            if id.is_some() {
120                JsonRpcOutgoing::success(id, serde_json::json!({}))
121            } else {
122                JsonRpcOutgoing::notification_ack()
123            }
124        }
125
126        // Tool methods
127        "tools/list" => {
128            let tools = handler.list_tools();
129            let result = serde_json::json!({ "tools": tools });
130            JsonRpcOutgoing::success(id, result)
131        }
132
133        "tools/call" => {
134            let params = request.params.unwrap_or_default();
135            let name = params
136                .get("name")
137                .and_then(|v| v.as_str())
138                .unwrap_or_default();
139            let args = params.get("arguments").cloned().unwrap_or_default();
140
141            match handler.call_tool(name, args, ctx).await {
142                Ok(result) => match serde_json::to_value(&result) {
143                    Ok(result_value) => JsonRpcOutgoing::success(id, result_value),
144                    Err(e) => JsonRpcOutgoing::error(
145                        id,
146                        McpError::internal(alloc::format!(
147                            "Failed to serialize tool result: {}",
148                            e
149                        )),
150                    ),
151                },
152                Err(err) => JsonRpcOutgoing::error(id, err),
153            }
154        }
155
156        // Resource methods
157        "resources/list" => {
158            let resources = handler.list_resources();
159            let result = serde_json::json!({ "resources": resources });
160            JsonRpcOutgoing::success(id, result)
161        }
162
163        "resources/read" => {
164            let params = request.params.unwrap_or_default();
165            let uri = params
166                .get("uri")
167                .and_then(|v| v.as_str())
168                .unwrap_or_default();
169
170            match handler.read_resource(uri, ctx).await {
171                Ok(result) => match serde_json::to_value(&result) {
172                    Ok(result_value) => JsonRpcOutgoing::success(id, result_value),
173                    Err(e) => JsonRpcOutgoing::error(
174                        id,
175                        McpError::internal(alloc::format!(
176                            "Failed to serialize resource result: {}",
177                            e
178                        )),
179                    ),
180                },
181                Err(err) => JsonRpcOutgoing::error(id, err),
182            }
183        }
184
185        // Prompt methods
186        "prompts/list" => {
187            let prompts = handler.list_prompts();
188            let result = serde_json::json!({ "prompts": prompts });
189            JsonRpcOutgoing::success(id, result)
190        }
191
192        "prompts/get" => {
193            let params = request.params.unwrap_or_default();
194            let name = params
195                .get("name")
196                .and_then(|v| v.as_str())
197                .unwrap_or_default();
198            let args = params.get("arguments").cloned();
199
200            match handler.get_prompt(name, args, ctx).await {
201                Ok(result) => match serde_json::to_value(&result) {
202                    Ok(result_value) => JsonRpcOutgoing::success(id, result_value),
203                    Err(e) => JsonRpcOutgoing::error(
204                        id,
205                        McpError::internal(alloc::format!(
206                            "Failed to serialize prompt result: {}",
207                            e
208                        )),
209                    ),
210                },
211                Err(err) => JsonRpcOutgoing::error(id, err),
212            }
213        }
214
215        // Task methods (SEP-1686)
216        "tasks/list" => {
217            let params = request.params.unwrap_or_default();
218            let cursor = params.get("cursor").and_then(|v| v.as_str());
219            let limit = params
220                .get("limit")
221                .and_then(|v| v.as_u64())
222                .map(|v| v as usize);
223
224            match handler.list_tasks(cursor, limit, ctx).await {
225                Ok(result) => match serde_json::to_value(&result) {
226                    Ok(v) => JsonRpcOutgoing::success(id, v),
227                    Err(e) => JsonRpcOutgoing::error(id, McpError::internal(e.to_string())),
228                },
229                Err(err) => JsonRpcOutgoing::error(id, err),
230            }
231        }
232
233        "tasks/get" => {
234            let params = request.params.unwrap_or_default();
235            let Some(task_id) = params.get("taskId").and_then(|v| v.as_str()) else {
236                return JsonRpcOutgoing::error(id, McpError::invalid_params("Missing taskId"));
237            };
238
239            match handler.get_task(task_id, ctx).await {
240                Ok(result) => match serde_json::to_value(&result) {
241                    Ok(v) => JsonRpcOutgoing::success(id, v),
242                    Err(e) => JsonRpcOutgoing::error(id, McpError::internal(e.to_string())),
243                },
244                Err(err) => JsonRpcOutgoing::error(id, err),
245            }
246        }
247
248        "tasks/cancel" => {
249            let params = request.params.unwrap_or_default();
250            let Some(task_id) = params.get("taskId").and_then(|v| v.as_str()) else {
251                return JsonRpcOutgoing::error(id, McpError::invalid_params("Missing taskId"));
252            };
253
254            match handler.cancel_task(task_id, ctx).await {
255                Ok(result) => match serde_json::to_value(&result) {
256                    Ok(v) => JsonRpcOutgoing::success(id, v),
257                    Err(e) => JsonRpcOutgoing::error(id, McpError::internal(e.to_string())),
258                },
259                Err(err) => JsonRpcOutgoing::error(id, err),
260            }
261        }
262
263        "tasks/result" => {
264            let params = request.params.unwrap_or_default();
265            let Some(task_id) = params.get("taskId").and_then(|v| v.as_str()) else {
266                return JsonRpcOutgoing::error(id, McpError::invalid_params("Missing taskId"));
267            };
268
269            match handler.get_task_result(task_id, ctx).await {
270                Ok(result) => JsonRpcOutgoing::success(id, result),
271                Err(err) => JsonRpcOutgoing::error(id, err),
272            }
273        }
274
275        // Ping
276        "ping" => JsonRpcOutgoing::success(id, serde_json::json!({})),
277
278        // Unknown method
279        _ => JsonRpcOutgoing::error(id, McpError::method_not_found(&request.method)),
280    }
281}
282
283/// Build the initialize result with server info and capabilities.
284///
285/// # MCP Spec Compliance
286///
287/// The capabilities object follows the MCP 2025-11-25 specification:
288/// - Each capability is an object (not boolean)
289/// - Capabilities are only included if the server supports them
290/// - Sub-properties like `listChanged` indicate notification support
291fn build_initialize_result<H: McpHandler>(
292    info: &ServerInfo,
293    handler: &H,
294    protocol_version: &str,
295) -> Value {
296    let has_tools = !handler.list_tools().is_empty();
297    let has_resources = !handler.list_resources().is_empty();
298    let has_prompts = !handler.list_prompts().is_empty();
299
300    // Build capabilities object per MCP spec
301    let mut capabilities = serde_json::Map::new();
302
303    if has_tools {
304        capabilities.insert(
305            "tools".to_string(),
306            serde_json::json!({ "listChanged": true }),
307        );
308    }
309
310    if has_resources {
311        capabilities.insert(
312            "resources".to_string(),
313            serde_json::json!({ "listChanged": true }),
314        );
315    }
316
317    if has_prompts {
318        capabilities.insert(
319            "prompts".to_string(),
320            serde_json::json!({ "listChanged": true }),
321        );
322    }
323
324    // NOTE: Per MCP 2025-11-25, `elicitation` and `sampling` are CLIENT
325    // capabilities, not server capabilities. Servers do NOT advertise them.
326    //
327    // Task capabilities are only advertised if the handler actually supports
328    // them (default trait impl returns CapabilityNotSupported). The tasks
329    // capability follows the spec structure:
330    //   { list?: object, cancel?: object, requests?: { tools?: { call?: object } } }
331    //
332    // Task capability advertising is unconditional for now. Handlers that don't
333    // support tasks return CapabilityNotSupported at runtime. Conditional
334    // advertisement via handler opt-in is tracked for a future release.
335
336    // Build server info
337    let mut server_info = serde_json::Map::new();
338    server_info.insert("name".to_string(), serde_json::json!(info.name));
339    server_info.insert("version".to_string(), serde_json::json!(info.version));
340
341    // Build final result
342    let mut result = serde_json::Map::new();
343    result.insert(
344        "protocolVersion".to_string(),
345        serde_json::json!(protocol_version),
346    );
347    result.insert("capabilities".to_string(), Value::Object(capabilities));
348    result.insert("serverInfo".to_string(), Value::Object(server_info));
349
350    Value::Object(result)
351}
352
353/// Parse a JSON string into a JSON-RPC incoming request.
354///
355/// This is a convenience function for parsing incoming messages.
356pub fn parse_request(input: &str) -> Result<JsonRpcIncoming, McpError> {
357    JsonRpcIncoming::parse(input).map_err(|e| McpError::parse_error(e.to_string()))
358}
359
360/// Serialize a JSON-RPC outgoing response to a string.
361///
362/// This is a convenience function for serializing outgoing messages.
363pub fn serialize_response(response: &JsonRpcOutgoing) -> Result<alloc::string::String, McpError> {
364    response
365        .to_json()
366        .map_err(|e| McpError::internal(e.to_string()))
367}
368
369#[cfg(test)]
370mod tests {
371    use super::*;
372    use crate::error::McpResult;
373    use crate::marker::MaybeSend;
374    use core::future::Future;
375    use turbomcp_types::{Prompt, PromptResult, Resource, ResourceResult, Tool, ToolResult};
376
377    #[derive(Clone)]
378    struct TestHandler;
379
380    impl McpHandler for TestHandler {
381        fn server_info(&self) -> ServerInfo {
382            ServerInfo::new("test-router", "1.0.0")
383        }
384
385        fn list_tools(&self) -> Vec<Tool> {
386            vec![Tool::new("greet", "Say hello")]
387        }
388
389        fn list_resources(&self) -> Vec<Resource> {
390            vec![]
391        }
392
393        fn list_prompts(&self) -> Vec<Prompt> {
394            vec![]
395        }
396
397        fn call_tool<'a>(
398            &'a self,
399            name: &'a str,
400            args: Value,
401            _ctx: &'a RequestContext,
402        ) -> impl Future<Output = McpResult<ToolResult>> + MaybeSend + 'a {
403            let name = name.to_string();
404            async move {
405                match name.as_str() {
406                    "greet" => {
407                        let who = args.get("name").and_then(|v| v.as_str()).unwrap_or("World");
408                        Ok(ToolResult::text(alloc::format!("Hello, {}!", who)))
409                    }
410                    _ => Err(McpError::tool_not_found(&name)),
411                }
412            }
413        }
414
415        fn read_resource<'a>(
416            &'a self,
417            uri: &'a str,
418            _ctx: &'a RequestContext,
419        ) -> impl Future<Output = McpResult<ResourceResult>> + MaybeSend + 'a {
420            let uri = uri.to_string();
421            async move { Err(McpError::resource_not_found(&uri)) }
422        }
423
424        fn get_prompt<'a>(
425            &'a self,
426            name: &'a str,
427            _args: Option<Value>,
428            _ctx: &'a RequestContext,
429        ) -> impl Future<Output = McpResult<PromptResult>> + MaybeSend + 'a {
430            let name = name.to_string();
431            async move { Err(McpError::prompt_not_found(&name)) }
432        }
433    }
434
435    #[test]
436    fn test_parse_request() {
437        let input = r#"{"jsonrpc": "2.0", "id": 1, "method": "ping"}"#;
438        let request = parse_request(input).unwrap();
439        assert_eq!(request.method, "ping");
440        assert_eq!(request.id, Some(serde_json::json!(1)));
441    }
442
443    #[test]
444    fn test_serialize_response() {
445        let response = JsonRpcOutgoing::success(Some(serde_json::json!(1)), serde_json::json!({}));
446        let serialized = serialize_response(&response).unwrap();
447        assert!(serialized.contains("\"jsonrpc\":\"2.0\""));
448        assert!(serialized.contains("\"id\":1"));
449    }
450
451    #[tokio::test]
452    async fn test_route_initialize() {
453        let handler = TestHandler;
454        let ctx = RequestContext::stdio();
455        let config = RouteConfig::default();
456        let request = JsonRpcIncoming {
457            jsonrpc: "2.0".to_string(),
458            id: Some(serde_json::json!(1)),
459            method: "initialize".to_string(),
460            params: Some(serde_json::json!({
461                "protocolVersion": "2025-11-25",
462                "clientInfo": {
463                    "name": "test-client",
464                    "version": "1.0.0"
465                },
466                "capabilities": {}
467            })),
468        };
469
470        let response = route_request(&handler, request, &ctx, &config).await;
471        assert!(response.result.is_some());
472        assert!(response.error.is_none());
473
474        let result = response.result.unwrap();
475        assert_eq!(result["serverInfo"]["name"], "test-router");
476        assert!(result["capabilities"]["tools"].is_object());
477        assert_eq!(result["capabilities"]["tools"]["listChanged"], true);
478    }
479
480    #[tokio::test]
481    async fn test_route_initialize_missing_client_info() {
482        let handler = TestHandler;
483        let ctx = RequestContext::stdio();
484        let config = RouteConfig::default();
485        let request = JsonRpcIncoming {
486            jsonrpc: "2.0".to_string(),
487            id: Some(serde_json::json!(1)),
488            method: "initialize".to_string(),
489            params: Some(serde_json::json!({
490                "protocolVersion": "2025-11-25"
491            })),
492        };
493
494        let response = route_request(&handler, request, &ctx, &config).await;
495        assert!(response.error.is_some());
496        let error = response.error.unwrap();
497        assert_eq!(error.code, -32602); // INVALID_PARAMS
498    }
499
500    #[tokio::test]
501    async fn test_route_tools_list() {
502        let handler = TestHandler;
503        let ctx = RequestContext::stdio();
504        let config = RouteConfig::default();
505        let request = JsonRpcIncoming {
506            jsonrpc: "2.0".to_string(),
507            id: Some(serde_json::json!(1)),
508            method: "tools/list".to_string(),
509            params: None,
510        };
511
512        let response = route_request(&handler, request, &ctx, &config).await;
513        assert!(response.result.is_some());
514
515        let result = response.result.unwrap();
516        let tools = result["tools"].as_array().unwrap();
517        assert_eq!(tools.len(), 1);
518        assert_eq!(tools[0]["name"], "greet");
519    }
520
521    #[tokio::test]
522    async fn test_route_tools_call() {
523        let handler = TestHandler;
524        let ctx = RequestContext::stdio();
525        let config = RouteConfig::default();
526        let request = JsonRpcIncoming {
527            jsonrpc: "2.0".to_string(),
528            id: Some(serde_json::json!(1)),
529            method: "tools/call".to_string(),
530            params: Some(serde_json::json!({
531                "name": "greet",
532                "arguments": {"name": "Alice"}
533            })),
534        };
535
536        let response = route_request(&handler, request, &ctx, &config).await;
537        assert!(response.result.is_some());
538        assert!(response.error.is_none());
539    }
540
541    #[tokio::test]
542    async fn test_route_ping() {
543        let handler = TestHandler;
544        let ctx = RequestContext::stdio();
545        let config = RouteConfig::default();
546        let request = JsonRpcIncoming {
547            jsonrpc: "2.0".to_string(),
548            id: Some(serde_json::json!(1)),
549            method: "ping".to_string(),
550            params: None,
551        };
552
553        let response = route_request(&handler, request, &ctx, &config).await;
554        assert!(response.result.is_some());
555        assert!(response.error.is_none());
556    }
557
558    #[tokio::test]
559    async fn test_route_notification() {
560        let handler = TestHandler;
561        let ctx = RequestContext::stdio();
562        let config = RouteConfig::default();
563        let request = JsonRpcIncoming {
564            jsonrpc: "2.0".to_string(),
565            id: None,
566            method: "notifications/initialized".to_string(),
567            params: None,
568        };
569
570        let response = route_request(&handler, request, &ctx, &config).await;
571        assert!(!response.should_send());
572    }
573
574    #[tokio::test]
575    async fn test_route_unknown_method() {
576        let handler = TestHandler;
577        let ctx = RequestContext::stdio();
578        let config = RouteConfig::default();
579        let request = JsonRpcIncoming {
580            jsonrpc: "2.0".to_string(),
581            id: Some(serde_json::json!(1)),
582            method: "unknown/method".to_string(),
583            params: None,
584        };
585
586        let response = route_request(&handler, request, &ctx, &config).await;
587        assert!(response.error.is_some());
588        let error = response.error.unwrap();
589        assert_eq!(error.code, -32601); // METHOD_NOT_FOUND
590    }
591
592    #[tokio::test]
593    async fn test_route_with_custom_protocol_version() {
594        let handler = TestHandler;
595        let ctx = RequestContext::stdio();
596        let config = RouteConfig {
597            protocol_version: Some("2025-11-25"),
598        };
599        let request = JsonRpcIncoming {
600            jsonrpc: "2.0".to_string(),
601            id: Some(serde_json::json!(1)),
602            method: "initialize".to_string(),
603            params: Some(serde_json::json!({
604                "protocolVersion": "2025-11-25",
605                "clientInfo": {
606                    "name": "test-client",
607                    "version": "1.0.0"
608                }
609            })),
610        };
611
612        let response = route_request(&handler, request, &ctx, &config).await;
613        let result = response.result.unwrap();
614        assert_eq!(result["protocolVersion"], "2025-11-25");
615    }
616}