Skip to content

MCP: Order SSE handshake before early tool calls - #2273

Open
SohamKukreti wants to merge 1 commit into
developfrom
fix/2233-mcp-sse-init-gate
Open

SohamKukreti wants to merge 1 commit into
developfrom
fix/2233-mcp-sse-init-gate

Conversation

@SohamKukreti

Copy link
Copy Markdown
Collaborator

Summary

Fixes #2233

Over /mcp/sse, every client message is a separate HTTP POST to /mcp/messages. A fan-out client that fires initialize and several tools/call requests in the same burst can land a tool call in the session inbox before initialize. The MCP session rejects it with -32602 "Invalid request parameters", and the Docker log prints Received request before initialization was complete. The arguments were fine; the session simply was not initialized yet. The WebSocket transport never hits this because one socket preserves the client's send order.

This PR adds _sse_init_gate between the SSE inbox and the MCP server. It forwards initialize and notifications/initialized first, parks every other message, and flushes the parked messages in arrival order once the handshake is through. If the handshake never arrives within 2 seconds it flushes anyway, so a misbehaving client cannot pin the session. If the client disconnects mid-handshake the parked messages are dropped instead of being pushed into a closed session.

Well-behaved clients are unaffected: nothing is parked and replies still go straight to write_stream. The WebSocket transport, the schema endpoint and all tools are untouched.

Note on PR #2234: that change copies the WebSocket init_done latch, which forwards the first frame without checking what it is. On SSE the first frame is often a tools/call, so it is forwarded, rejected, and its error reply is what opens the latch. Tested live, it leaves the failure rate at 3/3. This PR gates on message type instead of position.

List of files changed and why

  • deploy/docker/mcp_bridge.py - Adds _sse_init_gate and routes the SSE handler's inbound stream through it. 37 lines added, 1 changed. The WebSocket path is not touched.
  • tests/test_issue_2233_mcp_sse.py - New unit tests that feed real SessionMessage objects through the gate and assert the forwarded order.

How Has This Been Tested?

Unit tests (pytest tests/test_issue_2233_mcp_sse.py): 4 tests, all pass with the fix. Each was checked to fail when the behaviour it guards is removed.

  1. Three tools/call posted before the handshake come out after initialize and notifications/initialized, in arrival order (also fails against a PR fix(mcp): gate SSE session until initialize completes聽#2234-style gate).
  2. A well-behaved client's messages pass through unchanged.
  3. With no handshake and an open inbox, parked calls are released only by the timeout.
  4. A client that disconnects while calls are parked gets nothing flushed.

Live, against unclecode/crawl4ai:0.9.3 with the patched mcp_bridge.py copied into the container (the image's copy is byte-identical to develop):

Scenario Before After
3 tools/call POSTed before initialize, 3 trials 3/3 rejected 0/3 rejected
initialize POSTed first, 3 trials 0/3 0/3
Clean handshake, 3 trials 0/3 0/3
  • Official mcp Python SDK client over SSE: 2 sequential and 5 concurrent sessions, all list 7 tools and get a real md result.
  • Claude Code connected as an MCP client, listed tools and ran 2 tool calls. Log shows 0 before initialization warnings and 0 tracebacks.
  • Edge cases: ping before initialize, notifications/initialized before initialize, 150 calls before initialize (all 151 replies OK), disconnect while parked, disconnect after initialize. All handled with no tracebacks from the gate.
  • /md and /mcp/schema unaffected.

Pre-existing and unrelated: tests/test_issue_1594_mcp_sse.py::test_sse_handler_is_raw_asgi greps for a function name that was replaced in #1850 and fails on develop too.

Checklist:

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • I have added/updated unit tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes

Gate the SSE inbox so initialize is processed before concurrent tools/call POSTs, fixing -32602 rejections on connect (#2233).
@SohamKukreti SohamKukreti changed the title Order SSE handshake before early tool calls MCP: Order SSE handshake before early tool calls Sep 16, 2026

@c4-crew-bot c4-crew-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A read of this PR. It is a comment, not an approval.

What it fixes
#2233: over SSE, a client that sends tool calls right after initialize gets -32602 Received request before initialization was complete. The new _sse_init_gate in deploy/docker/mcp_bridge.py forwards initialize and notifications/initialized first. It holds every other message and forwards the held messages in arrival order after the handshake. The server session refuses requests until notifications/initialized arrives, so the gate waits for both messages.

Size and tests
2 files, +132 -1, 1 commit, base develop. mcp_bridge.py +37 -1. tests/test_issue_2233_mcp_sse.py is new and has 4 unit tests: reorder, pass-through, timeout, disconnect. They need no browser and no network.

Overlap
#2234 fixes the same issue against main. It copies the WebSocket latch, which forwards the first frame whatever that frame is. This PR says it tested #2234 live and saw 3 failures in 3 runs. Only one of the two should land.

Risk
Low. Only the SSE path changes.

  • A client that never sends notifications/initialized waits 2 s on its first calls. After 2 s the gate forwards the held messages anyway.
  • A ping sent before initialize is also held until the handshake completes.
  • If the client disconnects during the handshake, the held messages are dropped.
  • The memory stream between the gate and mcp.run has a buffer of 100 messages.

Not run
I did not run the tests or a live MCP client. I did not check the claim that tests/test_issue_1594_mcp_sse.py::test_sse_handler_is_raw_asgi also fails on develop.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant