Skip to content
Closed
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
14 changes: 5 additions & 9 deletions src/mcp/server/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,15 +186,9 @@ async def _inner(ctx: ServerRequestContext[LifespanT, Any]) -> HandlerResult:
# Read method/params off `ctx` so a middleware that rewrote them via
# `call_next(replace(ctx, ...))` reaches lookup and the handler.
method, params = ctx.method, ctx.params
# Pinned compat: spec methods are surface-validated before lookup,
# so malformed params are INVALID_PARAMS even with no handler
# registered. Custom methods miss the monolith map and fall through
# to `entry.params_type` exactly as before.
if method in _methods.SPEC_CLIENT_METHODS:
try:
_methods.validate_client_request(method, version, params)
except KeyError:
raise MCPError(code=METHOD_NOT_FOUND, message="Method not found", data=method) from None
# Spec version gate: if a spec method is not valid at this protocol version, reject it.
if method in _methods.SPEC_CLIENT_METHODS and (method, version) not in _methods.CLIENT_REQUESTS:
raise MCPError(code=METHOD_NOT_FOUND, message="Method not found", data=method)
# TODO(L29): the 2026-07-28 spec drops the handshake; this branch and
# the gate become a per-version legacy path then. Initialize runs inline
# (read loop parked), so awaiting the peer anywhere on this path deadlocks.
Expand All @@ -211,6 +205,8 @@ async def _inner(ctx: ServerRequestContext[LifespanT, Any]) -> HandlerResult:
if not self.connection.initialize_accepted and method not in _INIT_EXEMPT:
# Pinned compat: the same error shape the union validation produced.
raise MCPError(code=INVALID_PARAMS, message="Invalid request parameters", data="")
if method in _methods.SPEC_CLIENT_METHODS:
_methods.validate_client_request(method, version, params)
# Absent params validate as {} (required fields still reject), so
# the handler receives the model with its defaults, never None.
typed_params = entry.params_type.model_validate({} if params is None else params, by_name=False)
Expand Down
5 changes: 5 additions & 0 deletions tests/interaction/lowlevel/test_wire.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,12 @@ async def test_malformed_request_params_are_answered_with_invalid_params() -> No
initialization handshake at the JSON-RPC layer and then sends a tools/call whose `name` is an
integer. Reserve this pattern for behaviour the typed API cannot produce.
"""

async def dummy_handler(ctx: object, p: CallToolRequestParams) -> None:
pass

server = Server("strict")
server.add_request_handler("tools/call", CallToolRequestParams, dummy_handler)
errors: list[ErrorData] = []

async with create_client_server_memory_streams() as (client_streams, server_streams):
Expand Down
10 changes: 7 additions & 3 deletions tests/server/test_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -295,12 +295,12 @@ async def test_runner_non_spec_method_with_no_handler_raises_method_not_found(se


@pytest.mark.anyio
async def test_runner_malformed_params_for_unregistered_spec_method_raises_invalid_params(server: SrvT):
"""A spec method with malformed params is INVALID_PARAMS even with no handler."""
async def test_runner_unregistered_spec_method_raises_method_not_found(server: SrvT):
"""An unregistered spec method returns METHOD_NOT_FOUND per JSON-RPC 2.0 (issue #3193)."""
async with connected_runner(server) as (client, _):
with pytest.raises(MCPError) as exc:
await client.send_raw_request("tools/call", {"name": 123})
assert exc.value.error == ErrorData(code=INVALID_PARAMS, message="Invalid request parameters", data="")
assert exc.value.error == ErrorData(code=METHOD_NOT_FOUND, message="Method not found", data="tools/call")


@pytest.mark.anyio
Expand Down Expand Up @@ -845,6 +845,10 @@ async def greet(ctx: Ctx, params: GreetParams) -> dict[str, Any]:

@pytest.mark.anyio
async def test_runner_spec_method_with_invalid_params_is_invalid_params_at_the_negotiated_version(server: SrvT):
async def dummy_handler(ctx: Ctx, p: CallToolRequestParams) -> None:
pass

server.add_request_handler("tools/call", CallToolRequestParams, dummy_handler)
async with connected_runner(server) as (client, runner):
assert runner.connection.protocol_version == LATEST_HANDSHAKE_VERSION
with pytest.raises(MCPError) as exc:
Expand Down
Loading