@@ -177,10 +177,8 @@ def decode_header_value(value: str | None) -> str | None:
177177 decoded = base64 .b64decode (payload , validate = True )
178178 except binascii .Error :
179179 return None
180- # Canonical-form gate: `validate=True` still tolerates payloads whose
181- # trailing bits are non-zero (e.g. `SGVsbG9=` for `Hello`); re-encoding
182- # must reproduce the payload exactly. The encoder only emits canonical
183- # base64, so no conforming peer is affected.
180+ # Reject non-canonical base64 (e.g. non-zero trailing bits), which
181+ # `validate=True` tolerates; the encoder only ever emits canonical form.
184182 if base64 .b64encode (decoded ).decode ("ascii" ) != payload :
185183 return None
186184 try :
@@ -251,27 +249,18 @@ def x_mcp_header_map(input_schema: Any) -> dict[tuple[str, ...], str]:
251249def _annotated_positions (input_schema : Any ) -> Iterator [tuple [tuple [str , ...], str , dict [str , Any ]]]:
252250 """Yield `(path, token, schema)` for every statically-reachable `x-mcp-header` annotation.
253251
254- The single source of the "what counts as a declared header" predicate,
255- consumed by both the client-emit side (:func:`x_mcp_header_map`) and the
256- server-validate side (:func:`validate_mcp_param_headers`) so the two ends
257- of the mirror contract cannot drift.
252+ Shared by client emit and server validate so both ends agree on what counts as a declared header.
258253 """
259254 for path , schema in _walk_schema_positions (input_schema ):
260255 if path and isinstance (token := schema .get (X_MCP_HEADER_KEY ), str ):
261256 yield path , token , schema
262257
263258
264259def _render_header_scalar (value : Any ) -> str | None :
265- """Render an argument value the way the client mirrors it into a header, or `None`
266- when no header rendering exists.
267-
268- The single source of the "mirrorable value" fact, shared by emit
269- (:func:`mcp_param_headers`, which omits the header) and validate
270- (:func:`validate_mcp_param_headers`, for which a header claiming an
271- unmirrorable value can never match) so the two sides agree by
272- construction. Unmirrorable: non-primitive values — the spec defines
273- rendering for string/integer/boolean only — and integers beyond CPython's
274- int-to-str conversion limit, which no JSON peer can express anyway.
260+ """Render `value` the way the client mirrors it into a header, or `None` when no rendering exists.
261+
262+ Shared by emit and validate so both sides agree on what is mirrorable:
263+ non-primitives and ints beyond CPython's int-to-str digit limit are not.
275264 """
276265 if isinstance (value , bool ):
277266 return "true" if value else "false"
@@ -291,8 +280,8 @@ def mcp_param_headers(header_map: Mapping[tuple[str, ...], str], arguments: Mapp
291280 `Mcp-Param-<token>` carrying it: `bool` as `true`/`false`, other scalars via
292281 `str`, each passed through :func:`encode_header_value` so a non-token value
293282 is base64-wrapped. A path that hits a missing key or a non-mapping node is
294- skipped, matching the spec's "omit the header when no value is present" —
295- as is a value with no header rendering (see :func:`_render_header_scalar`) .
283+ skipped, matching the spec's "omit the header when no value is present",
284+ as is a value with no header rendering.
296285 """
297286 headers : dict [str , str ] = {}
298287 for path , token in header_map .items ():
@@ -363,16 +352,9 @@ class InboundLadderRejection:
363352def find_duplicated_routing_header (headers : Iterable [tuple [str , str ]]) -> str | None :
364353 """Name of a routing header supplied more than once in raw header lines, or `None`.
365354
366- `headers` is the carrier's raw `(name, value)` pairs (e.g.
367- `request.headers.items()`), which — unlike a folded mapping — still shows
368- duplicates. The routing headers (`MCP-Protocol-Version`, `Mcp-Method`,
369- `Mcp-Name`) are always recognized, so a duplicate is always unverifiable:
370- a consumer reading one copy and a validator reading the other would
371- disagree, the divergence header validation exists to prevent. Callers
372- reject with `HEADER_MISMATCH` before running the validation ladder.
373- `Mcp-Param-*` duplicates are not this function's job:
374- :func:`validate_mcp_param_headers` rejects recognized ones and the spec's
375- forward-and-ignore rule covers unrecognized ones.
355+ Takes raw `(name, value)` pairs — a folded mapping hides duplicates. A
356+ duplicate is rejected because first-copy and last-copy readers would
357+ disagree. `Mcp-Param-*` duplicates are :func:`validate_mcp_param_headers`'s job.
376358 """
377359 seen : set [str ] = set ()
378360 for name , _ in headers :
@@ -468,27 +450,18 @@ def classify_inbound_request(
468450 )
469451
470452
471- # An integer header value the spec's numeric-comparison SHOULD applies to:
472- # optional sign, digits, optional pure-decimal fraction. Scientific notation
473- # (`1e2`) and anything looser never compares numerically — the same canonical-
474- # decimal gate the typescript-sdk applies on the header side.
453+ # Header values eligible for the spec's numeric-comparison SHOULD; scientific
454+ # notation never compares numerically (matching the typescript-sdk's gate).
475455_CANONICAL_DECIMAL = re .compile (r"^-?[0-9]+(\.[0-9]+)?$" )
476456
477457
478458def _mcp_param_value_matches (prop_type : Any , value : Any , rendered : str , decoded : str ) -> bool :
479459 """True when a decoded `Mcp-Param-*` header value agrees with the body argument.
480460
481- `rendered` is the argument's own header rendering, so the fallback
482- comparison is the exact inverse of the emit side by construction. An
483- integer-typed declaration with an integral body value (`int`, or a `float`
484- like `42.0` that JSON Schema also admits as an integer; `bool` excluded —
485- it renders `true`/`false`) compares numerically (`42` matches `42.0`, the
486- spec's SHOULD) but only for canonical-decimal headers, and exactly — no
487- float round-trip, so values outside the IEEE754 safe range still compare
488- correctly. A non-canonical header (e.g. scientific notation) falls back to
489- the rendered comparison, so the client's own rendering of any value always
490- matches; a header whose integer part overflows CPython's int-conversion
491- digit limit can never name a JSON-expressible value, so it does not match.
461+ Integer-typed declarations with an integral body value compare numerically
462+ (`42` matches `42.0`, the spec's SHOULD) for canonical-decimal headers —
463+ exact, no float round-trip, so values beyond the IEEE754 safe range still
464+ compare. Anything else compares against `rendered`, the emit-side rendering.
492465 """
493466 if (
494467 prop_type == "integer"
@@ -513,29 +486,15 @@ def validate_mcp_param_headers(
513486) -> InboundLadderRejection | None :
514487 """Compare a `tools/call` request's `Mcp-Param-*` headers against its body arguments.
515488
516- The server half of the `x-mcp-header` contract: for every annotated
517- property in `input_schema`, the body argument and its mirroring header
518- must agree — present together and equal after sentinel decoding, or absent
519- together (`null` counts as absent). Returns the first failure as an
520- :class:`InboundLadderRejection` carrying `HEADER_MISMATCH` (callers map it
521- to HTTP 400 via :data:`ERROR_CODE_HTTP_STATUS`), or `None` when every
522- declaration is satisfied.
523-
524- Header names are matched case-insensitively regardless of the carrier's
525- canonicalization, and a recognized header supplied more than once (the
526- carrier's `items()` yielding duplicate names) is rejected — consumers that
527- read the first copy and a validator that read the last would otherwise
528- disagree. Headers with no matching declaration are ignored — the spec's
529- forward-and-ignore rule for unrecognized `Mcp-Param-*` headers. A header
530- present for an absent/null argument is rejected, as is one for an
531- argument no scalar rendering can match (a non-primitive value at an
532- annotated path): the spec's purpose clause is exactly the case of an
533- intermediary routing on a header value the body never carried
534- (streamable-http.mdx §Server Validation). Without the header, a
535- non-primitive argument is left to params validation at dispatch. An
536- `input_schema` that :func:`find_invalid_x_mcp_header` rejects validates
537- nothing: the spec assigns definition rejection to clients, which drop the
538- tool from `tools/list`, so no conforming client emitted headers for it.
489+ Each annotated property's header and argument must agree: present together
490+ and equal after sentinel decoding, or absent together (`null` counts as
491+ absent). Returns the first failure as a `HEADER_MISMATCH` rejection, else `None`.
492+
493+ A header whose argument is absent or unrenderable is deliberately rejected:
494+ the spec's purpose clause is exactly an intermediary routing on a value the
495+ body never carried. A duplicated recognized header is rejected — first-copy
496+ and last-copy readers would disagree. A schema :func:`find_invalid_x_mcp_header`
497+ rejects validates nothing: conforming clients drop the tool and emit no headers.
539498 """
540499 if find_invalid_x_mcp_header (input_schema ) is not None :
541500 return None
@@ -566,8 +525,7 @@ def validate_mcp_param_headers(
566525 continue
567526 rendered = _render_header_scalar (value )
568527 if rendered is None :
569- # No header rendering exists for this value, so a conforming
570- # client omitted the header; one claiming it can never match.
528+ # Unrenderable value: a conforming client omitted the header, so one claiming it can never match.
571529 if raw is not None :
572530 return InboundLadderRejection (
573531 code = HEADER_MISMATCH ,
0 commit comments