Skip to content

fix(mcp): surface mid-session re-authorization and stop serving dead auth links - #2660

Closed
mattzcarey wants to merge 6 commits into
mainfrom
fix/2658-mcp-midsession-reauth
Closed

mattzcarey wants to merge 6 commits into
mainfrom
fix/2658-mcp-midsession-reauth

Conversation

@mattzcarey

@mattzcarey mattzcarey commented Oct 22, 2026 •

Copy link
Copy Markdown
Member

When a request on a ready MCP server needs re-authorization, MCPClientManager now moves the server to authenticating, saves the new auth_url and throws MCPAuthorizationRequiredError. The OAuth callback for that link then exchanges the code instead of dropping it as a stale popup. On wake, a saved auth_url restores only while its state can still be redeemed, and a dead link is cleared so the server reconnects.

Fixes #2658. The SDK already built the step-up URL for a 403 insufficient_scope or a failed token refresh, but nobody saw it:

  • the server stayed ready and auth_url was never saved
  • the callback for that URL was consumed without a token exchange
  • the conformance worker patched connectionState by hand to get around it, and that patch is removed here

Other changes callers will see:

  • MCPAuthorizationRequiredError extends UnauthorizedError and keeps its name, so existing checks still match
  • callTool(), readResource() and getPrompt() reject with MCPConnectionNotFoundError for an unknown serverId, where they used to throw a TypeError
  • addMcpServer() returns { state: "authenticating", authUrl } when discovery gets a 401, and waits for a server that is still connecting instead of reporting it ready
  • requests that get a 401 at the same time now share one OAuth flow. Before, each registered its own client, so against a server that allows the handshake without a token the returned link could never complete, and two calls whose token expired together refreshed in parallel and lost the race on a rotating refresh token
import { MCPAuthorizationRequiredError } from "agents/mcp/client";

try {
  return await this.mcp.callTool({ serverId, name: "deploy", arguments: {} });
} catch (error) {
  if (error instanceof MCPAuthorizationRequiredError) {
    return { needsAuthorization: true, authUrl: error.authUrl };
  }
  throw error;
}

First of four stacked PRs, followed by #2661, #2662 and #2663. The first commit is @jonnyparris's fix from his fork, kept as he wrote it.

pnpm run check and the agents workers suite pass. The client conformance lanes pass without the old conformance patch.

@changeset-bot

changeset-bot Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 978d6f9

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a 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.

Note

Newer findings are available below. Devin Review posted a newer report on this PR, in addition to the findings presented here.

✅ Devin Review: No Issues Found

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

Devin Review

@agent-think

agent-think Bot commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

🟢 agents import sizes: 1 entry point changed, no growth

Entry point Exports Largest gzip change Size now
🆕 agents/mcp/client 2 new — 74.7 KiB
Changed exports (2)
Import Gzip change Size now
🆕 agents/mcp/client#MCPAuthorizationRequiredError — 74.7 KiB
🆕 agents/mcp/client#MCPConnectionNotFoundError — 74.7 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 → 978d6f95 · workflow run · reported by agent-think[bot]

@pkg-pr-new

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

Copy link
Copy Markdown

Open in StackBlitz

agents

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

@cloudflare/ai-chat

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

@cloudflare/codemode

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

hono-agents

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

@cloudflare/shell

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

@cloudflare/think

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

@cloudflare/voice

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

@cloudflare/worker-bundler

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

commit: 978d6f9

devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

@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 found 1 new potential issue.

Devin Review

Comment thread .changeset/mcp-sdk-2-4-cimd.md Resolved
jonnyparris and others added 6 commits October 29, 2026 19:24
…nager

A request on a ready connection can fail with 403 insufficient_scope
(scope step-up) or a 401 whose refresh fails. The SDK transport builds a
new authorization URL, stores it on the provider and throws
UnauthorizedError. MCPClientManager only surfaced auth URLs at connect
time, so the URL was never shown, and a callback for it was dropped as
stale because the connection was still ready.

callTool(), readResource() and getPrompt() now detect this (unauthorized
error plus a new provider authUrl), move the connection to
authenticating, persist auth_url, and throw MCPAuthorizationRequiredError
with authUrl. The normal OAuth callback then exchanges the code and
reconnects. Other errors are rethrown unchanged.
- connect, discovery and mid-session requests enter authenticating through
  enterAuthenticating, so auth_url, state and broadcast always agree
- addMcpServer returns authenticating when discovery needs OAuth, waits for
  in-flight connection work instead of reporting it ready, and rediscovers a
  connected-but-undiscovered server
- restore only trusts a persisted auth_url whose state can still be redeemed;
  otherwise it reconnects (fixes dead links and the callback crash window)
- callTool/readResource/getPrompt reject with MCPConnectionNotFoundError for
  an unknown serverId
- conformance worker drops its requestReauth state patch
When several requests got a 401 together, as discovery's parallel catalog
lists do against a server that allows the handshake without a token, each
started its own OAuth flow on the shared provider: five client registrations,
and a saved link whose code verifier belonged to another flow, so the
callback failed with "No code verifier found for OAuth state". A 401 from the
MCP endpoint now waits until the authorization an earlier one started has
built a link or saved tokens, so later flows reuse that client. Other
responses are not delayed.
… turns too

The SSE transport posts to a per-session message endpoint, not the connection URL, so matching on the URL let concurrent 401s there refresh in parallel. A request is MCP traffic when it carries a JSON-RPC message or opens an event stream; OAuth requests are neither.
Wrap the transport's onUnauthorized instead of inferring the end of a flow
from fetch responses and provider calls. A flow that failed outright used to
hold later 401s until a 10 second timeout; now the next flow starts as soon
as the previous one returns or throws. A flow that finds a newer access token
than the one it was called with skips authorizing, so two calls hitting an
expired token refresh once.
@mattzcarey

Copy link
Copy Markdown
Member Author

Replaced by #2782 (MCP client changes) and #2783 (examples).

@mattzcarey mattzcarey closed this Oct 30, 2026
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

2 participants