Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 86 additions & 4 deletions crates/rmcp/src/transport/streamable_http_server/tower.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -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::<http::HeaderValue>().unwrap(),
);
}
headers
}

fn no_tool_schema(_: &str) -> Option<Arc<JsonObject>> {
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,
Expand Down Expand Up @@ -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,
Expand All @@ -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())
Expand Down