Conversation
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
When several callers share one MCP session and reuse JSON-RPC ids, a
tools/callresult can reach the wrong caller.StreamableHTTPServerTransport.sendForRequest(agents 0.17.3, patched inpatches/agents@0.17.3.patch) picks the target stream by JSON-RPC id. It prefers the stream that carried the request only while that stream's connection is live. The patch ends that connection early on purpose: the POST bridge closes it when a write to the client fails, and a Last-Event-ID resume supersedes it. After that, the message goes to:-32603 Internal error. A stream whose only open request had that id then closes, so its own result has nowhere to go later.The MCP spec says a requestor must not reuse an id within a session, so this needs a client that pools callers on one session. We have seen callers of an executor.sh endpoint get another caller's result, and we suspect that kind of client. We could not see the server side, so the reproduction below is our evidence for the mechanism.
This change keeps a request's messages on the stream that carried it: the GET that resumed it, else its POST connection, else that stream's own event log. The async context is trusted only while that stream still owns the id. Otherwise routing falls back to the id lookup, as before. So a related message sent after the stream's final response still goes by id.
Reproduction: local workerd, agents 0.17.3 with this repo's patch, MCP SDK 1.29.0 clients. Several clients share one session, and each numbers its ids from 0. In the first two rows and the control,
enable_request_signallets the bridge see the hang-up locally. The third row uses this repo's own compatibility flags.-32603, 40 own, 11 abandoned-32603A caller starts its next call 1 s after its last one ends, so fast errors let the "before" run make more calls. We can share the harness if that helps.
packages/hosts/cloudflare/src/mcp/agents-request-routing.test.tspins this against the real patched transport. 4 of its 6 cases fail on main. The package suite (132 tests), typecheck, oxlint, and oxfmt pass.Separate issue, not changed here: responses replayed onto a fresh GET
A GET without
Last-Event-IDgets the stored POST responses replayed onto it (replayUndeliveredResponsesOnFreshGet). The MCP spec (2025-11-25, Transports, Listening for Messages from the Server) says: "The server MUST NOT send a JSON-RPC response on the stream unless resuming a stream associated with a previous client request." On a shared session, a client that opens a fresh GET while its own call is pending can have that call resolved by another caller's stored result, or by the session'sinitializeresult. The same harness shows this 3/3, before and after this change. The replay is a deliberate at-least-once fallback, so we left it alone. If you want it tightened, we can send a follow-up, for example one that skips a stored response while another in-flight stream still holds its id.The same routing code is in cloudflare/agents
packages/agents/src/mcp/server/transport.ts(0.24.0 and main).