feat: validate SEP-2243 Mcp-Param headers in modern server by prathamgoyal787 · Pull Request #1202 · modelcontextprotocol/java-sdk · GitHub
Skip to content

feat: validate SEP-2243 Mcp-Param headers in modern server - #1202

Open
prathamgoyal787 wants to merge 2 commits into
modelcontextprotocol:mainfrom
prathamgoyal787:feat/sep-2243-mcp-param-validation
Open

prathamgoyal787 wants to merge 2 commits into
modelcontextprotocol:mainfrom
prathamgoyal787:feat/sep-2243-mcp-param-validation

Conversation

@prathamgoyal787

@prathamgoyal787 prathamgoyal787 commented Oct 20, 2026 •

Copy link
Copy Markdown

Validates SEP-2243 Mcp-Param-{Name} headers in the modern server: for every tool parameter marked with x-mcp-header, tools/call now checks the header the client sent against the argument in the body.

Closes #1199

Motivation and Context

SEP-2243 (part of the 2026-07-28 revision) lets a tool mark parameters with x-mcp-header so clients copy their values into Mcp-Param-{Name} HTTP headers. Load balancers and gateways can then route on them without parsing the JSON body. The spec makes it a MUST for servers to reject a request whose headers disagree with the body, are missing while the body has a value, or are malformed. Otherwise an intermediary could route on a header the server never checked.

The modern server already validates Mcp-Method, Mcp-Name and MCP-Protocol-Version, but not Mcp-Param-*. This was listed as a Known Limitation in conformance-tests/VALIDATION_RESULTS.md, and http-custom-header-server-validation could not pass. The check needs the called tool's inputSchema, which the transport doesn't have. So, as proposed in #1199:

  • Transport: HttpServletMcpTransport collects the headers into a new TransportParams value.

    • Header names are case-insensitive.
    • Optional whitespace (OWS) around values is trimmed.
    • Values in the exact lowercase =?base64?…?= form are decoded strictly: padding is required and the bytes must be valid UTF-8.
    • Plain values must be printable ASCII.
    • Malformed or repeated headers are flagged rather than failing the request.
  • Context: the value is attached to the McpTransportContext under TransportParams.KEY after the user's contextExtractor runs, so the extractor can neither drop nor forge it.

  • stdio: StdioMcpTransport attaches TransportParams.NOT_MIRRORED, the only value that skips validation. A context without the key counts as "no headers sent", not "skip".

  • Validation: ToolsFeature validates after find and before inputSchema validation. Only x-mcp-header annotations reached through properties count; items, allOf and $ref are not followed.

    Case Outcome
    Argument present Header required and must match. Strings exactly, integers numerically (42.0 = 42), booleans as true/false
    Argument null or absent (incl. missing intermediate object) No header expected
    Argument present, header missing Rejected
    Header of a declared parameter malformed Rejected
    Mcp-Param-* header no parameter declares Ignored

    Rejections throw McpException with ErrorCodes.HEADER_MISMATCH (-32020), which the servlet answers with 400.

  • Invalid definitions: invalid server-side x-mcp-header definitions are ignored with a debug log, per the open question in Validate SEP-2243 Mcp-Param-{Name} headers in the modern server #1199. Invalid means a name that is empty or not an RFC 9110 token, a type other than string/integer/boolean, or a case-insensitive duplicate.

How Has This Been Tested?

New and extended tests:

  • TransportParamsTests covers:
    • valid Base64, and invalid Base64 (padding, alphabet, UTF-8)
    • values that only resemble the sentinel
    • OWS trimming, and characters that required Base64
    • case-insensitive names and repeated headers
    • attach overriding a value forged by the extractor
  • McpParamHeadersTests: nested properties paths; annotations behind items/allOf/$ref/$defs ignored; invalid names, types and duplicates.
  • ToolsFeatureParamHeaderTests: one test per table row, each asserting the tool and inputSchema validation never ran on rejection. Also 42.0 vs 42, booleans, Base64 and literal values, NOT_MIRRORED skipping validation, and a missing key not skipping it.
  • StdioMcpTransportTests: stdio passes NOT_MIRRORED.
  • HttpServletMcpTransportIntegrationTests, end to end over Tomcat: matching header → 200; mismatched, missing or malformed header → 400 / -32020.

Results:

  • mvn test -pl mcp-core: 635 tests, 0 failures, 0 errors.
  • mvn test -pl mcp-test: 1036 tests, 0 failures, 1 skipped. All modern-server tests pass. There were 12 errors, all Testcontainers "Could not find a valid Docker environment" in legacy client tests, because Docker wasn't available locally.
  • Conformance with @modelcontextprotocol/conformance@0.2.0-alpha.12 against conformance-tests/server-servlet-modern, with the new test_custom_header fixture:
    • --scenario http-custom-header-server-validation: 10/10 passed.
    • --requirements 2026-07-28: all 37 required scenarios pass, so no regressions. The only failures are the unscored tasks-* extension.

Breaking Changes

No public API is removed or changed. One behaviour change to be aware of: a custom transport or in-memory caller that passes an McpTransportContext without TransportParams (for example McpTransportContext.EMPTY) now gets -32020 when it calls a tool with an x-mcp-header argument value. This is deliberate: #1199 requires that a missing key must not skip validation. Such transports should attach TransportParams.NOT_MIRRORED, or the parsed headers, with TransportParams.attach(...). Tools without x-mcp-header are unaffected.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

  • A malformed header for a declared parameter is rejected even when the argument is absent, following the spec's "reject invalid characters".
  • A repeated Mcp-Param-X header counts as malformed.
  • Every case-insensitive duplicate definition is ignored, not just the later ones.
  • Numbers compare via BigDecimal, so 42, 42.0 and 4.2e1 all match.
  • Out of scope: the existing Mcp-Name decoder still accepts unpadded Base64.
  • Docs: VALIDATION_RESULTS.md moves the scenario to Passing and narrows Known Limitation Update the License #2 to "definitions aren't enforced at registration/listing". The server-servlet-modern README lists the new fixture.

HttpServletMcpTransport collects every Mcp-Param-* header into
TransportParams (case-insensitive names, =?base64?...?= decoding,
malformed values flagged rather than failing) and attaches it to the
McpTransportContext after the user's contextExtractor, so the extractor
can neither drop nor forge it. StdioMcpTransport declares NOT_MIRRORED;
a context without params counts as a request with no headers.

ToolsFeature checks, after find and before inputSchema validation, every
x-mcp-header parameter reachable through properties: a present argument
needs a matching header (integers compared numerically, booleans as
true/false), a null or absent argument expects none, malformed headers
of declared parameters are rejected and undeclared headers are ignored.
Rejections fail with -32020 (HEADER_MISMATCH), answered with 400 over
HTTP. Invalid x-mcp-header definitions are ignored with a debug log.

Adds the test_custom_header conformance fixture; the
http-custom-header-server-validation scenario now passes (10/10).

Closes modelcontextprotocol#1199

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Validate SEP-2243 Mcp-Param-{Name} headers in the modern server

1 participant