Skip to content

Commit 9470abf

Browse files
committed
D8: move method-existence from classifier rung-4 to kernel dispatch
1 parent 764650e commit 9470abf

4 files changed

Lines changed: 48 additions & 58 deletions

File tree

‎src/mcp/server/_streamable_http_modern.py‎

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -89,11 +89,10 @@ async def progress(self, progress: float, total: float | None = None, message: s
8989
def _typed(model: type[_ModelT], raw: Any) -> _ModelT | None:
9090
"""Validate the classifier's raw envelope value into a typed model.
9191
92-
The classifier checks presence only; a value that fails shape validation
93-
is treated as not supplied so the request still routes.
92+
Rung 1 guarantees the envelope key was present; a ``null`` or mis-shaped
93+
value falls through to ``ValidationError`` and is treated as not supplied
94+
so the request still routes.
9495
"""
95-
if raw is None:
96-
return None
9796
try:
9897
return model.model_validate(raw, by_name=False)
9998
except ValidationError:

‎src/mcp/shared/inbound.py‎

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727
PARSE_ERROR,
2828
UNSUPPORTED_PROTOCOL_VERSION,
2929
)
30-
from mcp.types.methods import CLIENT_REQUESTS
3130

3231
__all__ = [
3332
"ERROR_CODE_HTTP_STATUS",
@@ -60,13 +59,13 @@
6059

6160
@dataclass(frozen=True)
6261
class InboundModernRoute:
63-
"""A modern-protocol request that passed every ladder rung.
62+
"""A modern-protocol request whose envelope passed every ladder rung.
6463
6564
``client_info`` and ``client_capabilities`` are the raw envelope values;
66-
the classifier checks presence only, not shape.
65+
the classifier checks presence only, not shape. Method existence is not a
66+
ladder rung — kernel dispatch is the single source of truth for that.
6767
"""
6868

69-
method: str
7069
protocol_version: str
7170
client_info: Any
7271
client_capabilities: Any
@@ -100,10 +99,9 @@ def classify_inbound_request(
10099
3. When ``headers`` is given, its ``MCP-Protocol-Version`` entry equals
101100
the envelope's protocol version → else
102101
:data:`~mcp.types.jsonrpc.HEADER_MISMATCH`.
103-
4. ``(method, protocol_version)`` is a key of
104-
:data:`mcp.types.methods.CLIENT_REQUESTS` → else
105-
:data:`~mcp.types.jsonrpc.METHOD_NOT_FOUND`. Absence *is* the gate: a
106-
method removed at a given version simply has no row.
102+
103+
Method existence is *not* a rung: kernel dispatch owns that decision so
104+
custom-registered methods route and the answer lives in one place.
107105
108106
Args:
109107
body: The decoded JSON-RPC request mapping. Envelope shape
@@ -138,15 +136,7 @@ def classify_inbound_request(
138136
message=f"{MCP_PROTOCOL_VERSION_HEADER} header does not match the request envelope's protocol version",
139137
)
140138

141-
method: Any = body.get("method")
142-
if (method, protocol_version) not in CLIENT_REQUESTS:
143-
return InboundLadderRejection(
144-
code=METHOD_NOT_FOUND,
145-
message=f"Method not found: {method!r} at protocol version {protocol_version}",
146-
)
147-
148139
return InboundModernRoute(
149-
method=method,
150140
protocol_version=protocol_version,
151141
client_info=client_info,
152142
client_capabilities=client_capabilities,

‎tests/server/test_runner.py‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -923,6 +923,33 @@ async def discover(ctx: Ctx, params: RequestParams) -> Any:
923923
assert exc.value.error == ErrorData(code=METHOD_NOT_FOUND, message="Method not found", data="server/discover")
924924

925925

926+
@pytest.mark.anyio
927+
async def test_on_request_rejects_initialize_at_modern_version_with_method_not_found(server: SrvT):
928+
"""Spec-mandated: `initialize` has no `CLIENT_REQUESTS` row at the modern
929+
version; kernel dispatch (not the inbound classifier) rejects it."""
930+
born_ready = Connection.from_envelope(MODERN_PROTOCOL_VERSIONS[0], None, None)
931+
async with connected_runner(server, initialized=False, connection=born_ready) as (client, runner):
932+
assert runner.connection.protocol_version == MODERN_PROTOCOL_VERSIONS[0]
933+
with pytest.raises(MCPError) as exc:
934+
await client.send_raw_request("initialize", _initialize_params())
935+
assert exc.value.error.code == METHOD_NOT_FOUND
936+
937+
938+
@pytest.mark.anyio
939+
async def test_on_request_dispatches_custom_method_registered_via_add_request_handler(server: SrvT):
940+
"""SDK-defined: a method outside `SPEC_CLIENT_METHODS` skips the version
941+
gate and reaches its registered handler at any negotiated version."""
942+
943+
async def echo(ctx: Ctx, params: RequestParams) -> dict[str, Any]:
944+
return {"echoed": True}
945+
946+
server.add_request_handler("myorg/echo", RequestParams, echo)
947+
born_ready = Connection.from_envelope(MODERN_PROTOCOL_VERSIONS[0], None, None)
948+
async with connected_runner(server, initialized=False, connection=born_ready) as (client, _):
949+
result = await client.send_raw_request("myorg/echo", None)
950+
assert result == {"echoed": True}
951+
952+
926953
@pytest.mark.anyio
927954
async def test_runner_middleware_short_circuit_on_a_wrong_version_spec_method_skips_the_sieve(server: SrvT):
928955
"""A server-tier middleware that returns without calling `call_next` for a

‎tests/shared/test_inbound.py‎

Lines changed: 12 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -152,54 +152,28 @@ def test_header_rung_rejects_on_disagreement(headers: dict[str, str]) -> None:
152152
assert_rejected(classify_inbound_request(envelope(), headers=headers), HEADER_MISMATCH)
153153

154154

155-
# --- rung 4: method-exists-at-version ------------------------------------------
156-
157-
158-
@pytest.mark.parametrize(
159-
"method",
160-
[
161-
"initialize",
162-
"ping",
163-
"logging/setLevel",
164-
"resources/subscribe",
165-
"resources/unsubscribe",
166-
],
167-
)
168-
def test_method_rung_rejects_methods_removed_at_modern_version(method: str) -> None:
169-
"""Spec-mandated: absence from ``CLIENT_REQUESTS`` at the modern version *is* the gate — removed methods reject."""
170-
rejection = assert_rejected(classify_inbound_request(envelope(method)), METHOD_NOT_FOUND)
171-
assert method in rejection.message
172-
173-
174-
def test_method_rung_rejects_unknown_method() -> None:
175-
"""Spec-mandated: a method name not in the version's request table rejects METHOD_NOT_FOUND."""
176-
assert_rejected(classify_inbound_request(envelope("does/not/exist")), METHOD_NOT_FOUND)
177-
178-
179-
def test_method_rung_rejects_absent_method_key() -> None:
180-
"""Spec-mandated: a body with no ``method`` key cannot route and rejects METHOD_NOT_FOUND."""
181-
body = envelope()
182-
del body["method"]
183-
assert_rejected(classify_inbound_request(body), METHOD_NOT_FOUND)
184-
185-
186155
# --- all rungs pass ------------------------------------------------------------
187156

188157

189-
@pytest.mark.parametrize("method", ["tools/list", "tools/call", "server/discover"])
190-
def test_all_rungs_pass_yields_route(method: str) -> None:
158+
def test_all_rungs_pass_yields_route() -> None:
191159
"""Spec-mandated: a complete envelope at a supported version with agreeing header routes, surfacing the envelope."""
192-
result = classify_inbound_request(envelope(method), headers={MCP_PROTOCOL_VERSION_HEADER: MODERN})
160+
result = classify_inbound_request(envelope(), headers={MCP_PROTOCOL_VERSION_HEADER: MODERN})
193161
assert isinstance(result, InboundModernRoute)
194-
assert result.method == method
195162
assert result.protocol_version == MODERN
196163
assert result.client_info == CLIENT_INFO
197164
assert result.client_capabilities == CLIENT_CAPS
198165

199166

167+
@pytest.mark.parametrize("method", ["initialize", "myorg/custom", "does/not/exist"])
168+
def test_classifier_passes_unknown_method_through_to_route(method: str) -> None:
169+
"""SDK-defined: the classifier does not gate on method — kernel dispatch is the single owner of that decision."""
170+
result = classify_inbound_request(envelope(method), headers={MCP_PROTOCOL_VERSION_HEADER: MODERN})
171+
assert isinstance(result, InboundModernRoute)
172+
173+
200174
def test_ladder_first_failure_wins() -> None:
201-
"""Spec-mandated: rungs evaluate in order — version, header and method would all fail; the version rung fires."""
202-
body = envelope("initialize", version=LATEST_PROTOCOL_VERSION)
175+
"""Spec-mandated: rungs evaluate in order — version and header would both fail; the version rung fires."""
176+
body = envelope(version=LATEST_PROTOCOL_VERSION)
203177
result = classify_inbound_request(body, headers={MCP_PROTOCOL_VERSION_HEADER: MODERN})
204178
assert_rejected(result, UNSUPPORTED_PROTOCOL_VERSION)
205179

@@ -224,7 +198,7 @@ def test_error_code_http_status_table(code: int, status: int) -> None:
224198

225199
def test_error_code_http_status_covers_every_ladder_code() -> None:
226200
"""SDK-defined: every code the ladder can emit has an HTTP-status entry, so the transport never has to default."""
227-
ladder_codes = {INVALID_PARAMS, UNSUPPORTED_PROTOCOL_VERSION, HEADER_MISMATCH, METHOD_NOT_FOUND}
201+
ladder_codes = {INVALID_PARAMS, UNSUPPORTED_PROTOCOL_VERSION, HEADER_MISMATCH}
228202
assert ladder_codes <= ERROR_CODE_HTTP_STATUS.keys()
229203

230204

0 commit comments

Comments
 (0)