Skip to content

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

Merged
mattzcarey merged 18 commits into
mainfrom
fix/mcp-client-reauthorization
Nov 5, 2026
Merged

mattzcarey merged 18 commits into
mainfrom
fix/mcp-client-reauthorization

Conversation

@mattzcarey

@mattzcarey mattzcarey commented Oct 30, 2026 •

Copy link
Copy Markdown
Member

Fixes #2658, #2822 and #2824. 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 401 from a server with no OAuth provider now says the server rejected the request: check the credentials, or pass callbackHost if the server uses OAuth. It used to say "OAuth configuration incomplete: missing authUrl" (A 401 from a server without an OAuth provider reports "OAuth configuration incomplete: missing authUrl" #2824).
  • transport.headers is applied when the HTTP transport is built, so registerServer() on a standalone manager and restored connections send it. Before, only addMcpServer() did, because it converted the headers itself (MCPClientManager.registerServer() persists transport.headers but never sends them #2822).

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. cf_agents_mcp_servers gains three nullable columns, added the first time they're used: failure, authorization_rounds and pending_step_up. The base table definition doesn't change. 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 (717). 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.

@changeset-bot

changeset-bot Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c75bc9d

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

This PR includes changesets to release 3 packages
Name Type
@cloudflare/ai-chat Patch
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: c75bc9d

@agent-think

agent-think Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

🔴 agents import sizes: 1 entry point over threshold

Entry point Exports Largest gzip change Size now
🔴 agents/harness/pi 5 resized up to +179 B (+13.61%) 1.5 KiB
🟡 agents/mcp/client 1 resized, 2 new +4.2 KiB (+2.57%) 167.9 KiB
🟡 agents/mcp 15 resized up to +4 KiB (+1.1%) 365.7 KiB
🟡 agents 5 resized up to +4 KiB (+1.48%) 272.3 KiB
🟡 agents/chat-sdk 3 resized up to +3.9 KiB (+1.45%) 274.1 KiB
🟡 agents/browser 3 resized, 1 new up to +3.3 KiB (+6.61%) 53.1 KiB
🟡 agents/models/pi-ai 1 resized +1.8 KiB (+1.12%) 163.2 KiB
🟡 agents/browser/ai-sdk 1 resized +1004 B (+6.38%) 16.4 KiB
🟡 agents/browser/tanstack-ai 1 resized +984 B (+3.1%) 31.9 KiB
🟢 agents/websearch/pi 1 resized -19.2 KiB (-84.11%) 3.6 KiB
🟢 agents/webfetch/pi 1 resized -19.1 KiB (-69.7%) 8.3 KiB
🟢 agents/browser/pi 1 resized -18.3 KiB (-51.89%) 17 KiB
Changed exports (41)
Import Gzip change Size now
🔴 agents/harness/pi#PiSessions +179 B (+13.61%) 1.5 KiB
🟡 agents/mcp/client#MCPClientManager +4.2 KiB (+2.57%) 167.9 KiB
🟡 agents/mcp#WorkerTransport +4 KiB (+1.1%) 365.7 KiB
🟡 agents#getCurrentAgent +4 KiB (+1.48%) 272.3 KiB
🟡 agents#StreamingResponse +4 KiB (+1.47%) 272.3 KiB
🟡 agents#Agent +4 KiB (+1.47%) 272.3 KiB
🟡 agents#DEFAULT_AGENT_STATIC_OPTIONS +3.9 KiB (+1.47%) 272.3 KiB
🟡 agents#routeAgentEmail +3.9 KiB (+1.46%) 272.6 KiB
🟡 agents/chat-sdk#ChatSdkStateAgent +3.9 KiB (+1.45%) 274.1 KiB
🟡 agents/mcp#createLegacyMcpHandler +3.9 KiB (+1%) 396.2 KiB
🟡 agents/mcp#experimental_createMcpHandler +3.9 KiB (+1%) 396.4 KiB
🟡 agents/chat-sdk#createChatSdkState +3.9 KiB (+1.43%) 274.9 KiB
🟡 agents/mcp#createMcpHandler +3.9 KiB (+0.96%) 409.1 KiB
🟡 agents/chat-sdk#ChatSdkStateAdapter +3.9 KiB (+1.43%) 274.8 KiB
🟡 agents/mcp#StreamableHTTPEdgeClientTransport +3.8 KiB (+1.07%) 362 KiB
🟡 agents/mcp#getMcpAuthContext +3.8 KiB (+1.07%) 361.9 KiB
🟡 agents/mcp#MCP_SERVER_ID_MAX_LENGTH +3.8 KiB (+1.07%) 361.9 KiB
🟡 agents/mcp#McpAgent +3.8 KiB (+1.07%) 361.9 KiB
🟡 agents/mcp#ElicitRequestSchema +3.8 KiB (+1.07%) 361.9 KiB
🟡 agents/mcp#RPCServerTransport +3.8 KiB (+1.07%) 361.9 KiB
🟡 agents/mcp#SSEEdgeClientTransport +3.8 KiB (+1.07%) 362 KiB
🟡 agents/mcp#DurableObjectEventStore +3.8 KiB (+1.07%) 361.9 KiB
🟡 agents/mcp#RPC_DO_PREFIX +3.8 KiB (+1.06%) 361.9 KiB
🟡 agents/mcp#RPCClientTransport +3.8 KiB (+1.06%) 361.9 KiB
🟡 agents/mcp#normalizeServerId +3.8 KiB (+1.06%) 361.9 KiB
🟡 agents/browser#Browser +3.3 KiB (+6.61%) 53.1 KiB
🟡 agents/browser#BrowserSession +2.4 KiB (+5.72%) 45.2 KiB
🟡 agents/models/pi-ai#createAI +1.8 KiB (+1.12%) 163.2 KiB
🟡 agents/browser#CodemodeRuntime +1.7 KiB (+4.43%) 39.5 KiB
🟡 agents/browser/ai-sdk#browserTool +1004 B (+6.38%) 16.4 KiB
🟡 agents/browser/tanstack-ai#browserTool +984 B (+3.1%) 31.9 KiB
🟡 agents/harness/pi#openPiSessionStore +343 B (+4.28%) 8.2 KiB
🆕 agents/mcp/client#MCPAuthorizationRequiredError — 74.8 KiB
🆕 agents/mcp/client#MCPConnectionNotFoundError — 74.8 KiB
🆕 agents/browser#conversationScope — 36 KiB
🟢 agents/harness/pi#PiSession -49.4 KiB (-97.02%) 1.5 KiB
🟢 agents/harness/pi#PiHarness -48.9 KiB (-80.01%) 12.2 KiB
🟢 agents/harness/pi#skills -19.3 KiB (-87.75%) 2.7 KiB
🟢 agents/websearch/pi#webSearchTool -19.2 KiB (-84.11%) 3.6 KiB
🟢 agents/webfetch/pi#webFetchTool -19.1 KiB (-69.7%) 8.3 KiB
🟢 agents/browser/pi#browserTool -18.3 KiB (-51.89%) 17 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 → c75bc9d7 · workflow run · reported by agent-think[bot]

devin-ai-integration[bot]

This comment was marked as resolved.

ben-reitz and others added 14 commits November 2, 2026 15:16
* gardener: give pi-upgrade room to finish a minor upgrade

* gardener: keep pi-upgrade on its 9, 12 and 15 schedule

* gardener: run pi-upgrade at 9 and 15 so runs can't overlap; say piped checks keep their exit status
… resume handshake (#2805)

* fix(agents): make stop() cancel server turns streamed without a resume handshake

Fixes #2803

* docs(changeset): link request cancellation docs
* chore(agents): bump Pi to 1.5.0

* feat(agents): replace session.events() with session.watch()

Pi removed watchEvents() and its event types: a client renders the
conversation view's state instead of assembling it from events.
PiSession.watch(window?) passes Pi's conversation watch through, and
PiSession.entries() pages the transcript, the loader Pi's
ConversationTranscript takes.

The Pi and browser tool examples send the view, then Pi's operations,
and project the view with projectView() instead of folding events. The
channels example derives its session events by comparing each view with
the last.

* docs(agents): say how much streamed output a Pi crash can lose

Pi's next minor stores partial answers and running tool output within
settings.progress.persistWithinMs (default 1 s) instead of on every
partial commit.

* chore(agents): changeset for Pi 1.5

* feat(agents): drop retry() and parked operations

Pi no longer parks a task its restart limit cut off: a tool call ends
with an error result, a model request ends its run unanswered
(restart_limit), and an interrupted model request counts as a failed
attempt under settings.retry. Harness.retryTask() and the restart_limit
blocked reason are gone, so PiHarness.retry(), PiSession.retry() and the
`parked` pending status go too.

* docs(agents): use Pi's tagged reports and watch ends

Pi's onReport takes one Report tagged by kind, renames session_failed to
storage_failed, and tags a watch's end by kind. The recovery docs and
the examples' onReport callbacks follow.

* fix(agents): build Pi tool schemas with pi-schema

pi-ai no longer re-exports TypeBox (Type, Static, TSchema), and Pi
validates and types tool arguments with @earendil-works/pi-schema. Build
the browser, web_fetch, web_search and skill tool schemas with
pi-schema's builders (open objects, so the JSON matches what TypeBox
produced) and string enums with pi-ai's StringEnum, which gives
{ type: "string", enum } instead of an anyOf of consts. pi-schema joins
the optional Pi peers; the docs and examples use it for their tools.

* docs(agents): warn against rolling back below Pi 1.5

* feat(agents): refuse input on an idle Pi session with whenIdle

* docs(agents): finish moving the Pi docs to watch() and ended tasks

* fix(examples): show only the active context after a Pi reset

* fix(examples): settle Pi turns that finish between two watch values

* fix(examples): don't wait on a Pi turn submitted during the settle check
* feat(agents): continue a Pi session without a new message

* feat(agents): cap a Pi session's tool rounds with defaults.maxTurns

* chore(agents): changeset for Pi session controls
#2798)

* fix(agents): parse streamed chat-completions tool arguments once

Use pi-ai's ToolCallArgumentsStream for the chat-completions wire's
streamed tool arguments, kept off the content block, and its one full
parse at the end, which reports malformed arguments as argumentsError.
It replaces our own incremental parser (PartialArguments, #2740), now
that pi-ai ships one. Drop the responses wire's cleanup of scratch
fields pi no longer puts on blocks.

* test(agents): stream tool arguments with a __proto__ member through pi

pi-durable 1.4.0 merged a __proto__ member that a partial commit's
tool arguments gained into Object.prototype. Drive a harness with the
agents chat-completions wire over a binding that streams such arguments,
and check the isolate stays clean. Fails on 1.4.0, passes on Pi main.

* chore(agents): changeset for streamed tool arguments

* test(agents): assert the __proto__ test streams a partial that merges it
…#2801)

pi's code mode hands a script a schema-less tool's text only, so
tools.browser() resolved to a JSON string and a screenshot was dropped:
image(shot.result) threw. browserTool now declares a
structuredOutputSchema and returns the model projection of the run as
structuredOutput, with a screenshot as an image block in result. The
model's own output is unchanged.

The pi harness test fixture installs code mode with the inline QuickJS
sandbox (pi-codemode as a dev dependency), and a test runs a script
that screenshots, reads a value, and hits an error through
tools.browser.
* refactor(browser): move input validation and the default scope into their own modules

session.ts is about to import browser-functions.ts, which imported
validate() from session.ts. Moving validate() to validate.ts, and
DEFAULT_BROWSER_SCOPE to scope.ts, keeps the browser modules free of
import cycles. No behavior change.

* feat(browser): give BrowserSession the contract a Workspace module needs

- Browser.functionDocs() returns { functions, rules }: the browser.*
  docs for this browser's internal hosts, and the shared rules. The
  screenshot rule now says the call throws, not that the run fails.
- BrowserSession.functions is the browser.* table bound to the session,
  which browserTool now runs. Every entry, the hosts functions too,
  checks its input, then rejects once the session is closed and waits
  for the lock when the session locks.
- The report close() returns gains notices: the sentences the model
  reads, moved from browserTool so every caller writes the same ones.
- browser.session({ scope, lock: true }) holds the scope's lock, per
  BrowserSource, from its first call (or lock({ signal })) until
  close(), which releases it after the active tab is saved. Other
  locking sessions in the scope wait in order rather than fail. Each
  lock() call's signal bounds only that call's wait; the session
  leaves the queue once no call is still waiting, or when it closes,
  and an already-aborted signal leaves no queue behind.
  waitedForLockMs says how long it waited. Sessions without lock
  behave as before.

* feat(browser): export conversationScope() for pi's per-conversation scope

pi's browserTool already scoped each conversation as
conversation:<id>. Export the helper so app code (a Workspace module's
scope function, say) lands in the same tab, and use it in pi.ts. It
takes a string (pi.ts passes String(conversationId)), and throws on an
empty id instead of returning conversation:undefined.

* refactor(browser): build browserTool on BrowserSession's contract

Each run opens a session with lock: true and calls lock() before the
sandbox starts, so runs in one browser and scope still go one at a
time and the wait still doesn't count toward the timeout. It joins
the report's notices, so the description and notices read as before.
The tool's own queue and notice text are gone, and it now also waits
for any other locking session in its scope.

* docs(browser): document the session contract and add a changeset

* fix(browser): stop browserTool when its call is aborted

browserTool ignored the harness's abort signal, so a stopped chat left
its call waiting for the scope or running until the 60 s timeout, holding
the scope's lock. The pi, AI SDK and TanStack AI adapters now pass their
call's signal to the core. A call still waiting for the lock gives up
through session.lock({ signal }); a running one ends with an error result
the way the timeout does, and its session closes and frees the scope.

* chore(browser): release the session contract as a patch
* Version Packages

* docs(changelog): add 0.33.0 highlights and upgrade guide

* docs(changelog): correct the 0.33.0 upgrade notes

pi-schema's obj() rejects unlisted properties, unlike Type.Object;
the watch view keeps entries before resets and compactions, and an
idle pi.live keeps runFailure; defaults.maxTurns applies to new
sessions; describe the browserTool code mode change and add #2795.

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Ben Reitz <[email protected]>
…ects (#2807)

* feat(web): give pi code mode scripts web_search and web_fetch results as objects

pi's code mode hands a script a schema-less tool's text, so
tools.web_search() resolved to the numbered list the model reads and
tools.web_fetch() to a header line plus tagged content: a script had to
regex out result URLs and the next offset. Both pi tools now declare a
structuredOutputSchema and return structuredOutput:

- web_search: { query, items: [{ title, url, description?, lastModifiedDate? }] },
  descriptions trimmed as the model's view trims them.
- web_fetch: { url, status, contentType, title?, content, offset,
  nextOffset, totalChars }, with content still in the
  untrusted_web_content tags.

The model's output is unchanged; pi never sends structuredOutput to the
model. Failures stay error results without structuredOutput, which pi
turns into a rejection carrying the model's text.

A new pi harness fixture runs both tools over fake sources with code
mode's inline sandbox: a direct search, and a script that searches,
fetches the results in parallel, pages with nextOffset and catches a
failed fetch.

* fix(web): name the pi structured output helpers after structuredOutput, keep error imports small

- webSearchResultsForPrograms -> webSearchStructuredOutput and
  webFetchPageForPrograms -> webFetchStructuredOutput (types to match),
  using pi's own name for the field.
- Build the output schemas inside the tool factories, as the input
  schemas are, so importing only WebSearchError or WebFetchError no
  longer pulls them in (back to main's size).
- Docs: TypeScript fence for the search-then-fetch script; changeset
  links developers.cloudflare.com/agents/tools/web-fetch/.

* chore: drop the changeset link, and only ask for changeset links that add something

The pi web tools changeset's link pointed at a page without the new
section. AGENTS.md now asks for a docs link when the docs tell the
reader more than the changeset line does, and to skip it otherwise.
#2774)

* gardener: close issues merged work already resolved; upgrade to 0.1.22

* gardener: resolved-check reads every page of a pull request's changed files

---------

Co-authored-by: Ben Reitz <[email protected]>
Co-authored-by: Ben <[email protected]>
…ens (#2815)

* feat(examples): page the Pi example's transcript, re-watch after pi reopens, show compaction as a notice

The client merges every watch value into pi's ConversationTranscript, loads
earlier pages and missed entries over an entries request, and resumes a
reconnect after its newest entry; the server watches with pi's default
window. A watch that ends because pi closed is re-watched from where it
stopped. pi.compaction entries project as a summary notice.

* fix(examples): keep the Channels example compiling against the Pi example's transcript

Keeps projectEntries in the Pi examples' transcript.ts for the channels
example, which shows a compaction summary as a system message and skips
gaps, which entries never produce.

* fix(examples): forget the kept scroll offset once a page load ends

* docs(examples): say the Pi example compacts with pi's defaults

* fix(examples): follow new Pi messages only while the reader is at the bottom

* fix(examples): close the socket when a client's watch cannot open pi

* fix(examples): keep a compaction summary's headings small
…reopen fixes (#2816)

* feat(examples): page the browser example's transcript, re-watch after pi reopens, show compaction as a notice

Brings the Pi example's socket protocol, transcript mirror, and projection
to the browser tool example, with Load earlier, Load missing, and summary
notices in its feed.

* fix(examples): keep the browser example's transcript.ts in step with the Pi example's

Keeps projectEntries, as in the Pi example, so the shared files stay
identical.

* fix(examples): forget the kept scroll offset once a page load ends

* fix(examples): close the socket when a client's watch cannot open pi

* fix(examples): keep the browser example's scroll position when earlier messages load
* fix(ai-chat): stop a cancelled queued chat request from running

A cancel sent while a request waited in the queue found no abort controller
and was dropped, so the request ran anyway once it reached the front. Create
the controller when the request is accepted, and skip onChatMessage if it
was cancelled before its turn started.

Fixes #2701

* fix(ai-chat): keep a cancelled queued request from replacing the stored request context

The abort check ran after _setRequestContext, so a request cancelled while
queued still overwrote the body and client tools that tool continuations
and continueLastTurn read. Check before writing them.
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.
…; 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.
…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.
…jected 401 for what it is

Fixes #2822. Fixes #2824.

The connection applies transport.headers when it builds an HTTP transport
(requestInit for every request, eventSourceInit.fetch for the SSE stream),
so registerServer() and restored connections send them, not only
addMcpServer(). A 401 from a server with no OAuth provider now says the
server rejected the request and to check the credentials.
devin-ai-integration[bot]

This comment was marked as resolved.

…s a restart, and merge the SSE stream's own headers whatever their shape
@mattzcarey
mattzcarey merged commit d6b4441 into main Nov 5, 2026
26 checks passed
@mattzcarey
mattzcarey deleted the fix/mcp-client-reauthorization branch November 5, 2026 18:20
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

3 participants