#![cfg(not(feature = "local"))] //! Regression tests for the `MCP-Protocol-Version` header / initialize body consistency check. use std::sync::Arc; use rmcp::transport::streamable_http_server::{ StreamableHttpServerConfig, StreamableHttpService, session::local::LocalSessionManager, }; use tokio_util::sync::CancellationToken; mod common; use common::calculator::Calculator; fn init_body(body_version: &str) -> String { format!( r#"{{"jsonrpc":"2.0","id":1,"method":"initialize","params":{{"protocolVersion":"{body_version}","capabilities":{{}},"clientInfo":{{"name":"test","version":"1.0"}}}}}}"# ) } async fn spawn_server( config: StreamableHttpServerConfig, ) -> (reqwest::Client, String, CancellationToken) { spawn_server_with_manager(config, Arc::new(LocalSessionManager::default())).await } async fn spawn_server_with_manager( config: StreamableHttpServerConfig, session_manager: Arc, ) -> (reqwest::Client, String, CancellationToken) { let ct = config.cancellation_token.clone(); let service: StreamableHttpService = StreamableHttpService::new(|| Ok(Calculator::new()), session_manager, config); let router = axum::Router::new().nest_service("/mcp", service); let tcp_listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); let addr = tcp_listener.local_addr().unwrap(); tokio::spawn({ let ct = ct.clone(); async move { let _ = axum::serve(tcp_listener, router) .with_graceful_shutdown(async move { ct.cancelled_owned().await }) .await; } }); let client = reqwest::Client::new(); let base_url = format!("http://{addr}/mcp"); (client, base_url, ct) } fn stateless_json_config() -> StreamableHttpServerConfig { StreamableHttpServerConfig::default() .with_legacy_session_mode(false) .with_json_response(true) .with_sse_keep_alive(None) .with_cancellation_token(CancellationToken::new()) } fn stateful_config() -> StreamableHttpServerConfig { StreamableHttpServerConfig::default() .with_legacy_session_mode(true) .with_sse_keep_alive(None) .with_cancellation_token(CancellationToken::new()) } async fn post_init( client: &reqwest::Client, url: &str, header: Option<&str>, body_version: &str, ) -> reqwest::Response { let mut req = client .post(url) .header("Content-Type", "application/json") .header("Accept", "application/json, text/event-stream") .body(init_body(body_version)); if let Some(h) = header { req = req.header("MCP-Protocol-Version", h); } req.send().await.expect("send initialize request") } async fn post_non_initialize(client: &reqwest::Client, url: &str) -> reqwest::Response { client .post(url) .header("Content-Type", "application/json") .header("Accept", "application/json, text/event-stream") .body(r#"{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}"#) .send() .await .expect("send non-initialize request") } #[tokio::test] async fn stateless_init_rejects_when_header_older_than_body() -> anyhow::Result<()> { let (client, url, ct) = spawn_server(stateless_json_config()).await; let response = post_init(&client, &url, Some("2025-03-26"), "2025-11-25").await; assert_eq!(response.status(), 400); let body: serde_json::Value = response.json().await?; assert_eq!(body["error"]["code"], -32600); assert!( body["error"]["message"] .as_str() .unwrap_or_default() .contains("MCP-Protocol-Version"), "expected error message to mention the header, got: {body}" ); ct.cancel(); Ok(()) } #[tokio::test] async fn stateless_init_rejects_when_header_newer_than_body() -> anyhow::Result<()> { let (client, url, ct) = spawn_server(stateless_json_config()).await; let response = post_init(&client, &url, Some("2025-11-25"), "2025-03-26").await; assert_eq!(response.status(), 400); let body: serde_json::Value = response.json().await?; assert_eq!(body["error"]["code"], -32600); ct.cancel(); Ok(()) } #[tokio::test] async fn stateless_init_accepts_when_header_matches_body() -> anyhow::Result<()> { let (client, url, ct) = spawn_server(stateless_json_config()).await; let response = post_init(&client, &url, Some("2025-11-25"), "2025-11-25").await; assert_eq!(response.status(), 200); let body: serde_json::Value = response.json().await?; assert!( body["result"].is_object(), "expected an InitializeResult, got: {body}" ); ct.cancel(); Ok(()) } #[tokio::test] async fn stateless_init_accepts_when_header_absent() -> anyhow::Result<()> { let (client, url, ct) = spawn_server(stateless_json_config()).await; let response = post_init(&client, &url, None, "2025-11-25").await; assert_eq!(response.status(), 200); ct.cancel(); Ok(()) } #[tokio::test] async fn stateful_init_rejects_when_header_mismatches_body() -> anyhow::Result<()> { let (client, url, ct) = spawn_server(stateful_config()).await; let response = post_init(&client, &url, Some("2024-11-05"), "2025-11-25").await; assert_eq!(response.status(), 400); let body: serde_json::Value = response.json().await?; assert_eq!(body["error"]["code"], -32600); ct.cancel(); Ok(()) } #[tokio::test] async fn stateful_rejected_initial_posts_do_not_create_sessions() -> anyhow::Result<()> { let session_manager = Arc::new(LocalSessionManager::default()); let (client, url, ct) = spawn_server_with_manager(stateful_config(), session_manager.clone()).await; let response = post_non_initialize(&client, &url).await; assert_eq!(response.status(), 422); assert_eq!(session_manager.sessions.read().await.len(), 0); let response = post_init(&client, &url, Some("2024-11-05"), "2025-11-25").await; assert_eq!(response.status(), 400); assert_eq!(session_manager.sessions.read().await.len(), 0); ct.cancel(); Ok(()) } #[tokio::test] async fn stateless_missing_protocol_header_returns_header_mismatch() -> anyhow::Result<()> { let (client, url, ct) = spawn_server(stateless_json_config()).await; let body = r#"{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientInfo":{"name":"test","version":"1.0"},"io.modelcontextprotocol/clientCapabilities":{}}}}"#; let response = client .post(&url) .header("Content-Type", "application/json") .header("Accept", "application/json, text/event-stream") .header("Mcp-Method", "tools/list") .body(body) .send() .await .expect("send non-initialize request"); assert_eq!(response.status(), 400); let body: serde_json::Value = response.json().await?; assert_eq!(body["jsonrpc"], "2.0"); assert_eq!(body["id"], 1); assert_eq!(body["error"]["code"], -32020); assert!( body["error"]["message"] .as_str() .is_some_and(|message| message.contains("requires MCP-Protocol-Version header")), "expected missing protocol header message, got: {body}" ); ct.cancel(); Ok(()) }