Skip to content

fix(mcp): re-authorization and connection failures for the MCP client - #2782

Open
mattzcarey wants to merge 3 commits into
mainfrom
fix/mcp-client-reauthorization
Open

mattzcarey wants to merge 3 commits into
mainfrom
fix/mcp-client-reauthorization

Conversation

@mattzcarey

@mattzcarey mattzcarey commented Oct 30, 2026 •

Copy link
Copy Markdown
Member

Fixes #2658. When an MCP server needs the user to authorize again partway through a session, the agent now says so. The request throws MCPAuthorizationRequiredError with a link, the server moves to authenticating, and clients see the link through getMcpServers(). Connection failures are recorded and stay put, the same as in Claude Code. A connect makes a few quick attempts, then the server is failed, across wakes too, until someone calls the new reconnectMcpServer(id).

Before this, a step-up (403 insufficient_scope) or a 401 whose refresh failed surfaced as a plain UnauthorizedError. The SDK had built a new authorization link that nobody saw, and its callback was ignored because the server was still ready. Failures weren't recorded, so every wake tried a failed server again, and addMcpServer() sometimes replaced a pending link without being asked.

Re-authorization:

  • OAuth flows take turns per connection, so concurrent 401s share one refresh or one link.
  • Re-authorization stops after three rounds for the same scopes within an hour, as the MCP authorization spec asks.
  • During a step-up, requests the current grant already covers keep working. If the user declines, the server goes back to ready, reconnecting on the grant it holds if the agent has woken since.
  • A request made before the first authorization throws MCPAuthorizationRequiredError with the pending link.
  • A server added without callbackHost that needs OAuth now says so, and says how to fix it.

Failures:

  • Each connect attempt is a connect plus discovery, with the server's retry options (3 attempts by default).
  • Refused authorization (403, OAuth errors such as invalid_target, a revoked static key, rejected registration) skips the quick attempts and is recorded as refused. A denied callback is recorded too, so a wake doesn't start a flow the user just declined.
  • addMcpServer() for a registered server reports its state as is: ready, the pending link, or the recorded failure. A waiting server keeps its link until the user completes it or reconnects. Whether an old link still works is up to the authorization server.
  • Nothing retries or expires on a timer.

Internally, MCPClientManager is now a thin facade over controllers for servers, authorization and shared status. Connection work runs in per-server lanes, so connect, discovery, callbacks and restore never interleave on one server. A server removed and added again under the same id waits for the old server's running work, so a late code exchange can't overwrite the new flow. The public API only gains reconnectMcpServer() and MCPAuthorizationRequiredError.

The integration tests run an Agent with its default transport against an OAuth-protected MCP server served through msw. Callbacks go through the worker, and the object restarts between steps. They cover the happy path, step-ups and the round limit, outages, refusals, removal and re-add races, and users who click old links, reopen callbacks, decline, or press Reconnect at odd times. pnpm run build and pnpm run check pass, and so do the MCP tests (713). This replaces stack #2660, #2661, #2662, #2663, #2751, #2758, #2759, #2760, #2761 and #2778. The example apps are in the PR stacked on this one.

Fixes #2658.

Re-authorization: a request that needs the user again (scope step-up, or
a 401 whose refresh fails) throws MCPAuthorizationRequiredError with a
link and moves the server to authenticating. OAuth flows take turns per
connection, re-authorization stops after three rounds for the same scopes
(MCP spec step-up limit), a declined step-up keeps the session on its
current grant, and a request made before the first authorization gets the
pending link.

Connection failures: each connect makes the server's quick attempts
(connect plus discovery), then records the failure. A failed server stays
failed across wakes until reconnectMcpServer(id); refused authorization
skips the attempts. addMcpServer() for a registered server reports its
state as is; a waiting server keeps its link until reconnected.

The manager is split into a facade and controllers (servers,
authorization, status), with per-server lanes for connection work.
Integration tests run an Agent against an OAuth MCP server served through
msw.
@changeset-bot

changeset-bot Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: d1be052

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

This PR includes changesets to release 2 packages
Name Type
agents Minor
@cloudflare/agent-think 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[bot]

This comment was marked as resolved.

@pkg-pr-new

pkg-pr-new Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

agents

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

@cloudflare/ai-chat

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

@cloudflare/codemode

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

hono-agents

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

@cloudflare/shell

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

@cloudflare/think

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

@cloudflare/voice

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

@cloudflare/worker-bundler

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

commit: d1be052

@agent-think

agent-think Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

🟡 agents import sizes: 4 entry points grew

