|
13 | 13 | from mcp.shared.message import SessionMessage |
14 | 14 | from mcp.shared.session import RequestResponder |
15 | 15 | from mcp.types import ( |
| 16 | + INVALID_REQUEST, |
16 | 17 | ClientNotification, |
17 | 18 | CompletionsCapability, |
18 | 19 | InitializedNotification, |
@@ -489,4 +490,67 @@ async def mock_client(): |
489 | 490 | tg.start_soon(mock_client) |
490 | 491 |
|
491 | 492 | 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