MCP: Order SSE handshake before early tool calls - #2273
SohamKukreti wants to merge 1 commit into
Conversation
Gate the SSE inbox so initialize is processed before concurrent tools/call POSTs, fixing -32602 rejections on connect (#2233).
There was a problem hiding this comment.
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/initializedwaits 2 s on its first calls. After 2 s the gate forwards the held messages anyway. - A
pingsent beforeinitializeis 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.runhas 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.
Summary
Fixes #2233
Over
/mcp/sse, every client message is a separate HTTP POST to/mcp/messages. A fan-out client that firesinitializeand severaltools/callrequests in the same burst can land a tool call in the session inbox beforeinitialize. The MCP session rejects it with-32602 "Invalid request parameters", and the Docker log printsReceived 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_gatebetween the SSE inbox and the MCP server. It forwardsinitializeandnotifications/initializedfirst, 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_donelatch, which forwards the first frame without checking what it is. On SSE the first frame is often atools/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_gateand 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 realSessionMessageobjects 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.tools/callposted before the handshake come out afterinitializeandnotifications/initialized, in arrival order (also fails against a PR fix(mcp): gate SSE session until initialize completes聽#2234-style gate).Live, against
unclecode/crawl4ai:0.9.3with the patchedmcp_bridge.pycopied into the container (the image's copy is byte-identical todevelop):tools/callPOSTed beforeinitialize, 3 trialsinitializePOSTed first, 3 trialsmcpPython SDK client over SSE: 2 sequential and 5 concurrent sessions, all list 7 tools and get a realmdresult.before initializationwarnings and 0 tracebacks.pingbeforeinitialize,notifications/initializedbeforeinitialize, 150 calls beforeinitialize(all 151 replies OK), disconnect while parked, disconnect afterinitialize. All handled with no tracebacks from the gate./mdand/mcp/schemaunaffected.Pre-existing and unrelated:
tests/test_issue_1594_mcp_sse.py::test_sse_handler_is_raw_asgigreps for a function name that was replaced in #1850 and fails ondeveloptoo.Checklist: