Skip to content

Commit 05183d9

Browse files
fix(server): return a descriptive error for requests before initialization
Fixes #423.
1 parent 77001c9 commit 05183d9

3 files changed

Lines changed: 34 additions & 11 deletions

File tree

‎src/mcp/server/runner.py‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@
2828
CLIENT_INFO_META_KEY,
2929
CORE_RESULT_TYPES,
3030
INTERNAL_ERROR,
31-
INVALID_PARAMS,
3231
INVALID_REQUEST,
3332
METHOD_NOT_FOUND,
3433
PROTOCOL_VERSION_META_KEY,
@@ -209,8 +208,15 @@ async def _inner(ctx: ServerRequestContext[LifespanT, Any]) -> HandlerResult:
209208
if entry is None:
210209
raise MCPError(code=METHOD_NOT_FOUND, message="Method not found", data=method)
211210
if not self.connection.initialize_accepted and method not in _INIT_EXEMPT:
212-
# Pinned compat: the same error shape the union validation produced.
213-
raise MCPError(code=INVALID_PARAMS, message="Invalid request parameters", data="")
211+
# A request before the handshake is a lifecycle error, not a
212+
# parameter error: answer -32600 with the reason, so a client
213+
# on a stale session (e.g. after a server restart) learns it
214+
# must (re-)run `initialize` instead of debugging its params.
215+
raise MCPError(
216+
code=INVALID_REQUEST,
217+
message="Received request before initialization was complete",
218+
data="The client must send an 'initialize' request before any other request.",
219+
)
214220
# Absent params validate as {} (required fields still reject), so
215221
# the handler receives the model with its defaults, never None.
216222
typed_params = entry.params_type.model_validate({} if params is None else params, by_name=False)

‎tests/interaction/lowlevel/test_initialize.py‎

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
import pytest
1313
from inline_snapshot import snapshot
1414
from mcp_types import (
15-
INVALID_PARAMS,
15+
INVALID_REQUEST,
1616
CallToolResult,
1717
ClientCapabilities,
1818
CompletionsCapability,
@@ -228,11 +228,12 @@ async def list_roots(context: ClientRequestContext) -> types.ListRootsResult:
228228

229229
@requirement("lifecycle:requests-before-initialized")
230230
async def test_request_before_initialization_is_rejected() -> None:
231-
"""A feature request sent before the handshake completes is rejected; ping is exempt.
231+
"""A feature request sent before the handshake completes is rejected with a
232+
descriptive error; ping is exempt.
232233
233234
Client always initializes on entry, so this drives a bare ClientSession that never sends
234-
initialize. The server's stated reason for the rejection never reaches the client: the error
235-
is reported as a generic invalid-params failure.
235+
initialize. The rejection names the missing handshake (-32600), so a client on a stale
236+
session learns it must (re-)run `initialize` instead of debugging its params.
236237
"""
237238

238239
async def list_tools(ctx: ServerRequestContext, params: types.PaginatedRequestParams | None) -> ListToolsResult:
@@ -253,7 +254,11 @@ async def list_tools(ctx: ServerRequestContext, params: types.PaginatedRequestPa
253254
pong = await session.send_ping()
254255

255256
assert exc_info.value.error == snapshot(
256-
ErrorData(code=INVALID_PARAMS, message="Invalid request parameters", data="")
257+
ErrorData(
258+
code=INVALID_REQUEST,
259+
message="Received request before initialization was complete",
260+
data="The client must send an 'initialize' request before any other request.",
261+
)
257262
)
258263
assert pong == snapshot(EmptyResult())
259264

‎tests/server/test_runner.py‎

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,11 @@ async def test_runner_gates_requests_before_initialize(server: SrvT):
208208
async with connected_runner(server, initialized=False) as (client, _):
209209
with pytest.raises(MCPError) as exc:
210210
await client.send_raw_request("tools/list", None)
211-
assert exc.value.error == ErrorData(code=INVALID_PARAMS, message="Invalid request parameters", data="")
211+
assert exc.value.error == ErrorData(
212+
code=INVALID_REQUEST,
213+
message="Received request before initialization was complete",
214+
data="The client must send an 'initialize' request before any other request.",
215+
)
212216
# ping is exempt from the gate
213217
assert await client.send_raw_request("ping", None) == {}
214218

@@ -249,7 +253,11 @@ async def greet(ctx: Ctx, params: RequestParams | None) -> Any:
249253
async with connected_runner(server, initialized=False) as (client, _):
250254
with pytest.raises(MCPError) as exc:
251255
await client.send_raw_request("custom/greet", None)
252-
assert exc.value.error == ErrorData(code=INVALID_PARAMS, message="Invalid request parameters", data="")
256+
assert exc.value.error == ErrorData(
257+
code=INVALID_REQUEST,
258+
message="Received request before initialization was complete",
259+
data="The client must send an 'initialize' request before any other request.",
260+
)
253261

254262

255263
@pytest.mark.anyio
@@ -632,7 +640,11 @@ async def reject_initialize(ctx: Ctx, call_next: Any) -> Any:
632640
assert exc.value.error.message == "rejected by middleware"
633641
with pytest.raises(MCPError) as gate_exc:
634642
await client.send_raw_request("tools/list", None)
635-
assert gate_exc.value.error == ErrorData(code=INVALID_PARAMS, message="Invalid request parameters", data="")
643+
assert gate_exc.value.error == ErrorData(
644+
code=INVALID_REQUEST,
645+
message="Received request before initialization was complete",
646+
data="The client must send an 'initialize' request before any other request.",
647+
)
636648
# ping passes through the middleware untouched
637649
assert await client.send_raw_request("ping", None) == {}
638650
assert runner.connection.initialize_accepted is False

0 commit comments

Comments
 (0)