use alloc::string::ToString;
use serde_json::Value;
use crate::PROTOCOL_VERSION;
use crate::context::RequestContext;
use crate::error::McpError;
use crate::handler::McpHandler;
use crate::jsonrpc::{JsonRpcIncoming, JsonRpcOutgoing};
use turbomcp_types::ServerInfo;
#[derive(Debug, Clone, Default)]
pub struct RouteConfig<'a> {
pub protocol_version: Option<&'a str>,
}
pub async fn route_request<H: McpHandler>(
handler: &H,
request: JsonRpcIncoming,
ctx: &RequestContext,
config: &RouteConfig<'_>,
) -> JsonRpcOutgoing {
let id = request.id.clone();
match request.method.as_str() {
"initialize" => {
let params = request.params.clone().unwrap_or_default();
let Some(client_info) = params.get("clientInfo") else {
return JsonRpcOutgoing::error(
id,
McpError::invalid_params("Missing required field: clientInfo"),
);
};
let client_name = client_info.get("name").and_then(|v| v.as_str());
let client_version = client_info.get("version").and_then(|v| v.as_str());
if client_name.is_none() || client_version.is_none() {
return JsonRpcOutgoing::error(
id,
McpError::invalid_params("clientInfo must contain 'name' and 'version' fields"),
);
}
let protocol_version = config.protocol_version.unwrap_or(PROTOCOL_VERSION);
let info = handler.server_info();
let result = build_initialize_result(&info, handler, protocol_version);
JsonRpcOutgoing::success(id, result)
}
"initialized" | "notifications/initialized" => {
if id.is_some() {
JsonRpcOutgoing::success(id, serde_json::json!({}))
} else {
JsonRpcOutgoing::notification_ack()
}
}
"tools/list" => {
let tools = handler.list_tools();
let result = serde_json::json!({ "tools": tools });
JsonRpcOutgoing::success(id, result)
}
"tools/call" => {
let params = request.params.unwrap_or_default();
let name = params
.get("name")
.and_then(|v| v.as_str())
.unwrap_or_default();
let args = params.get("arguments").cloned().unwrap_or_default();
match handler.call_tool(name, args, ctx).await {
Ok(result) => match serde_json::to_value(&result) {
Ok(result_value) => JsonRpcOutgoing::success(id, result_value),
Err(e) => JsonRpcOutgoing::error(
id,
McpError::internal(alloc::format!(
"Failed to serialize tool result: {}",
e
)),
),
},
Err(err) => JsonRpcOutgoing::error(id, err),
}
}
"resources/list" => {
let resources = handler.list_resources();
let result = serde_json::json!({ "resources": resources });
JsonRpcOutgoing::success(id, result)
}
"resources/read" => {
let params = request.params.unwrap_or_default();
let uri = params
.get("uri")
.and_then(|v| v.as_str())
.unwrap_or_default();
match handler.read_resource(uri, ctx).await {
Ok(result) => match serde_json::to_value(&result) {
Ok(result_value) => JsonRpcOutgoing::success(id, result_value),
Err(e) => JsonRpcOutgoing::error(
id,
McpError::internal(alloc::format!(
"Failed to serialize resource result: {}",
e
)),
),
},
Err(err) => JsonRpcOutgoing::error(id, err),
}
}
"prompts/list" => {
let prompts = handler.list_prompts();
let result = serde_json::json!({ "prompts": prompts });
JsonRpcOutgoing::success(id, result)
}
"prompts/get" => {
let params = request.params.unwrap_or_default();
let name = params
.get("name")
.and_then(|v| v.as_str())
.unwrap_or_default();
let args = params.get("arguments").cloned();
match handler.get_prompt(name, args, ctx).await {
Ok(result) => match serde_json::to_value(&result) {
Ok(result_value) => JsonRpcOutgoing::success(id, result_value),
Err(e) => JsonRpcOutgoing::error(
id,
McpError::internal(alloc::format!(
"Failed to serialize prompt result: {}",
e
)),
),
},
Err(err) => JsonRpcOutgoing::error(id, err),
}
}
"tasks/list" => {
let params = request.params.unwrap_or_default();
let cursor = params.get("cursor").and_then(|v| v.as_str());
let limit = params
.get("limit")
.and_then(|v| v.as_u64())
.map(|v| v as usize);
match handler.list_tasks(cursor, limit, ctx).await {
Ok(result) => match serde_json::to_value(&result) {
Ok(v) => JsonRpcOutgoing::success(id, v),
Err(e) => JsonRpcOutgoing::error(id, McpError::internal(e.to_string())),
},
Err(err) => JsonRpcOutgoing::error(id, err),
}
}
"tasks/get" => {
let params = request.params.unwrap_or_default();
let Some(task_id) = params.get("taskId").and_then(|v| v.as_str()) else {
return JsonRpcOutgoing::error(id, McpError::invalid_params("Missing taskId"));
};
match handler.get_task(task_id, ctx).await {
Ok(result) => match serde_json::to_value(&result) {
Ok(v) => JsonRpcOutgoing::success(id, v),
Err(e) => JsonRpcOutgoing::error(id, McpError::internal(e.to_string())),
},
Err(err) => JsonRpcOutgoing::error(id, err),
}
}
"tasks/cancel" => {
let params = request.params.unwrap_or_default();
let Some(task_id) = params.get("taskId").and_then(|v| v.as_str()) else {
return JsonRpcOutgoing::error(id, McpError::invalid_params("Missing taskId"));
};
match handler.cancel_task(task_id, ctx).await {
Ok(result) => match serde_json::to_value(&result) {
Ok(v) => JsonRpcOutgoing::success(id, v),
Err(e) => JsonRpcOutgoing::error(id, McpError::internal(e.to_string())),
},
Err(err) => JsonRpcOutgoing::error(id, err),
}
}
"tasks/result" => {
let params = request.params.unwrap_or_default();
let Some(task_id) = params.get("taskId").and_then(|v| v.as_str()) else {
return JsonRpcOutgoing::error(id, McpError::invalid_params("Missing taskId"));
};
match handler.get_task_result(task_id, ctx).await {
Ok(result) => JsonRpcOutgoing::success(id, result),
Err(err) => JsonRpcOutgoing::error(id, err),
}
}
"ping" => JsonRpcOutgoing::success(id, serde_json::json!({})),
_ => JsonRpcOutgoing::error(id, McpError::method_not_found(&request.method)),
}
}
fn build_initialize_result<H: McpHandler>(
info: &ServerInfo,
handler: &H,
protocol_version: &str,
) -> Value {
let capabilities = match serde_json::to_value(handler.server_capabilities()) {
Ok(Value::Object(map)) => map,
Ok(_) | Err(_) => serde_json::Map::new(),
};
let server_info = match serde_json::to_value(info) {
Ok(Value::Object(map)) => map,
Ok(_) | Err(_) => {
let mut fallback = serde_json::Map::new();
fallback.insert("name".to_string(), serde_json::json!(info.name));
fallback.insert("version".to_string(), serde_json::json!(info.version));
fallback
}
};
let mut result = serde_json::Map::new();
result.insert(
"protocolVersion".to_string(),
serde_json::json!(protocol_version),
);
result.insert("capabilities".to_string(), Value::Object(capabilities));
result.insert("serverInfo".to_string(), Value::Object(server_info));
Value::Object(result)
}
pub fn parse_request(input: &str) -> Result<JsonRpcIncoming, McpError> {
JsonRpcIncoming::parse(input).map_err(|e| McpError::parse_error(e.to_string()))
}
pub fn serialize_response(response: &JsonRpcOutgoing) -> Result<alloc::string::String, McpError> {
response
.to_json()
.map_err(|e| McpError::internal(e.to_string()))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::McpResult;
use crate::marker::MaybeSend;
use core::future::Future;
use std::collections::HashMap;
use turbomcp_types::{
Prompt, PromptResult, Resource, ResourceResult, ServerCapabilities, ServerTaskCapabilities,
ServerTaskRequests, ServerTaskToolRequests, Tool, ToolResult,
};
#[derive(Clone)]
struct TestHandler;
impl McpHandler for TestHandler {
fn server_info(&self) -> ServerInfo {
ServerInfo::new("test-router", "1.0.0")
}
fn list_tools(&self) -> Vec<Tool> {
vec![Tool::new("greet", "Say hello")]
}
fn list_resources(&self) -> Vec<Resource> {
vec![]
}
fn list_prompts(&self) -> Vec<Prompt> {
vec![]
}
fn call_tool<'a>(
&'a self,
name: &'a str,
args: Value,
_ctx: &'a RequestContext,
) -> impl Future<Output = McpResult<ToolResult>> + MaybeSend + 'a {
let name = name.to_string();
async move {
match name.as_str() {
"greet" => {
let who = args.get("name").and_then(|v| v.as_str()).unwrap_or("World");
Ok(ToolResult::text(alloc::format!("Hello, {}!", who)))
}
_ => Err(McpError::tool_not_found(&name)),
}
}
}
fn read_resource<'a>(
&'a self,
uri: &'a str,
_ctx: &'a RequestContext,
) -> impl Future<Output = McpResult<ResourceResult>> + MaybeSend + 'a {
let uri = uri.to_string();
async move { Err(McpError::resource_not_found(&uri)) }
}
fn get_prompt<'a>(
&'a self,
name: &'a str,
_args: Option<Value>,
_ctx: &'a RequestContext,
) -> impl Future<Output = McpResult<PromptResult>> + MaybeSend + 'a {
let name = name.to_string();
async move { Err(McpError::prompt_not_found(&name)) }
}
}
#[test]
fn test_parse_request() {
let input = r#"{"jsonrpc": "2.0", "id": 1, "method": "ping"}"#;
let request = parse_request(input).unwrap();
assert_eq!(request.method, "ping");
assert_eq!(request.id, Some(serde_json::json!(1)));
}
#[test]
fn test_serialize_response() {
let response = JsonRpcOutgoing::success(Some(serde_json::json!(1)), serde_json::json!({}));
let serialized = serialize_response(&response).unwrap();
assert!(serialized.contains("\"jsonrpc\":\"2.0\""));
assert!(serialized.contains("\"id\":1"));
}
#[tokio::test]
async fn test_route_initialize() {
let handler = TestHandler;
let ctx = RequestContext::stdio();
let config = RouteConfig::default();
let request = JsonRpcIncoming {
jsonrpc: "2.0".to_string(),
id: Some(serde_json::json!(1)),
method: "initialize".to_string(),
params: Some(serde_json::json!({
"protocolVersion": "2025-11-25",
"clientInfo": {
"name": "test-client",
"version": "1.0.0"
},
"capabilities": {}
})),
};
let response = route_request(&handler, request, &ctx, &config).await;
assert!(response.result.is_some());
assert!(response.error.is_none());
let result = response.result.unwrap();
assert_eq!(result["serverInfo"]["name"], "test-router");
assert!(result["capabilities"]["tools"].is_object());
assert_eq!(result["capabilities"]["tools"]["listChanged"], true);
}
#[tokio::test]
async fn test_route_initialize_preserves_server_info_metadata() {
#[derive(Clone)]
struct MetadataHandler;
#[allow(clippy::manual_async_fn)]
impl McpHandler for MetadataHandler {
fn server_info(&self) -> ServerInfo {
ServerInfo::new("test-router", "1.0.0")
.with_title("Test Router")
.with_description("Initialize metadata should survive serialization")
.with_website_url("https://example.com")
.with_icon(
turbomcp_types::Icon::new("https://example.com/icon.png")
.with_mime_type("image/png"),
)
}
fn list_tools(&self) -> Vec<Tool> {
vec![]
}
fn list_resources(&self) -> Vec<Resource> {
vec![]
}
fn list_prompts(&self) -> Vec<Prompt> {
vec![]
}
fn call_tool<'a>(
&'a self,
_name: &'a str,
_args: Value,
_ctx: &'a RequestContext,
) -> impl Future<Output = McpResult<ToolResult>> + MaybeSend + 'a {
async move { unreachable!("tool calls are not used in this test") }
}
fn read_resource<'a>(
&'a self,
_uri: &'a str,
_ctx: &'a RequestContext,
) -> impl Future<Output = McpResult<ResourceResult>> + MaybeSend + 'a {
async move { unreachable!("resource reads are not used in this test") }
}
fn get_prompt<'a>(
&'a self,
_name: &'a str,
_args: Option<Value>,
_ctx: &'a RequestContext,
) -> impl Future<Output = McpResult<PromptResult>> + MaybeSend + 'a {
async move { unreachable!("prompt reads are not used in this test") }
}
}
let handler = MetadataHandler;
let ctx = RequestContext::stdio();
let config = RouteConfig::default();
let request = JsonRpcIncoming {
jsonrpc: "2.0".to_string(),
id: Some(serde_json::json!(1)),
method: "initialize".to_string(),
params: Some(serde_json::json!({
"protocolVersion": "2025-11-25",
"clientInfo": {
"name": "test-client",
"version": "1.0.0"
},
"capabilities": {}
})),
};
let response = route_request(&handler, request, &ctx, &config).await;
let result = response.result.expect("initialize should succeed");
assert_eq!(result["serverInfo"]["title"], "Test Router");
assert_eq!(
result["serverInfo"]["description"],
"Initialize metadata should survive serialization"
);
assert_eq!(result["serverInfo"]["websiteUrl"], "https://example.com");
assert_eq!(
result["serverInfo"]["icons"][0]["src"],
"https://example.com/icon.png"
);
}
#[tokio::test]
async fn test_route_initialize_uses_handler_capabilities() {
#[derive(Clone)]
struct CapabilityHandler;
#[allow(clippy::manual_async_fn)]
impl McpHandler for CapabilityHandler {
fn server_info(&self) -> ServerInfo {
ServerInfo::new("capability-router", "1.0.0")
}
fn server_capabilities(&self) -> ServerCapabilities {
ServerCapabilities {
tasks: Some(ServerTaskCapabilities {
list: Some(HashMap::new()),
cancel: Some(HashMap::new()),
requests: Some(ServerTaskRequests {
tools: Some(ServerTaskToolRequests {
call: Some(HashMap::new()),
}),
}),
}),
extensions: Some(HashMap::from([(
"trace".to_string(),
serde_json::json!({"version": "1"}),
)])),
..Default::default()
}
}
fn list_tools(&self) -> Vec<Tool> {
vec![]
}
fn list_resources(&self) -> Vec<Resource> {
vec![]
}
fn list_prompts(&self) -> Vec<Prompt> {
vec![]
}
fn call_tool<'a>(
&'a self,
_name: &'a str,
_args: Value,
_ctx: &'a RequestContext,
) -> impl Future<Output = McpResult<ToolResult>> + MaybeSend + 'a {
async move { unreachable!("tool calls are not used in this test") }
}
fn read_resource<'a>(
&'a self,
_uri: &'a str,
_ctx: &'a RequestContext,
) -> impl Future<Output = McpResult<ResourceResult>> + MaybeSend + 'a {
async move { unreachable!("resource reads are not used in this test") }
}
fn get_prompt<'a>(
&'a self,
_name: &'a str,
_args: Option<Value>,
_ctx: &'a RequestContext,
) -> impl Future<Output = McpResult<PromptResult>> + MaybeSend + 'a {
async move { unreachable!("prompt reads are not used in this test") }
}
}
let handler = CapabilityHandler;
let ctx = RequestContext::stdio();
let request = JsonRpcIncoming {
jsonrpc: "2.0".to_string(),
id: Some(serde_json::json!(1)),
method: "initialize".to_string(),
params: Some(serde_json::json!({
"protocolVersion": "DRAFT-2026-v1",
"clientInfo": {
"name": "test-client",
"version": "1.0.0"
},
"capabilities": {}
})),
};
let response = route_request(&handler, request, &ctx, &RouteConfig::default()).await;
let result = response.result.expect("initialize should succeed");
assert!(result["capabilities"]["tasks"]["requests"]["tools"]["call"].is_object());
assert_eq!(
result["capabilities"]["extensions"]["trace"]["version"],
"1"
);
}
#[tokio::test]
async fn test_route_initialize_missing_client_info() {
let handler = TestHandler;
let ctx = RequestContext::stdio();
let config = RouteConfig::default();
let request = JsonRpcIncoming {
jsonrpc: "2.0".to_string(),
id: Some(serde_json::json!(1)),
method: "initialize".to_string(),
params: Some(serde_json::json!({
"protocolVersion": "2025-11-25"
})),
};
let response = route_request(&handler, request, &ctx, &config).await;
assert!(response.error.is_some());
let error = response.error.unwrap();
assert_eq!(error.code, -32602); }
#[tokio::test]
async fn test_route_tools_list() {
let handler = TestHandler;
let ctx = RequestContext::stdio();
let config = RouteConfig::default();
let request = JsonRpcIncoming {
jsonrpc: "2.0".to_string(),
id: Some(serde_json::json!(1)),
method: "tools/list".to_string(),
params: None,
};
let response = route_request(&handler, request, &ctx, &config).await;
assert!(response.result.is_some());
let result = response.result.unwrap();
let tools = result["tools"].as_array().unwrap();
assert_eq!(tools.len(), 1);
assert_eq!(tools[0]["name"], "greet");
}
#[tokio::test]
async fn test_route_tools_call() {
let handler = TestHandler;
let ctx = RequestContext::stdio();
let config = RouteConfig::default();
let request = JsonRpcIncoming {
jsonrpc: "2.0".to_string(),
id: Some(serde_json::json!(1)),
method: "tools/call".to_string(),
params: Some(serde_json::json!({
"name": "greet",
"arguments": {"name": "Alice"}
})),
};
let response = route_request(&handler, request, &ctx, &config).await;
assert!(response.result.is_some());
assert!(response.error.is_none());
}
#[tokio::test]
async fn test_route_ping() {
let handler = TestHandler;
let ctx = RequestContext::stdio();
let config = RouteConfig::default();
let request = JsonRpcIncoming {
jsonrpc: "2.0".to_string(),
id: Some(serde_json::json!(1)),
method: "ping".to_string(),
params: None,
};
let response = route_request(&handler, request, &ctx, &config).await;
assert!(response.result.is_some());
assert!(response.error.is_none());
}
#[tokio::test]
async fn test_route_notification() {
let handler = TestHandler;
let ctx = RequestContext::stdio();
let config = RouteConfig::default();
let request = JsonRpcIncoming {
jsonrpc: "2.0".to_string(),
id: None,
method: "notifications/initialized".to_string(),
params: None,
};
let response = route_request(&handler, request, &ctx, &config).await;
assert!(!response.should_send());
}
#[tokio::test]
async fn test_route_unknown_method() {
let handler = TestHandler;
let ctx = RequestContext::stdio();
let config = RouteConfig::default();
let request = JsonRpcIncoming {
jsonrpc: "2.0".to_string(),
id: Some(serde_json::json!(1)),
method: "unknown/method".to_string(),
params: None,
};
let response = route_request(&handler, request, &ctx, &config).await;
assert!(response.error.is_some());
let error = response.error.unwrap();
assert_eq!(error.code, -32601); }
#[tokio::test]
async fn test_route_with_custom_protocol_version() {
let handler = TestHandler;
let ctx = RequestContext::stdio();
let config = RouteConfig {
protocol_version: Some("2025-11-25"),
};
let request = JsonRpcIncoming {
jsonrpc: "2.0".to_string(),
id: Some(serde_json::json!(1)),
method: "initialize".to_string(),
params: Some(serde_json::json!({
"protocolVersion": "2025-11-25",
"clientInfo": {
"name": "test-client",
"version": "1.0.0"
}
})),
};
let response = route_request(&handler, request, &ctx, &config).await;
let result = response.result.unwrap();
assert_eq!(result["protocolVersion"], "2025-11-25");
}
}