Skip to content

Commit 130e160

Browse files
committed
fix: ServerRunner error shapes match existing server (METHOD_NOT_FOUND, pre-init)
METHOD_NOT_FOUND message is now bare "Method not found" (no method suffix); the interaction suite pins that. The pre-init gate now returns the generic INVALID_PARAMS / "Invalid request parameters" / data="" shape. The existing server has no dedicated pre-init check; the request dies in ClientRequest validation, so clients see this shape. TODO(maxisbey) marked. Also: loosen the gap-8 _meta wire test to be tracer-agnostic (it was order-dependent on the SpanCapture fixture).
1 parent e9ee4b4 commit 130e160

3 files changed

Lines changed: 18 additions & 13 deletions

File tree

src/mcp/server/runner.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
from mcp.shared.exceptions import MCPError
3535
from mcp.shared.transport_context import TransportContext
3636
from mcp.types import (
37-
INVALID_REQUEST,
37+
INVALID_PARAMS,
3838
LATEST_PROTOCOL_VERSION,
3939
METHOD_NOT_FOUND,
4040
Implementation,
@@ -164,13 +164,13 @@ async def _on_request(
164164
if method == "initialize":
165165
return self._handle_initialize(params)
166166
if not self._initialized and method not in _INIT_EXEMPT:
167-
raise MCPError(
168-
code=INVALID_REQUEST,
169-
message=f"Received {method!r} before initialization was complete",
170-
)
167+
# TODO(maxisbey): pinned compat. The existing server has no
168+
# dedicated pre-init check; the request dies in ClientRequest
169+
# validation, so the client sees the generic invalid-params shape.
170+
raise MCPError(code=INVALID_PARAMS, message="Invalid request parameters", data="")
171171
entry = self.server.get_request_handler(method)
172172
if entry is None:
173-
raise MCPError(code=METHOD_NOT_FOUND, message=f"Method not found: {method}")
173+
raise MCPError(code=METHOD_NOT_FOUND, message="Method not found")
174174
# ValidationError propagates; the dispatcher's exception boundary maps
175175
# it to INVALID_PARAMS.
176176
typed_params = entry.params_type.model_validate(params or {})

tests/server/test_runner.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,12 @@
2424
from mcp.shared.exceptions import MCPError
2525
from mcp.types import (
2626
INTERNAL_ERROR,
27-
INVALID_REQUEST,
27+
INVALID_PARAMS,
2828
LATEST_PROTOCOL_VERSION,
2929
METHOD_NOT_FOUND,
3030
CallToolRequestParams,
3131
ClientCapabilities,
32+
ErrorData,
3233
Implementation,
3334
InitializeRequestParams,
3435
ListToolsResult,
@@ -142,7 +143,7 @@ async def test_runner_gates_requests_before_initialize(server: SrvT):
142143
async with connected_runner(server, initialized=False) as (client, _):
143144
with pytest.raises(MCPError) as exc:
144145
await client.send_raw_request("tools/list", None)
145-
assert exc.value.error.code == INVALID_REQUEST
146+
assert exc.value.error == ErrorData(code=INVALID_PARAMS, message="Invalid request parameters", data="")
146147
# ping is exempt from the gate
147148
assert await client.send_raw_request("ping", None) == {}
148149

@@ -361,7 +362,7 @@ async def test_otel_middleware_records_error_status_on_mcp_error(server: SrvT, s
361362
assert exc.value.error.code == METHOD_NOT_FOUND
362363
[span] = spans.finished()
363364
assert span.status.status_code == StatusCode.ERROR
364-
assert span.status.description == "Method not found: nonexistent/method"
365+
assert span.status.description == "Method not found"
365366
# MCPError is a protocol-level response, not a crash - no traceback event.
366367
assert not [e for e in span.events if e.name == "exception"]
367368

tests/shared/test_jsonrpc_dispatcher.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -419,11 +419,15 @@ async def noop_progress(progress: float, total: float | None, message: str | Non
419419
with anyio.fail_after(5):
420420
await client.send_raw_request("a", None)
421421
await client.send_raw_request("b", {"x": 1, "_meta": {"k": "v"}}, opts)
422-
assert seen[0] == {"_meta": {}}
423-
assert seen[1] is not None
424-
assert seen[1]["x"] == 1
422+
# `_meta` is always present. Its contents depend on the active otel
423+
# tracer (traceparent/tracestate may be injected), so assert presence
424+
# and that anything beyond W3C keys is exactly what we expect.
425+
w3c = {"traceparent", "tracestate"}
426+
assert seen[0] is not None and seen[0].keys() == {"_meta"}
427+
assert set(seen[0]["_meta"].keys()) <= w3c
428+
assert seen[1] is not None and seen[1]["x"] == 1
429+
assert set(seen[1]["_meta"].keys()) - w3c == {"k", "progressToken"}
425430
assert seen[1]["_meta"]["k"] == "v"
426-
assert "progressToken" in seen[1]["_meta"]
427431

428432

429433
@pytest.mark.anyio

0 commit comments

Comments
 (0)