diff --git a/crates/rmcp/src/transport/streamable_http_server/tower.rs b/crates/rmcp/src/transport/streamable_http_server/tower.rs index 1b5382015..29b3c0841 100644 --- a/crates/rmcp/src/transport/streamable_http_server/tower.rs +++ b/crates/rmcp/src/transport/streamable_http_server/tower.rs @@ -39,8 +39,8 @@ use crate::{ OneshotTransport, TransportAdapterIdentity, common::{ http_header::{ - EVENT_STREAM_MIME_TYPE, HEADER_LAST_EVENT_ID, HEADER_MCP_PROTOCOL_VERSION, - HEADER_SESSION_ID, JSON_MIME_TYPE, + EVENT_STREAM_MIME_TYPE, HEADER_LAST_EVENT_ID, HEADER_MCP_METHOD, + HEADER_MCP_PROTOCOL_VERSION, HEADER_SESSION_ID, JSON_MIME_TYPE, }, mcp_headers, server_side_http::{ @@ -690,6 +690,69 @@ mod jsonrpc_http_status_tests { } } +#[cfg(test)] +mod standard_header_init_tests { + use super::*; + + fn initialize_message() -> ClientJsonRpcMessage { + ClientJsonRpcMessage::request( + ClientRequest::InitializeRequest(InitializeRequest { + params: InitializeRequestParams { + protocol_version: ProtocolVersion::STANDARD_HEADERS, + ..Default::default() + }, + ..Default::default() + }), + RequestId::Number(1), + ) + } + + fn headers_with(mcp_method: Option<&str>) -> HeaderMap { + let mut headers = HeaderMap::new(); + headers.insert( + HEADER_MCP_PROTOCOL_VERSION, + http::HeaderValue::from_static(ProtocolVersion::STANDARD_HEADERS.as_str()), + ); + if let Some(method) = mcp_method { + headers.insert( + HEADER_MCP_METHOD, + method.parse::().unwrap(), + ); + } + headers + } + + fn no_tool_schema(_: &str) -> Option> { + None + } + + /// A supplied Mcp-Method header contradicting an initialize body must be + /// rejected at >= STANDARD_HEADERS. Regression test for + /// https://github.com/modelcontextprotocol/rust-sdk/issues/1271 + #[test] + fn initialize_rejects_contradicting_mcp_method_header() { + let headers = headers_with(Some("tools/list")); + assert!( + validate_standard_headers(&headers, &initialize_message(), no_tool_schema).is_err() + ); + } + + /// Absence of the Mcp-Method header on initialize stays accepted: clients + /// emit SEP-2243 headers only after the version has been negotiated. + #[test] + fn initialize_accepts_missing_mcp_method_header() { + let headers = headers_with(None); + assert!(validate_standard_headers(&headers, &initialize_message(), no_tool_schema).is_ok()); + } + + /// A supplied Mcp-Method header matching the initialize body is accepted. + #[test] + fn initialize_accepts_matching_mcp_method_header() { + let headers = headers_with(Some("initialize")); + assert!(validate_standard_headers(&headers, &initialize_message(), no_tool_schema).is_ok()); + } +} + fn jsonrpc_message_response( message: ServerJsonRpcMessage, map_protocol_status: bool, @@ -724,8 +787,11 @@ fn header_mismatch_jsonrpc_response( /// Validates SEP-2243 `Mcp-Method` / `Mcp-Name` / `Mcp-Param-*` headers against the body. /// /// Only enforced when the request declares a protocol version `>= STANDARD_HEADERS`. -/// The `initialize` handshake is exempt: clients emit these headers only after the -/// version has been negotiated. `tool_schema` supplies the called tool's input schema +/// The `initialize` handshake is exempt from *requiring* them: clients emit these +/// headers only after the version has been negotiated. But like +/// `validate_header_matches_init_body`, a *supplied* `Mcp-Method` header that +/// contradicts the body is rejected — middleboxes route on the header without +/// parsing the body. `tool_schema` supplies the called tool's input schema /// so annotated `Mcp-Param-*` headers can be checked (no schema => those are skipped). fn validate_standard_headers( headers: &HeaderMap, @@ -743,6 +809,22 @@ fn validate_standard_headers( let request_id = match message { ClientJsonRpcMessage::Request(req) => { if matches!(&req.request, ClientRequest::InitializeRequest(_)) { + // The handshake may omit SEP-2243 headers, but a supplied + // Mcp-Method header must still agree with the body: middleboxes + // route on the header without parsing the body. + if let Some(value) = headers + .get(HEADER_MCP_METHOD) + .and_then(|value| value.to_str().ok()) + && value != "initialize" + { + return Err(header_mismatch_jsonrpc_response( + Some(req.id.clone()), + format!( + "Mcp-Method header `{value}` does not match body method `initialize`" + ), + ) + .into()); + } return Ok(()); } Some(req.id.clone())