You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
MCPClientManager: mid-session OAuth re-authorization (insufficient_scope step-up) is never surfaced, and its callback is dropped as stale #2658
An MCP server connected through MCPClientManager (via agent.addMcpServer()) is ready. A tool call then needs a scope the grant lacks, and the server answers 403 with WWW-Authenticate: Bearer error="insufficient_scope", scope="...". A 401 whose token refresh fails behaves the same way.
The SDK transport handles the challenge correctly. It runs auth(), builds a new authorization URL (granted scopes plus the challenged ones), passes it to the provider (authProvider.authUrl), and throws UnauthorizedError.
MCPClientManager only turns that into authenticating plus authUrl at connect time (connectToServer → persistAuthContinuation). Mid-session, callTool() lets the error through:
The URL is never shown. The connection stays ready, auth_url is not saved, and the caller gets a plain UnauthorizedError.
The callback is dropped. If the user opens the URL anyway (it is on the provider), handleCallbackRequest() finds the connection in an auth-accepted state (ready). It treats the callback as a stale popup, consumes the state and returns success without exchanging the code. The new scopes are never saved, so the retry fails the same way.
Real case: the Cloudflare API MCP server (mcp.cloudflare.com) granted with a Minimal or Read-only template. The first write tool call hits this.
Repro
A fake OAuth MCP server (protected-resource metadata, AS metadata, DCR, token endpoint) passed as transport.fetch to a real MCPClientManager in a Durable Object:
registerServer + connectToServer → authenticating. Callback with a base-scope code → ready.
callTool on a tool that needs workers_scripts:write → the server returns 403 insufficient_scope.
Expected: something the app can show (the new authUrl), and a callback that completes the flow. Actual: UnauthorizedError: Unauthorized, connection still ready, auth_url null. A callback for the provider's new URL is ignored as stale.
The repo's own conformance client patches this in test code: requestReauth() in packages/agents/conformance/worker.ts sets conn.connectionState = AUTHENTICATING by hand after an unauthorized tool call.
Dodo (an agent app on agents) wraps callTool the same way, plus a call to the private persistAuthContinuation().
Proposed fix
In callTool() (and readResource() / getPrompt()): on an unauthorized error where the provider now holds a newauthUrl, move the connection to authenticating, call persistAuthContinuation(), fire onServerStateChanged, and throw a typed MCPAuthorizationRequiredError { serverId, authUrl, clientId } (with the original error as cause, so isUnauthorized() checks still match). The existing callback path then works unchanged. PR to follow.
callTool(), readResource() and getPrompt() take over only when all of these are true: the request failed as unauthorized, the provider now holds a newauthUrl whose state is still redeemable, and the connection is ready, connected or authenticating. A connection that is processing a callback is left alone.
When they take over, they call persistAuthContinuation(), move the connection to authenticating, and throw MCPAuthorizationRequiredError { serverId, authUrl, clientId }. That error extends the SDK's UnauthorizedError and keeps its name, so existing checks still match.
handleCallbackRequest() needs no change: once the connection is authenticating, the existing path exchanges the code.
src/tests/mcp/mid-session-reauth.test.ts uses a fake OAuth MCP server through transport.fetch. It covers a 403 step-up, a 401 with a failed refresh, two calls at once, and four cases that must rethrow the original error. The workers MCP suite passes 634/634.
Includes a docs section and a patch changeset.
A collaborator is welcome to open it as a PR, or to cherry-pick it.
Activity
jonnyparris commentedon Oct 22, 2026
beep-boop-🤖 A fix with tests is ready on a fork branch. I can't open the PR: this repo only accepts PRs from collaborators.
Branch: https://github.com/jonnyparris/agents/tree/fix/mcp-midsession-reauth
Compare: main...jonnyparris:agents:fix/mcp-midsession-reauth
Commit: jonnyparris/agents@718e54d
Summary:
callTool(),readResource()andgetPrompt()take over only when all of these are true: the request failed as unauthorized, the provider now holds a newauthUrlwhosestateis still redeemable, and the connection isready,connectedorauthenticating. A connection that is processing a callback is left alone.persistAuthContinuation(), move the connection toauthenticating, and throwMCPAuthorizationRequiredError { serverId, authUrl, clientId }. That error extends the SDK'sUnauthorizedErrorand keeps itsname, so existing checks still match.handleCallbackRequest()needs no change: once the connection isauthenticating, the existing path exchanges the code.src/tests/mcp/mid-session-reauth.test.tsuses a fake OAuth MCP server throughtransport.fetch. It covers a 403 step-up, a 401 with a failed refresh, two calls at once, and four cases that must rethrow the original error. TheworkersMCP suite passes 634/634.A collaborator is welcome to open it as a PR, or to cherry-pick it.
fix(mcp): close step-up gaps — flag SDK upgrade stamps, test connecte…
fix(mcp): re-authorization and connection failures for the MCP client
fix(mcp): re-authorization and connection failures for the MCP client (…