Skip to content

Commit 482d7cd

Browse files
committed
Require the version header's presence in the classifier's header rung
A null body protocolVersion slipped the header equality check (None == None), so the absent-header outcome depended on the body value: an int gave HEADER_MISMATCH while null fell through to the string guard's INVALID_PARAMS. The rung now checks presence explicitly, making an absent version header uniformly HEADER_MISMATCH and the string guard reachable only on header-less transports. Also updates the public Server.run docstring to the first-success era-lock wording.
1 parent f35d966 commit 482d7cd

3 files changed

Lines changed: 18 additions & 24 deletions

File tree

src/mcp/server/lowlevel/server.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -691,8 +691,8 @@ async def run(
691691
692692
Thin wrapper over `serve_dual_era_loop`: enters the server lifespan,
693693
then drives the loop, serving the legacy handshake era and the modern
694-
per-request-envelope era (the first era-distinctive message locks the
695-
connection). Transports with their own lifespan owner (the
694+
per-request-envelope era (the first era-distinctive message to succeed
695+
locks the connection). Transports with their own lifespan owner (the
696696
streamable-HTTP manager) call `serve_loop` directly instead.
697697
"""
698698
async with self.lifespan(self) as lifespan_context:

src/mcp/shared/inbound.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -415,7 +415,10 @@ def classify_inbound_request(
415415
"client-capabilities envelope keys",
416416
)
417417
if headers is not None:
418-
if headers.get(MCP_PROTOCOL_VERSION_HEADER) != protocol_version:
418+
version_header = headers.get(MCP_PROTOCOL_VERSION_HEADER)
419+
# Presence is checked explicitly: a null body version would otherwise
420+
# slip the equality check (None == None) and mask the absent header.
421+
if version_header is None or version_header != protocol_version:
419422
return InboundLadderRejection(
420423
code=HEADER_MISMATCH,
421424
message=f"{MCP_PROTOCOL_VERSION_HEADER} header does not match the request envelope's protocol version",
@@ -440,9 +443,11 @@ def classify_inbound_request(
440443
# Rung 3's precondition: a shape defect, not a version-negotiation
441444
# outcome - -32022 is the one code auto-negotiating clients do NOT
442445
# fall back from, and the typed rung-3 payload itself requires a
443-
# string `requested`. Sits after the header rung so the HTTP wire is
444-
# untouched when the version header is present (a string header can
445-
# never equal a non-string body value, so rung 2 fires first there).
446+
# string `requested`. Sits after the header rung, which fires first
447+
# for every header-bearing entry (an absent version header is a
448+
# mismatch, and a present one is a string that can never equal a
449+
# non-string body value) - so this rejection is reachable only on
450+
# header-less transports.
446451
return InboundLadderRejection(
447452
code=INVALID_PARAMS,
448453
message="the protocol-version envelope value must be a string",

tests/shared/test_inbound.py

Lines changed: 7 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -151,30 +151,19 @@ def test_non_string_protocol_version_over_http_still_rejects_at_the_header_rung(
151151
assert_rejected(classify_inbound_request(body, headers=headers), HEADER_MISMATCH)
152152

153153

154-
def test_non_string_protocol_version_with_absent_header_is_a_header_mismatch() -> None:
155-
"""SDK-defined: an absent version header is a header defect first - the
156-
same HEADER_MISMATCH a string-bodied request gets - even when the body
157-
version is also mis-shaped (a non-string value never equals the absent
158-
header's None, so rung 2 owns this cell)."""
154+
@pytest.mark.parametrize("version", [7, None], ids=["int", "null"])
155+
def test_absent_version_header_rejects_before_the_string_guard(version: Any) -> None:
156+
"""SDK-defined: the version header must be PRESENT, not merely equal - a
157+
null body version would otherwise slip the equality check (None == None)
158+
- so an absent header is HEADER_MISMATCH for every body value and the
159+
string guard stays reachable only on header-less transports."""
159160
body = envelope()
160161
headers = matching_headers(body)
161162
del headers[MCP_PROTOCOL_VERSION_HEADER]
162-
body["params"]["_meta"][PROTOCOL_VERSION_META_KEY] = 7
163+
body["params"]["_meta"][PROTOCOL_VERSION_META_KEY] = version
163164
assert_rejected(classify_inbound_request(body, headers=headers), HEADER_MISMATCH)
164165

165166

166-
def test_non_string_protocol_version_with_no_version_header_hits_the_guard() -> None:
167-
"""SDK-defined: a null body version with the version header absent slips
168-
the header equality check (None == None), so the string guard is the
169-
backstop that keeps the version rung's typed payload from raising."""
170-
body = envelope()
171-
headers = matching_headers(body)
172-
del headers[MCP_PROTOCOL_VERSION_HEADER]
173-
body["params"]["_meta"][PROTOCOL_VERSION_META_KEY] = None
174-
rejection = assert_rejected(classify_inbound_request(body, headers=headers), INVALID_PARAMS)
175-
assert "string" in rejection.message
176-
177-
178167
# --- rung 2: protocol-version-supported ----------------------------------------
179168

180169

0 commit comments

Comments
 (0)