Skip to content

fix(mcp): make server-initiated sends work without agent ALS context#1734

Open
whoiskatrin wants to merge 2 commits into
mainfrom
fix/mcp-transport-agent-ref
Open

fix(mcp): make server-initiated sends work without agent ALS context#1734
whoiskatrin wants to merge 2 commits into
mainfrom
fix/mcp-transport-agent-ref

Conversation

@whoiskatrin

@whoiskatrin whoiskatrin commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Fixes #1490

Problem

A McpAgent that dispatches tool execution through a Worker Loader child isolate (e.g. @cloudflare/codemode sandboxes) cannot perform server-initiated MCP requests (elicitInput, createMessage, listRoots) from a host-side callback the child invokes via RPC. The transport throws:

Error: Agent was not found in send

and the tool call fails with no useful output to the client.

Root cause

StreamableHTTPServerTransport.send resolved its agent from AsyncLocalStorage (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 sessionId from the agent). It now also stores the agent reference, and sendStandalone, sendForRequest, and close use that reference instead of getCurrentAgent(). Server-initiated sends now work regardless of how the calling code was reached — no user-facing API changes.

sendForRequest still 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.md now covers it. (An earlier revision of this PR exported a runWithAgentContext helper 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 / replayEvents keep using ALS: they are inherently request-scoped (they need the live connection, which can only come from the current invocation).
  • McpSSETransport already captures its agent via closure at construction — no change needed.
  • RPCServerTransport and WorkerTransport don't use getCurrentAgent() at all.

Testing

New outside-context-send.test.ts plus two test tools that simulate the cross-isolate RPC callback by running this.server.server.elicitInput(...) inside agentContext.exit(...) — which strips the ALS store exactly like a fresh entrypoint invocation does. (Note: the test tools must use the SDK path server.server.elicitInput, not McpAgent.elicitInput, because agent methods are auto-wrapped and would mask the bug.)

  • request-scoped elicit (relatedRequestIdsendForRequest) round-trips on the originating POST stream
  • standalone elicit (no relatedRequestIdsendStandalone) is delivered on the standalone GET stream

Verified 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.

…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-bot

changeset-bot Bot commented Jun 11, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 6a74ede

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
agents Patch

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

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no bugs or issues to report.

Open in Devin Review

@pkg-pr-new

pkg-pr-new Bot commented Jun 11, 2026

Copy link
Copy Markdown

Open in StackBlitz

agents

npm i https://pkg.pr.new/agents@1734

@cloudflare/ai-chat

npm i https://pkg.pr.new/@cloudflare/ai-chat@1734

@cloudflare/codemode

npm i https://pkg.pr.new/@cloudflare/codemode@1734

create-think

npm i https://pkg.pr.new/create-think@1734

hono-agents

npm i https://pkg.pr.new/hono-agents@1734

@cloudflare/shell

npm i https://pkg.pr.new/@cloudflare/shell@1734

@cloudflare/think

npm i https://pkg.pr.new/@cloudflare/think@1734

@cloudflare/voice

npm i https://pkg.pr.new/@cloudflare/voice@1734

@cloudflare/worker-bundler

npm i https://pkg.pr.new/@cloudflare/worker-bundler@1734

commit: 6a74ede

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.
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.

agentContext ALS not preserved in Worker-Loader-child→host RPC callbacks; server-initiated MCP requests throw "Agent was not found in send"

1 participant