Skip to content

Commit 4468a7f

Browse files
committed
Answer unknown methods with METHOD_NOT_FOUND before the initialize gate
The runner's pre-initialize gate rejected every non-exempt request with INVALID_PARAMS, including methods the server does not know at all. JSON-RPC 2.0 reserves -32601 for unknown methods regardless of session state, and clients probing a server before the handshake key off that code. Check for unknown methods before the gate; known-but-ungated methods keep the existing rejection. METHOD_NOT_FOUND errors now carry the method name in the error data.
1 parent 3230c3f commit 4468a7f

5 files changed

Lines changed: 44 additions & 6 deletions

File tree

src/mcp/server/runner.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,12 +243,17 @@ async def _inner() -> HandlerResult:
243243
# (read loop parked), so awaiting the peer anywhere on this path deadlocks.
244244
if method == "initialize":
245245
return self._handle_initialize(params)
246+
# Unknown methods are METHOD_NOT_FOUND regardless of initialization
247+
# state: JSON-RPC 2.0 reserves -32601 for them, and clients probing
248+
# a server before the handshake key off that code.
249+
if method not in _SPEC_CLIENT_METHODS and self.server.get_request_handler(method) is None:
250+
raise MCPError(code=METHOD_NOT_FOUND, message="Method not found", data=method)
246251
if not self.connection.initialize_accepted and method not in _INIT_EXEMPT:
247252
# Pinned compat: the same error shape the union validation produced.
248253
raise MCPError(code=INVALID_PARAMS, message="Invalid request parameters", data="")
249254
entry = self.server.get_request_handler(method)
250255
if entry is None:
251-
raise MCPError(code=METHOD_NOT_FOUND, message="Method not found")
256+
raise MCPError(code=METHOD_NOT_FOUND, message="Method not found", data=method)
252257
# Absent params validate as {} (required fields still reject), so
253258
# the handler receives the model with its defaults, never None.
254259
typed_params = entry.params_type.model_validate({} if params is None else params, by_name=False)

tests/interaction/lowlevel/test_completion.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,4 +128,6 @@ async def test_complete_without_handler_is_method_not_found(connect: Connect) ->
128128
with pytest.raises(MCPError) as exc_info:
129129
await client.complete(PromptReference(name="anything"), argument={"name": "topic", "value": ""})
130130

131-
assert exc_info.value.error == snapshot(ErrorData(code=METHOD_NOT_FOUND, message="Method not found"))
131+
assert exc_info.value.error == snapshot(
132+
ErrorData(code=METHOD_NOT_FOUND, message="Method not found", data="completion/complete")
133+
)

tests/interaction/lowlevel/test_resources.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,9 @@ async def list_resources(
236236
with pytest.raises(MCPError) as exc_info:
237237
await client.subscribe_resource("file:///watched.txt")
238238

239-
assert exc_info.value.error == snapshot(ErrorData(code=METHOD_NOT_FOUND, message="Method not found"))
239+
assert exc_info.value.error == snapshot(
240+
ErrorData(code=METHOD_NOT_FOUND, message="Method not found", data="resources/subscribe")
241+
)
240242

241243

242244
@requirement("resources:unsubscribe")

tests/interaction/mcpserver/test_context.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,9 @@ async def collect(params: LoggingMessageNotificationParams) -> None:
262262

263263
await client.call_tool("chatter", {})
264264

265-
assert exc_info.value.error == snapshot(ErrorData(code=METHOD_NOT_FOUND, message="Method not found"))
265+
assert exc_info.value.error == snapshot(
266+
ErrorData(code=METHOD_NOT_FOUND, message="Method not found", data="logging/setLevel")
267+
)
266268
assert received == snapshot(
267269
[
268270
LoggingMessageNotificationParams(level="debug", data="noise"),

tests/server/test_runner.py

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,33 @@ async def test_runner_gates_requests_before_initialize(server: SrvT):
168168
assert await client.send_raw_request("ping", None) == {}
169169

170170

171+
@pytest.mark.anyio
172+
async def test_runner_unknown_method_before_initialize_raises_method_not_found(server: SrvT):
173+
"""An unknown method is METHOD_NOT_FOUND even before initialize: JSON-RPC
174+
2.0 reserves -32601 for it, and clients probing a server before the
175+
handshake key off that code. The init gate only applies to methods the
176+
server actually knows."""
177+
async with connected_runner(server, initialized=False) as (client, _):
178+
with pytest.raises(MCPError) as exc:
179+
await client.send_raw_request("x/unknown", None)
180+
assert exc.value.error == ErrorData(code=METHOD_NOT_FOUND, message="Method not found", data="x/unknown")
181+
182+
183+
@pytest.mark.anyio
184+
async def test_runner_custom_method_with_handler_is_still_gated_before_initialize(server: SrvT):
185+
"""A custom-registered method is a known method: before initialize it is
186+
rejected by the init gate, not answered with METHOD_NOT_FOUND."""
187+
188+
async def greet(ctx: Ctx, params: RequestParams | None) -> Any:
189+
raise NotImplementedError # the gate rejects the request first
190+
191+
server.add_request_handler("custom/greet", RequestParams, greet)
192+
async with connected_runner(server, initialized=False) as (client, _):
193+
with pytest.raises(MCPError) as exc:
194+
await client.send_raw_request("custom/greet", None)
195+
assert exc.value.error == ErrorData(code=INVALID_PARAMS, message="Invalid request parameters", data="")
196+
197+
171198
@pytest.mark.anyio
172199
async def test_runner_routes_to_handler_and_builds_context(server: SrvT):
173200
async with connected_runner(server) as (client, runner):
@@ -186,7 +213,7 @@ async def test_runner_spec_method_with_no_handler_raises_method_not_found(server
186213
async with connected_runner(server) as (client, _):
187214
with pytest.raises(MCPError) as exc:
188215
await client.send_raw_request("resources/list", None)
189-
assert exc.value.error.code == METHOD_NOT_FOUND
216+
assert exc.value.error == ErrorData(code=METHOD_NOT_FOUND, message="Method not found", data="resources/list")
190217

191218

192219
@pytest.mark.anyio
@@ -196,7 +223,7 @@ async def test_runner_non_spec_method_with_no_handler_raises_method_not_found(se
196223
async with connected_runner(server) as (client, _):
197224
with pytest.raises(MCPError) as exc:
198225
await client.send_raw_request("nonexistent/method", None)
199-
assert exc.value.error.code == METHOD_NOT_FOUND
226+
assert exc.value.error == ErrorData(code=METHOD_NOT_FOUND, message="Method not found", data="nonexistent/method")
200227

201228

202229
@pytest.mark.anyio

0 commit comments

Comments
 (0)