Skip to content

Commit a29f66b

Browse files
fix(session): return INVALID_REQUEST error instead of crashing on pre-init requests
When an MCP client sends a request (e.g. tools/list) before completing the initialize handshake, the server now responds with a proper INVALID_REQUEST JSON-RPC error instead of raising a RuntimeError that propagates through the anyio task group and crashes the ASGI application. This affects real-world clients such as Cursor, MCP Inspector, and anything-llm that skip re-initialization after a server restart or dropped SSE connection. Github-Issue: #423
1 parent cf4e435 commit a29f66b

File tree

2 files changed

+74
-2
lines changed

2 files changed

+74
-2
lines changed

src/mcp/server/session.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ async def handle_list_prompts(ctx: RequestContext, params) -> ListPromptsResult:
5050
RequestResponder,
5151
)
5252
from mcp.shared.version import SUPPORTED_PROTOCOL_VERSIONS
53+
from mcp.types import INVALID_REQUEST, ErrorData
5354

5455

5556
class InitializationState(Enum):
@@ -192,7 +193,14 @@ async def _received_request(self, responder: RequestResponder[types.ClientReques
192193
pass
193194
case _:
194195
if self._initialization_state != InitializationState.Initialized:
195-
raise RuntimeError("Received request before initialization was complete")
196+
with responder:
197+
await responder.respond(
198+
ErrorData(
199+
code=INVALID_REQUEST,
200+
message="Received request before initialization was complete",
201+
)
202+
)
203+
return
196204

197205
async def _received_notification(self, notification: types.ClientNotification) -> None:
198206
# Need this to avoid ASYNC910

tests/server/test_session.py

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from mcp.shared.message import SessionMessage
1414
from mcp.shared.session import RequestResponder
1515
from mcp.types import (
16+
INVALID_REQUEST,
1617
ClientNotification,
1718
CompletionsCapability,
1819
InitializedNotification,
@@ -489,4 +490,67 @@ async def mock_client():
489490
tg.start_soon(mock_client)
490491

491492
assert error_response_received
492-
assert error_code == types.INVALID_PARAMS
493+
494+
495+
@pytest.mark.anyio
496+
async def test_request_before_initialization_returns_error():
497+
"""Test that sending a request before initialize returns a proper JSON-RPC error.
498+
499+
This reproduces the crash reported in GitHub issue #423, where MCP clients
500+
(e.g. Cursor, MCP Inspector) send requests such as tools/list immediately
501+
after a server restart, without first completing the initialize handshake.
502+
503+
Previously the server raised RuntimeError which propagated through the anyio
504+
task group and crashed the ASGI application. Now it must respond with an
505+
INVALID_REQUEST JSON-RPC error and keep running.
506+
"""
507+
server_to_client_send, server_to_client_receive = anyio.create_memory_object_stream[SessionMessage](10)
508+
client_to_server_send, client_to_server_receive = anyio.create_memory_object_stream[SessionMessage | Exception](10)
509+
510+
error_code_received: int | None = None
511+
error_message_received: str | None = None
512+
client_done = anyio.Event()
513+
514+
async def run_server():
515+
async with ServerSession(
516+
client_to_server_receive,
517+
server_to_client_send,
518+
InitializationOptions(
519+
server_name="test-server",
520+
server_version="1.0.0",
521+
capabilities=ServerCapabilities(),
522+
),
523+
):
524+
# The error response is sent directly inside _received_request
525+
# without reaching incoming_messages. Keep the session alive until
526+
# the mock client has received the error and signals completion.
527+
with anyio.fail_after(5):
528+
await client_done.wait()
529+
530+
async def mock_client():
531+
nonlocal error_code_received, error_message_received
532+
533+
# Send tools/list WITHOUT any prior initialize handshake
534+
await client_to_server_send.send(SessionMessage(types.JSONRPCRequest(jsonrpc="2.0", id=1, method="tools/list")))
535+
536+
# Expect a JSON-RPC error response, not a crash
537+
with anyio.fail_after(5):
538+
response = await server_to_client_receive.receive()
539+
540+
assert isinstance(response.message, types.JSONRPCError)
541+
error_code_received = response.message.error.code
542+
error_message_received = response.message.error.message
543+
client_done.set()
544+
545+
async with (
546+
client_to_server_send,
547+
client_to_server_receive,
548+
server_to_client_send,
549+
server_to_client_receive,
550+
anyio.create_task_group() as tg,
551+
):
552+
tg.start_soon(run_server)
553+
tg.start_soon(mock_client)
554+
555+
assert error_code_received == INVALID_REQUEST
556+
assert error_message_received == "Received request before initialization was complete"

0 commit comments

Comments
 (0)