fix(mcp): make server-initiated sends work without agent ALS context#1734
Open
whoiskatrin wants to merge 2 commits into
Open
fix(mcp): make server-initiated sends work without agent ALS context#1734whoiskatrin wants to merge 2 commits into
whoiskatrin wants to merge 2 commits into
Conversation
…1490) StreamableHTTPServerTransport recovered its agent from AsyncLocalStorage at send time, so server-initiated MCP requests (elicitInput, createMessage, listRoots) threw "Agent was not found in send" when issued from code with no agent context on its call stack — e.g. a host-side callback invoked via RPC from a Worker Loader child isolate (sandboxed tool execution / codemode), a service binding, or a queue consumer. ALS only propagates along the call tree of the original invocation; cross-isolate RPC arrives as a fresh entrypoint invocation with an empty store. The transport now captures its owning agent at construction (which already required agent context) and uses that reference in sendStandalone, sendForRequest, and close. The originating connection is still read from ALS in sendForRequest — it is a routing optimization only — but is now ignored when the store belongs to a different agent, so a foreign context can never influence routing. Also exports a stable runWithAgentContext(agent, fn) helper that re-enters an agent's context for arbitrary callbacks reached outside the invocation call tree, replacing the unsupported workaround of importing __DO_NOT_USE_WILL_BREAK__agentContext directly. Context entered this way carries no connection/request/email. Fixes #1490
🦋 Changeset detectedLatest commit: 6a74ede The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
agents
@cloudflare/ai-chat
@cloudflare/codemode
create-think
hono-agents
@cloudflare/shell
@cloudflare/think
@cloudflare/voice
@cloudflare/worker-bundler
commit: |
Discussed with Sunil and decided not to add a public API for re-entering the agent context — if users need to wrap things manually, that's a bug in our own wrapping logic to fix at the boundary. The transport fix covers MCP sends on its own, and for everything else routing callbacks through a public agent method (auto-wrapped) already works, so the docs now describe that pattern instead.
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.
Fixes #1490
Problem
A
McpAgentthat dispatches tool execution through a Worker Loader child isolate (e.g.@cloudflare/codemodesandboxes) cannot perform server-initiated MCP requests (elicitInput,createMessage,listRoots) from a host-side callback the child invokes via RPC. The transport throws:and the tool call fails with no useful output to the client.
Root cause
StreamableHTTPServerTransport.sendresolved its agent fromAsyncLocalStorage(getCurrentAgent()) at send time. ALS only propagates along the call tree of the original invocation — a child→host RPC arrives as a fresh entrypoint invocation with an empty store, so the lookup failed. The same applies to callbacks reached via service bindings, DO RPC, or queue consumers.The agent was never actually unreachable: the transport is created by and owned by its agent (same Durable Object, same lifetime) — it just looked the agent up the fragile way.
Fix
The transport constructor already runs inside agent context (it throws otherwise, and reads
sessionIdfrom the agent). It now also stores the agent reference, andsendStandalone,sendForRequest, andcloseuse that reference instead ofgetCurrentAgent(). Server-initiated sends now work regardless of how the calling code was reached — no user-facing API changes.sendForRequeststill reads the originating connection from ALS — but that's purely a routing optimization for disambiguating colliding request ids, with an existing graceful fallback when absent. It is now additionally ignored when the ALS store belongs to a different agent, so a foreign context (e.g. an agent-to-agent call) can never influence routing.For other context-dependent APIs in such callbacks (
getCurrentAgent()in arbitrary closures), the supported pattern is routing the callback through a public agent method — custom methods are auto-wrapped and re-enter the context. This was previously undocumented;docs/get-current-agent.mdnow covers it. (An earlier revision of this PR exported arunWithAgentContexthelper for this; after discussion we dropped it — if users need to wrap things manually it points at a gap in our own wrapping logic, which we'd rather fix at the boundary.)What I deliberately did NOT change
handleGetRequest/handlePostRequest/replayEventskeep using ALS: they are inherently request-scoped (they need the liveconnection, which can only come from the current invocation).McpSSETransportalready captures its agent via closure at construction — no change needed.RPCServerTransportandWorkerTransportdon't usegetCurrentAgent()at all.Testing
New
outside-context-send.test.tsplus two test tools that simulate the cross-isolate RPC callback by runningthis.server.server.elicitInput(...)insideagentContext.exit(...)— which strips the ALS store exactly like a fresh entrypoint invocation does. (Note: the test tools must use the SDK pathserver.server.elicitInput, notMcpAgent.elicitInput, because agent methods are auto-wrapped and would mask the bug.)relatedRequestId→sendForRequest) round-trips on the originating POST streamrelatedRequestId→sendStandalone) is delivered on the standalone GET streamVerified both tests fail with the exact issue error on unfixed code and pass with the fix. Full MCP suite (27 files, 492 tests), typecheck, oxlint, and oxfmt all pass.