Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 19 additions & 4 deletions src/mcp/shared/inbound.py
Original file line number Diff line number Diff line change
Expand Up @@ -385,9 +385,10 @@ def classify_inbound_request(
2. When `headers` is given, `MCP-Protocol-Version` equals the envelope's
protocol version, `Mcp-Method` equals `body.method`, and — for the
methods in :data:`NAME_BEARING_METHODS` — `Mcp-Name` equals the named
body param → else :data:`~mcp_types.jsonrpc.HEADER_MISMATCH`. Runs
before the supported-version rung so a client that disagrees with itself
is told so, rather than told the body's version is unsupported.
body param, and is absent when that param is → else
:data:`~mcp_types.jsonrpc.HEADER_MISMATCH`. Runs before the
supported-version rung so a client that disagrees with itself is told
so, rather than told the body's version is unsupported.
3. The envelope's protocol version is a string in
`supported_modern_versions` → non-string values are
:data:`~mcp_types.jsonrpc.INVALID_PARAMS` (a shape defect, not a
Expand Down Expand Up @@ -444,7 +445,21 @@ def classify_inbound_request(
if name_key is not None:
# Rung 1 already proved body["params"] is a mapping (its `_meta` is one).
body_value = cast("Mapping[str, Any]", body["params"]).get(name_key)
if body_value is not None and decode_header_value(headers.get(MCP_NAME_HEADER)) != body_value:
name_header = headers.get(MCP_NAME_HEADER)
if body_value is None:
# An orphan header claiming a route the body never carried is the
# same spoofing risk validate_mcp_param_headers rejects for
# Mcp-Param-*: a conforming client never emits Mcp-Name unless the
# named param is present (see `emit`/matching_headers), so a header
# here with no matching body value did not come from this request's
# own body. The param's absence is INVALID_PARAMS elsewhere; that is
# orthogonal to whether a present header is trustworthy.
if name_header is not None:
return InboundLadderRejection(
code=HEADER_MISMATCH,
message=f"{MCP_NAME_HEADER} header is present but the body's {name_key!r} parameter is absent",
)
elif decode_header_value(name_header) != body_value:
return InboundLadderRejection(
code=HEADER_MISMATCH,
message=f"{MCP_NAME_HEADER} header does not match the request body's {name_key!r} parameter",
Expand Down
19 changes: 17 additions & 2 deletions tests/shared/test_inbound.py
Original file line number Diff line number Diff line change
Expand Up @@ -311,13 +311,28 @@ def test_header_rung_does_not_require_name_header_for_non_name_bearing_method()


def test_header_rung_does_not_require_name_header_when_body_omits_the_named_param() -> None:
"""SDK-defined: a name-bearing method whose body lacks the named param skips the `Mcp-Name`
check — the param's absence is INVALID_PARAMS later, not HEADER_MISMATCH here."""
"""SDK-defined: a name-bearing method whose body lacks the named param, and whose headers
carry no `Mcp-Name` either, skips the check — the param's absence is INVALID_PARAMS later,
not HEADER_MISMATCH here."""
body = envelope("tools/call")
result = classify_inbound_request(body, headers=matching_headers(body))
assert isinstance(result, InboundModernRoute)


@pytest.mark.parametrize(
("method", "name_key"),
[(m, k) for m, k in NAME_BEARING_METHODS.items()],
)
def test_header_rung_rejects_orphan_name_header_when_body_omits_the_named_param(method: str, name_key: str) -> None:
"""Regression for the asymmetry with `validate_mcp_param_headers`: a `Mcp-Name` header
claiming a route the body never carried is a spoofing risk, not a value with nothing to
compare against — mirrors the `Mcp-Param-*` "header present but argument absent" rejection.
"""
body = envelope(method)
headers = matching_headers(body) | {MCP_NAME_HEADER: encode_header_value("someone-elses-tool")}
assert_rejected(classify_inbound_request(body, headers=headers), HEADER_MISMATCH)


# --- all rungs pass ------------------------------------------------------------


Expand Down
Loading