#![cfg(all( not(feature = "local"), feature = "reqwest", feature = "transport-streamable-http-server" ))] use std::borrow::Cow; use rmcp::{ ServerHandler, model::{Implementation, ProtocolVersion, ServerCapabilities, ServerConfig}, transport::streamable_http_server::{ StreamableHttpServerConfig, StreamableHttpService, session::local::LocalSessionManager, }, }; use serde_json::json; use tokio_util::sync::CancellationToken; #[derive(Clone, Default)] struct DiscoveryServer; impl ServerHandler for DiscoveryServer { fn get_info(&self) -> ServerConfig { ServerConfig::new(ServerCapabilities::builder().enable_tools().build()) .with_server_info(Implementation::new("discovery-server", "1.0.0")) .with_instructions("Use the tools carefully") } fn supported_protocol_versions(&self) -> Cow<'static, [ProtocolVersion]> { Cow::Borrowed(&[ProtocolVersion::V_2025_11_25]) } } async fn spawn_server(json_response: bool) -> (reqwest::Client, String, CancellationToken) { spawn_server_with_legacy_session_mode(json_response, false).await } async fn spawn_server_with_legacy_session_mode( json_response: bool, legacy_session_mode: bool, ) -> (reqwest::Client, String, CancellationToken) { let cancellation_token = CancellationToken::new(); let config = StreamableHttpServerConfig::default() .with_legacy_session_mode(legacy_session_mode) .with_json_response(json_response) .with_sse_keep_alive(None) .with_cancellation_token(cancellation_token.clone()); let service: StreamableHttpService = StreamableHttpService::new(|| Ok(DiscoveryServer), Default::default(), config); let router = axum::Router::new().nest_service("/mcp", service); let listener = tokio::net::TcpListener::bind("127.0.0.1:0") .await .expect("listener should bind"); let address = listener.local_addr().expect("listener should have address"); tokio::spawn({ let cancellation_token = cancellation_token.clone(); async move { let _ = axum::serve(listener, router) .with_graceful_shutdown(async move { cancellation_token.cancelled_owned().await; }) .await; } }); ( reqwest::Client::new(), format!("http://{address}/mcp"), cancellation_token, ) } fn discover_body(version: Option<&str>) -> serde_json::Value { let meta = version.map(|version| { json!({ "io.modelcontextprotocol/protocolVersion": version, "io.modelcontextprotocol/clientInfo": { "name": "test-client", "version": "1.0.0" }, "io.modelcontextprotocol/clientCapabilities": {} }) }); json!({ "jsonrpc": "2.0", "id": 1, "method": "server/discover", "params": meta.map(|meta| json!({ "_meta": meta })).unwrap_or_else(|| json!({})) }) } async fn post_discover( client: &reqwest::Client, url: &str, header_version: &str, body_version: Option<&str>, ) -> reqwest::Response { client .post(url) .header("Content-Type", "application/json") .header("Accept", "application/json, text/event-stream") .header("MCP-Protocol-Version", header_version) .header("Mcp-Method", "server/discover") .json(&discover_body(body_version)) .send() .await .expect("discover request should send") } #[tokio::test] async fn discover_returns_server_metadata_without_session() { let (client, url, cancellation_token) = spawn_server(true).await; let response = post_discover(&client, &url, "2025-11-25", Some("2025-11-25")).await; assert_eq!(response.status(), 200); let body: serde_json::Value = response.json().await.expect("response should be JSON"); assert_eq!( body["result"], json!({ "resultType": "complete", "supportedVersions": ["2025-11-25"], "capabilities": { "tools": {} }, "_meta": { "io.modelcontextprotocol/serverInfo": { "name": "discovery-server", "version": "1.0.0" } }, "instructions": "Use the tools carefully", "ttlMs": 0, "cacheScope": "private" }) ); cancellation_token.cancel(); } #[tokio::test] async fn discover_does_not_require_initialization_in_legacy_session_mode() { let (client, url, cancellation_token) = spawn_server_with_legacy_session_mode(true, true).await; let response = post_discover(&client, &url, "2025-11-25", Some("2025-11-25")).await; assert_eq!(response.status(), 200); assert!(response.headers().get("Mcp-Session-Id").is_none()); cancellation_token.cancel(); } #[tokio::test] async fn discover_rejects_unsupported_version_with_http_400() { let (client, url, cancellation_token) = spawn_server(true).await; let response = post_discover(&client, &url, "2026-07-28", Some("2026-07-28")).await; assert_eq!(response.status(), 400); let body: serde_json::Value = response.json().await.expect("response should be JSON"); assert_eq!(body["error"]["code"], -32022); assert_eq!( body["error"]["data"], json!({ "requested": "2026-07-28", "supported": ["2025-11-25"] }) ); cancellation_token.cancel(); } #[tokio::test] async fn discover_rejects_unknown_version_with_typed_error() { let (client, url, cancellation_token) = spawn_server(true).await; let response = post_discover(&client, &url, "2099-01-01", Some("2099-01-01")).await; assert_eq!(response.status(), 400); let body: serde_json::Value = response.json().await.expect("response should be JSON"); assert_eq!(body["error"]["code"], -32022); cancellation_token.cancel(); } #[tokio::test] async fn regular_request_rejects_server_unsupported_meta_version() { let (client, url, cancellation_token) = spawn_server(true).await; let body = json!({ "jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": { "_meta": { "io.modelcontextprotocol/protocolVersion": "2026-07-28" } } }); let response = client .post(&url) .header("Content-Type", "application/json") .header("Accept", "application/json, text/event-stream") .header("MCP-Protocol-Version", "2026-07-28") .header("Mcp-Method", "tools/list") .json(&body) .send() .await .expect("request should send"); assert_eq!(response.status(), 400); let body: serde_json::Value = response.json().await.expect("response should be JSON"); assert_eq!(body["error"]["code"], -32022); cancellation_token.cancel(); } #[tokio::test] async fn unknown_rpc_uses_http_404_for_per_request_protocol() { let (client, url, cancellation_token) = spawn_server(true).await; let body = json!({ "jsonrpc": "2.0", "id": 3, "method": "unknown/method", "params": { "_meta": { "io.modelcontextprotocol/protocolVersion": "2025-11-25" } } }); let response = client .post(&url) .header("Content-Type", "application/json") .header("Accept", "application/json, text/event-stream") .header("MCP-Protocol-Version", "2025-11-25") .json(&body) .send() .await .expect("request should send"); assert_eq!(response.status(), 404); cancellation_token.cancel(); } #[tokio::test] async fn legacy_unknown_rpc_preserves_http_200_jsonrpc_error() { let (client, url, cancellation_token) = spawn_server(true).await; let body = json!({ "jsonrpc": "2.0", "id": 5, "method": "unknown/method", "params": {} }); let response = client .post(&url) .header("Content-Type", "application/json") .header("Accept", "application/json, text/event-stream") .header("MCP-Protocol-Version", "2025-11-25") .json(&body) .send() .await .expect("request should send"); assert_eq!(response.status(), 200); let body: serde_json::Value = response.json().await.expect("response should be JSON"); assert_eq!(body["error"]["code"], -32601); cancellation_token.cancel(); } #[tokio::test] async fn discover_rejects_header_meta_version_mismatch() { let (client, url, cancellation_token) = spawn_server(true).await; let response = post_discover(&client, &url, "2026-07-28", Some("2025-11-25")).await; assert_eq!(response.status(), 400); cancellation_token.cancel(); } #[tokio::test] async fn discover_rejects_missing_request_meta() { let (client, url, cancellation_token) = spawn_server(true).await; let response = post_discover(&client, &url, "2025-11-25", None).await; assert_eq!(response.status(), 400); let body: serde_json::Value = response.json().await.expect("response should be JSON"); assert_eq!(body["error"]["code"], -32602); cancellation_token.cancel(); } #[tokio::test] async fn discover_rejects_missing_client_capabilities() { let (client, url, cancellation_token) = spawn_server(true).await; let body = json!({ "jsonrpc": "2.0", "id": 4, "method": "server/discover", "params": { "_meta": { "io.modelcontextprotocol/protocolVersion": "2025-11-25", "io.modelcontextprotocol/clientInfo": { "name": "test-client", "version": "1.0.0" } } } }); let response = client .post(&url) .header("Content-Type", "application/json") .header("Accept", "application/json, text/event-stream") .header("MCP-Protocol-Version", "2025-11-25") .json(&body) .send() .await .expect("request should send"); assert_eq!(response.status(), 400); let body: serde_json::Value = response.json().await.expect("response should be JSON"); assert_eq!(body["error"]["code"], -32602); cancellation_token.cancel(); } #[tokio::test] async fn discover_accepts_missing_optional_client_info() { let (client, url, cancellation_token) = spawn_server(true).await; let body = json!({ "jsonrpc": "2.0", "id": 5, "method": "server/discover", "params": { "_meta": { "io.modelcontextprotocol/protocolVersion": "2025-11-25", "io.modelcontextprotocol/clientCapabilities": {} } } }); let response = client .post(&url) .header("Content-Type", "application/json") .header("Accept", "application/json, text/event-stream") .header("MCP-Protocol-Version", "2025-11-25") .json(&body) .send() .await .expect("request should send"); assert_eq!(response.status(), 200); let body: serde_json::Value = response.json().await.expect("response should be JSON"); assert!(body.get("result").is_some()); cancellation_token.cancel(); } #[tokio::test] async fn discover_error_uses_http_400_when_sse_is_configured() { let (client, url, cancellation_token) = spawn_server(false).await; let response = post_discover(&client, &url, "2026-07-28", Some("2026-07-28")).await; assert_eq!(response.status(), 400); assert_eq!( response .headers() .get("Content-Type") .and_then(|value| value.to_str().ok()), Some("application/json") ); cancellation_token.cancel(); }