Entry point Exports Largest gzip change Size now
🟡 agents/mcp/client 1 resized, 2 new +4 KiB (+2.46%) 167.7 KiB
🟡 agents/mcp 15 resized up to +3.9 KiB (+1.07%) 365.6 KiB
🟡 agents 5 resized up to +3.8 KiB (+1.43%) 272.2 KiB
🟡 agents/chat-sdk 3 resized up to +3.8 KiB (+1.4%) 273.9 KiB
Changed exports (26)
Import Gzip change Size now
🟡 agents/mcp/client#MCPClientManager +4 KiB (+2.46%) 167.7 KiB
🟡 agents/mcp#WorkerTransport +3.9 KiB (+1.07%) 365.6 KiB
🟡 agents#getCurrentAgent +3.8 KiB (+1.43%) 272.2 KiB
🟡 agents#StreamingResponse +3.8 KiB (+1.42%) 272.2 KiB
🟡 agents#Agent +3.8 KiB (+1.42%) 272.2 KiB
🟡 agents#DEFAULT_AGENT_STATIC_OPTIONS +3.8 KiB (+1.42%) 272.2 KiB
🟡 agents/chat-sdk#ChatSdkStateAgent +3.8 KiB (+1.4%) 273.9 KiB
🟡 agents/mcp#createLegacyMcpHandler +3.8 KiB (+0.96%) 396.1 KiB
🟡 agents#routeAgentEmail +3.8 KiB (+1.41%) 272.4 KiB
🟡 agents/mcp#experimental_createMcpHandler +3.8 KiB (+0.96%) 396.2 KiB
🟡 agents/chat-sdk#ChatSdkStateAdapter +3.8 KiB (+1.39%) 274.7 KiB
🟡 agents/chat-sdk#createChatSdkState +3.8 KiB (+1.39%) 274.7 KiB
🟡 agents/mcp#ElicitRequestSchema +3.7 KiB (+1.04%) 361.8 KiB
🟡 agents/mcp#RPCServerTransport +3.7 KiB (+1.04%) 361.8 KiB
🟡 agents/mcp#createMcpHandler +3.7 KiB (+0.92%) 409 KiB
🟡 agents/mcp#DurableObjectEventStore +3.7 KiB (+1.03%) 361.8 KiB
🟡 agents/mcp#MCP_SERVER_ID_MAX_LENGTH +3.7 KiB (+1.03%) 361.8 KiB
🟡 agents/mcp#StreamableHTTPEdgeClientTransport +3.7 KiB (+1.03%) 361.9 KiB
🟡 agents/mcp#RPC_DO_PREFIX +3.7 KiB (+1.03%) 361.8 KiB
🟡 agents/mcp#RPCClientTransport +3.7 KiB (+1.03%) 361.7 KiB
🟡 agents/mcp#McpAgent +3.7 KiB (+1.03%) 361.7 KiB
🟡 agents/mcp#normalizeServerId +3.7 KiB (+1.03%) 361.7 KiB
🟡 agents/mcp#SSEEdgeClientTransport +3.7 KiB (+1.03%) 361.8 KiB
🟡 agents/mcp#getMcpAuthContext +3.7 KiB (+1.03%) 361.8 KiB
🆕 agents/mcp/client#MCPAuthorizationRequiredError — 74.8 KiB
🆕 agents/mcp/client#MCPConnectionNotFoundError — 74.8 KiB
How this works

Each runtime export is bundled on its own, minified, and gzipped. Changes smaller than 100 B, or smaller than 1% and 1 KiB, are ignored. Growth over 10% or 5 KiB is marked 🔴. This report is informational and does not fail CI. The workflow artifact contains every measurement.

Compared be174922 → d1be0522 · workflow run · reported by agent-think[bot]

…; stale-session recovery keeps its link; a declined step-up survives a wake

- Work for a server added again waits for the removed server's running
  work, so a late code exchange cannot delete the new flow's discovery
  state, and it clears only its own tokens.
- Recovering a forgotten session records what discovery found, including
  a link when only the catalog needs OAuth.
- A declined step-up on a server that still holds tokens reconnects on
  that grant when the session it ran on is gone.
devin-ai-integration[bot]

This comment was marked as resolved.

…er a wake resumes only a step-up

The flag is stored with the link (pending_step_up), so a fresh sign-in
that the user declines is still recorded, whatever tokens are stored.
The resume starts from the callback handling itself, so standalone
callers of handleCallbackRequest() get it too.
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.

MCPClientManager: mid-session OAuth re-authorization (insufficient_scope step-up) is never surfaced, and its callback is dropped as stale

1 participant