Skip to content

Commit 6c1743e

Browse files
committed
Tighten comments and docstrings to load-bearing facts only
1 parent 58b4cc1 commit 6c1743e

5 files changed

Lines changed: 84 additions & 208 deletions

File tree

src/mcp/server/_streamable_http_modern.py

Lines changed: 17 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -219,9 +219,7 @@ async def _write(
219219
_MCP_PARAM_PREFIX_LOWER: Final = MCP_PARAM_HEADER_PREFIX.lower()
220220

221221
_MCP_PARAM_LIST_PAGE_CAP: Final = 100
222-
"""Upper bound on `tools/list` pages walked while resolving a tool schema for
223-
`Mcp-Param-*` validation. A paginator bug (non-terminating or cycling cursor
224-
chain) must degrade to a logged validation skip, never a request-path hang."""
222+
"""Page cap for the schema-resolving tools/list walk: a buggy paginator degrades to a logged skip, not a hang."""
225223

226224

227225
async def _tool_input_schema(
@@ -234,14 +232,9 @@ async def _tool_input_schema(
234232
) -> Any | None:
235233
"""Resolve `name`'s inputSchema from the server's own registered `tools/list` handler.
236234
237-
The synthetic listing runs through the normal `serve_one` path — the
238-
caller's envelope (rebuilt from the classifier verdict, so caller-side
239-
`_meta` extras like a progress token are not replayed), the middleware
240-
chain, a fresh per-page `Connection`, and an absorbing dispatch context
241-
(no SSE sink, so request-scoped notifications drop instead of raising) —
242-
so a visibility-scoped catalog yields exactly what *this* caller was
243-
advertised. Returns `None` (caller skips validation) when the listing
244-
raises, paginates past the cap or cycles, or never advertises the tool.
235+
The listing runs through the normal `serve_one` path, so a visibility-scoped
236+
catalog yields exactly what *this* caller was advertised. Returns None
237+
(caller skips validation) when the listing fails or never advertises the tool.
245238
"""
246239
meta = {
247240
PROTOCOL_VERSION_META_KEY: verdict.protocol_version,
@@ -258,9 +251,7 @@ async def _tool_input_schema(
258251
message_metadata=ServerMessageMetadata(request_context=request),
259252
)
260253
for _ in range(_MCP_PARAM_LIST_PAGE_CAP):
261-
# A fresh per-page Connection is load-bearing: serve_one tears down
262-
# the connection's exit stack on the way out. The dispatch context is
263-
# page-independent and reused.
254+
# Fresh Connection per page: serve_one tears down the connection's exit stack on the way out.
264255
connection = Connection.from_envelope(verdict.protocol_version, client_info, client_capabilities)
265256
try:
266257
result = await serve_one(
@@ -271,26 +262,17 @@ async def _tool_input_schema(
271262
return tool.get("inputSchema")
272263
cursor = result.get("nextCursor")
273264
except ValidationError:
274-
# Client fault: a mis-shaped envelope value the classifier admits
275-
# on key presence alone fails the kernel's surface validation here
276-
# first. The real dispatch produces the INVALID_PARAMS reply, so
277-
# this is not worth more than a debug line (an exception-level log
278-
# would let any client flood the log blaming the server).
265+
# Client-fault envelope: the real dispatch produces the INVALID_PARAMS
266+
# reply, and anything above a debug line would let clients flood the log.
279267
logger.debug("Mcp-Param header validation skipped: the request envelope fails tools/list validation")
280268
return None
281269
except Exception:
282-
# Boundary by design: header validation must never break a working
283-
# call path, so a failing listing — the handler raising, or a
284-
# middleware short-circuit returning a mis-shaped result — skips
285-
# validation for this request. Loudly, because the skip is
286-
# fail-open. (A server broken here is broken for real discovery
287-
# too.)
270+
# Fail-open boundary by design: header validation must never break a
271+
# working call path. Loud, precisely because the skip is fail-open.
288272
logger.exception("Mcp-Param header validation skipped: the tools/list listing failed")
289273
return None
290274
if not isinstance(cursor, str):
291-
# Listing exhausted without advertising `name`: nothing was
292-
# declared to this caller, so there is nothing to validate —
293-
# dispatch owns rejecting a genuinely unknown tool.
275+
# Listing exhausted without advertising `name`; dispatch owns rejecting an unknown tool.
294276
return None
295277
if cursor in seen_cursors:
296278
logger.warning("Mcp-Param header validation skipped: the tools/list handler returned a cursor cycle")
@@ -313,13 +295,9 @@ async def _mcp_param_rejection(
313295
) -> InboundLadderRejection | None:
314296
"""Validate a `tools/call` request's `Mcp-Param-*` headers against the called tool's schema.
315297
316-
Runs post-classification, pre-dispatch — and before any SSE machinery, so
317-
a rejection is always a plain `application/json` 400 (the spec's MUST for
318-
header-validation failures). The schema source is the registered
319-
`tools/list` handler; with none registered the catalog is undiscoverable,
320-
no client can have been told about an `x-mcp-header` annotation, and there
321-
is no recognized header to validate. A mis-shaped `name`/`arguments` is
322-
left to params validation at dispatch.
298+
Runs pre-dispatch, before any SSE machinery, so a rejection is always a
299+
plain `application/json` 400 (the spec's MUST). With no `tools/list` handler
300+
the catalog is undiscoverable and there is no recognized header to validate.
323301
"""
324302
if req.method != "tools/call" or app.get_request_handler("tools/list") is None:
325303
return None
@@ -331,11 +309,9 @@ async def _mcp_param_rejection(
331309
if raw_arguments is not None and not isinstance(raw_arguments, Mapping):
332310
return None
333311
arguments: Mapping[str, Any] = cast("Mapping[str, Any]", raw_arguments) if raw_arguments is not None else {}
334-
# ASGI guarantees lowercase header names, same invariant the classifier
335-
# leans on; the pure validator re-folds for arbitrary carriers.
312+
# ASGI guarantees lowercase header names, so no case-folding here.
336313
if not arguments and not any(header.startswith(_MCP_PARAM_PREFIX_LOWER) for header in request.headers):
337-
# With no argument values and no `Mcp-Param-*` headers, no declaration
338-
# could be violated in either direction — skip the listing outright.
314+
# No argument values and no `Mcp-Param-*` headers: no declaration can be violated either way.
339315
return None
340316
input_schema = await _tool_input_schema(app, request, req.id, verdict, lifespan_state, name)
341317
if input_schema is None:
@@ -382,9 +358,7 @@ async def handle_modern_request(
382358
try:
383359
decoded = json.loads(body)
384360
except ValueError:
385-
# ValueError, not its JSONDecodeError subclass: an integer literal
386-
# beyond CPython's int-conversion digit limit raises the bare parent
387-
# and is just as unparseable.
361+
# Not just JSONDecodeError: integer literals beyond CPython's digit limit raise bare ValueError.
388362
rej = JSONRPCError(jsonrpc="2.0", id=None, error=ErrorData(code=PARSE_ERROR, message="Parse error"))
389363
await _write(rej, scope, receive, send)
390364
return
@@ -408,10 +382,7 @@ async def handle_modern_request(
408382

409383
duplicated = find_duplicated_routing_header(request.headers.items())
410384
if duplicated is not None:
411-
# The classifier receives a folded mapping that can no longer show
412-
# duplicates, so the raw-carrier check lives here: a routing header
413-
# supplied twice is unverifiable (a consumer reading one copy and the
414-
# validator the other would disagree).
385+
# The raw carrier is the only place duplicates are visible; the classifier sees a folded mapping.
415386
rejection = InboundLadderRejection(code=HEADER_MISMATCH, message=f"{duplicated} header appears more than once")
416387
await _write_rejection(rejection, req.id, scope, receive, send)
417388
return

src/mcp/shared/inbound.py

Lines changed: 28 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -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]:
251249
def _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

264259
def _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:
363352
def 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

478458
def _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,

tests/interaction/transports/test_hosting_http_modern.py

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -474,11 +474,9 @@ async def test_modern_client_emits_no_param_headers_for_an_unlisted_tool() -> No
474474
The spec lets a client that lacks the tool's `inputSchema` send the request without custom headers.
475475
The call is made with no prior `list_tools`, so the first `tools/call` POST -- captured before the
476476
implicit output-schema `list_tools` runs -- has no cached annotations and emits no `Mcp-Param-*` header.
477-
The server, which does validate `Mcp-Param-*` headers against its own catalog, rejects exactly as the
478-
spec's scenario table requires for an omitted header whose value is in the body (the client-side
479-
relist-and-retry recovery is a spec SHOULD the client does not implement yet).
477+
The server validates `Mcp-Param-*` against its own catalog and rejects as the spec's scenario table
478+
requires for an omitted header (the relist-and-retry recovery is a SHOULD the client does not implement yet).
480479
"""
481-
# The rejected call is the only request: no implicit output-schema list follows a failure.
482480
requests: list[httpx.Request] = []
483481

484482
async def on_request(request: httpx.Request) -> None:
@@ -519,10 +517,8 @@ async def test_modern_client_stops_mirroring_after_a_re_list_drops_the_tool() ->
519517
bad_schema = {"type": "object", "properties": {"a": {"type": "string", "x-mcp-header": "bad name"}}}
520518
valid = Tool(name="run", input_schema=schema)
521519
invalid = Tool(name="run", input_schema=bad_schema)
522-
# First listing valid, every later one invalid. Listings are not counted: beyond the client's own
523-
# calls (initial list, the re-list that drops the tool, the implicit re-list before the second call,
524-
# since the prune also cleared `run`'s schema entry), the server reads its own catalog to validate
525-
# `Mcp-Param-*` headers on each tools/call.
520+
# First listing valid, every later one invalid; the count is not pinned because the server also
521+
# reads its own catalog on each tools/call.
526522
listings: list[None] = []
527523

528524
async def list_tools(ctx: ServerRequestContext, params: PaginatedRequestParams | None) -> ListToolsResult:

0 commit comments

Comments
 (0)