feat(transport): add SEP-2243 Mcp-Method / Mcp-Name header mirroring with MCP-Protocol-Version validation - #1112
Open
slachiewicz wants to merge 4 commits into
Open
Conversation
…with MCP-Protocol-Version validation Implement SEP-2243 HTTP header standardization across client and server servlet transports. * Client: Emit 'Mcp-Method' on outbound Streamable HTTP requests and notifications, and 'Mcp-Name' when targeting named tools, prompts, or resources. * Server: Validate 'Mcp-Method' and 'Mcp-Name' headers against deserialized JSON-RPC payloads in HttpServletStreamableServerTransportProvider and HttpServletStatelessServerTransport. Reject mismatches with HTTP 400 while tolerating absent headers for backward compatibility. * Versioning: Validate 'MCP-Protocol-Version' against supported protocol versions on incoming servlet requests. * Tests: Add Sep2243ClientRequestHeaderTests and Sep2243ServerHeaderValidationTests verifying emission, mismatch rejections, and absent-header tolerance.
…checks Per the Streamable HTTP spec the MCP-Protocol-Version header is required only after initialization completes; version selection for initialize happens through body-level negotiation, not header validation. * Client: stop sending MCP-Protocol-Version on initialize requests * Servlet servers: skip strict header validation for initialize so clients advertising an unsupported version negotiate instead of getting 400 * Tests: pin client omission and server tolerance for initialize; make version-negotiation test contextExtractor null-safe for absent headers
Previous run failed on pre-existing flaky race in AbstractMcpClientServerIntegrationTests.testRootsNotificationWithEmptyRootsList (Stream unavailable for session); unrelated transport changes all green.
The GET /mcp stream is opened asynchronously once initialize creates the session, so asserting recorded calls immediately races under load (seen as Jackson 2 Integration Tests failing usesLatestVersion with Expected size: 3 but was: 2). Await the recorded GET before asserting header propagation.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #990. Supersedes #994, #1026, #1092 (with credit to @cooleditphoto,
@nikita-kibitkin, and @ez-lbz — the consolidation proposed in #990).
Summary
Implements SEP-2243
HTTP header standardization end-to-end:
Mcp-Methodheader on every POST andan
Mcp-Nameheader whenever the request targets a named artifact;Mcp-Method/Mcp-Nameheaders against the deserialized request body, and validate the
MCP-Protocol-Versionheader against the supported version set.Semantics (the decision this PR stands on)
SEP-2243 requires servers to reject header/body mismatches, and discussion on
#994 established that rejecting absent headers must not break legacy clients:
Mcp-MethodorMcp-Nameis rejected with400 Bad Requestand a message naming both values.not send the new headers keep working unchanged.
MCP-Protocol-Versionfalls back to negotiated behavior, while apresent-but-unsupported version is rejected.
This replaces the opt-in
rejectMissingHeaders(false)switch discussed for theconsolidation: gating absence would wrongly gate mismatches too, and splitting
the two concerns needs two flags where current semantics need none.
Implementation notes
HttpHeaders.MCP_METHODandHttpHeaders.MCP_NAME.HttpClientStreamableHttpTransport.sendMessagefor both requests and notifications. The name/URI comes from typed binding
of the params (
tools/call,prompts/getuse.name(); the resourceoperations use
.uri()), so no regex-on-body parsing is involved. Extractionfailures log at debug and omit the header rather than fail the request.
so validation compares header against the parsed body, not raw text.
Responses carry no method and are skipped.
Testing
Sep2243ClientRequestHeaderTestsasserts emitted headers through a realJDK HTTP server round-trip.
Sep2243ServerHeaderValidationTestscovers the rejection matrix (badprotocol version, method mismatch, name mismatch) and the tolerance rule
(absent headers produce only unrelated errors such as a missing session),
for both the streamable servlet provider and the stateless transport.
Conformance Tests (
@modelcontextprotocol/conformance@0.2.0-alpha.11)json-schema-2020-12schema preservationclient-jdk-http-client(initialize,tools_call,defaults,sse-retry)sse-retryfailure in baseline)client-spring-http-client(14 OAuth2 scenarios)All tests match the expected baseline 100% across all released spec versions (
2025-03-26,2025-06-18, and2025-11-25).Compatibility
Non-breaking for conformant legacy traffic: emission is additive on outbound
requests, and server behavior changes only for clients that already send these
headers with wrong values, which is what the SEP requires servers to catch.
Out of scope, deliberately
The Spring/WebClient/WebMvc stack has no separate streamable-HTTP transport in
this codebase, so there is nothing further to wire. Name extraction is
duplicated as private helpers in the two servlet classes today; pulling it
into a shared utility can follow once a third caller exists.