Skip to content

fix(mcp): configure client elicitation handler#1911

Merged
mattzcarey merged 5 commits into
mainfrom
fix/mcp-configure-elicitation-handler
Jul 9, 2026
Merged

fix(mcp): configure client elicitation handler#1911
mattzcarey merged 5 commits into
mainfrom
fix/mcp-configure-elicitation-handler

Conversation

@mattzcarey

@mattzcarey mattzcarey commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Follow-up to #1903/#1910 (none of this has shipped — the pending Version Packages release absorbs all of it, so no compatibility path is kept).

What changed

Elicitation handling moves off the Agent class onto the MCP client manager, matching the configureOAuthCallback pattern. Agent.onElicitRequest is removed.

How to use

Register handlers in onStart(). The handler keys are the capability declaration: connections advertise exactly the modes with configured handlers at the initialize handshake — form only, url only, or both. No handlers → no elicitation capability advertised, so servers use their non-elicitation fallbacks.

class MyAgent extends Agent<Env> {
  onStart() {
    this.mcp.configureElicitationHandler({
      url: async (request, serverId) => {
        // deliver request.params.url to the user out-of-band
        return { action: "accept", content: {} };
      },
      form: async (request, serverId) => {
        // collect fields matching request.params.requestedSchema
        return { action: "accept", content: { /* fields */ } };
      },
    });
  }
}

An explicit client.capabilities.elicitation on addMcpServer still wins wholesale (persisted, survives hibernation), e.g. to declare form: { applyDefaults: true }.

Surviving hibernation: persisted capabilities

Handlers are functions and cannot be persisted, but the capabilities they advertise can. Whenever handlers are configured (and whenever a server is registered), the handler-derived client capabilities are recorded on each stored server row (server_options.capabilities, a general ClientCapabilities object — only elicitation is populated today, but future handler-driven capabilities such as sampling reuse the same field with no storage migration).

On wake, restored connections re-advertise the same modes at the initialize handshake before onStart() has run. Elicitation dispatch looks handlers up at request time, so when onStart() calls configureElicitationHandler, the handlers attach to the already-live connections — no reconnect needed. The seeding is self-contained in the manager: createConnection consumes the stamp from its own stored row, so every path that recreates a known server (storage restore, the Agent RPC restore, addMcpServer re-adds) gets it uniformly with no parameter threading.

The stamp is valid for one restore: consuming it clears it, and any configureElicitationHandler call re-stamps every row. Apps that configure handlers each wake see no difference; a deploy that deletes the configure call stops advertising the stale modes after a single wake instead of forever (adversarial review caught the forever-stale variant). Configuring handlers also clears the in-memory seed (handlers become the source of truth). Explicit client.capabilities declarations win over the seed, and reconnects rebuild the SDK client so handler changes on a live connection take effect at the next handshake.

Precedence at client build: client.capabilities.elicitation (declared) → handler-derived → persisted seed.

Lifecycle

Because restored connections no longer depend on handlers existing at connect time, automatic MCP restore keeps its original position: before fiber/chat recovery and user onStart(). Recovered turns (waitForMcpConnections) see the pending connections and run with MCP tools — an earlier revision of this PR moved restore after onStart() and traded that away; the persisted-capability seed removes the tradeoff. An end-to-end test replays the wake sequence and pins the ordering (mutation-tested: reordering restore after recovery fails it).

addMcpServer reuses the stored server id when re-adding a known server (requestedId ?? existingServer?.id ?? nanoid(8)), keeping registration idempotent during the onStart window and preserving OAuth token storage keyed by server id across hibernation.

Two windows remain by construction: an elicitation request arriving after a restored connection connects but before onStart() configures handlers (including during a recovered turn) is answered with a JSON-RPC error — servers handle elicitation failure per spec, and this equals main's behavior for form mode. And the first wake after handlers are first introduced has no stored record yet, so restored connections advertise nothing for that boot; the record is written during that wake and every later wake is correct.

Implementation notes

  • MCPClientConnection.configureElicitationHandler rebuilds the SDK client for not-yet-connected connections because SDK registerCapabilities is merge-only — a rebuild is the only way to un-advertise a mode when handlers are cleared before connecting (evaluated and rejected the registerCapabilities approach for exactly this reason). init() re-entry (mid-session 401 → OAuth → reconnect) also rebuilds, so reconnects genuinely refresh advertised capabilities.
  • migrateServerId rescopes handlers to the new server id; when the manager has no handlers it leaves the connection untouched so an id migration cannot wipe a restored capability seed (mutation-tested).
  • RPC elicitation tests use the connection-level elicitationHandlers option; conformance host and the mcp-client example configure via onStart().

Tests

Full agents suite (114 files / 2,267 tests), full MCP suite, npm run check, and all conformance jobs green. New coverage: seed precedence (seed vs handlers vs explicit declaration), capability persistence on stored rows, restore-then-configure across a simulated hibernation (the fiber-recovery window), a live RPC connection seeded at connect with handlers attached afterwards (full elicitation round-trip), and the wake-ordering test above. Verified end-to-end against the running mcp-client + mcp-elicitation examples: both form- and url-mode round-trips complete through configureElicitationHandler.

@changeset-bot

changeset-bot Bot commented Jul 9, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 456c3d6

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 changesets to release 2 packages
Name Type
agents Patch
@cloudflare/agent-think Patch

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

@pkg-pr-new

pkg-pr-new Bot commented Jul 9, 2026

Copy link
Copy Markdown

Open in StackBlitz

agents

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

@cloudflare/ai-chat

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

@cloudflare/codemode

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

create-think

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

hono-agents

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

@cloudflare/shell

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

@cloudflare/think

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

@cloudflare/voice

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

@cloudflare/worker-bundler

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

commit: 456c3d6

@mattzcarey mattzcarey force-pushed the fix/mcp-configure-elicitation-handler branch from ce77585 to c9b4418 Compare July 9, 2026 18:22
@mattzcarey mattzcarey marked this pull request as ready for review July 9, 2026 18:23
@mattzcarey mattzcarey requested a review from Copilot July 9, 2026 18:23

@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 potential bugs to report.

View in Devin Review to see 1 additional finding.

Open in Devin Review

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

Pull request overview

Updates the Agents SDK MCP client API so elicitation handling is configured on the MCP client manager (this.mcp) rather than via an Agent lifecycle hook, ensuring handlers configured in onStart() apply to restored connections before capability advertisement.

Changes:

  • Added MCPClientManager.configureElicitationHandler(...) (with connection-level reconfiguration for uninitialized clients) and exported MCPClientElicitationHandler.
  • Removed Agent.onElicitRequest, updated Agent startup ordering (run user onStart() before MCP restore), and adjusted dedup logic to reuse stored server IDs pre-restore.
  • Updated conformance worker, example app, docs, changesets, and MCP client tests to match the new elicitation API.

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
packages/agents/src/tests/mcp/http-dedup.test.ts Adds coverage for reusing stored HTTP server IDs before connection restore.
packages/agents/src/tests/mcp/client-elicitation.test.ts Updates/expands tests for manager-scoped elicitation handler behavior.
packages/agents/src/tests/agents/mcp.ts Adds an agent test helper for the “stored server without restored connection” scenario.
packages/agents/src/mcp/index.ts Re-exports the new MCPClientElicitationHandler type.
packages/agents/src/mcp/client.ts Implements manager-scoped elicitation configuration and wiring into connections.
packages/agents/src/mcp/client-connection.ts Adds connection-level handler reconfiguration for pre-initialized clients and updates messaging.
packages/agents/src/index.ts Removes onElicitRequest, adjusts Agent startup sequencing, and reuses stored server IDs when adding MCP servers.
packages/agents/conformance/worker.ts Migrates conformance agent to configure elicitation via this.mcp in onStart().
examples/mcp-client/src/server.ts Migrates example agent to configure elicitation via this.mcp in onStart().
examples/mcp-client/README.md Updates example documentation to reference configureElicitationHandler.
docs/agents/mcp-client.md Updates public docs to the new elicitation configuration flow and lifecycle ordering.
.changeset/no-elicitation-capability-without-handler.md Updates release notes to reflect handler configuration via this.mcp.
.changeset/mcp-client-elicitation-options.md Updates release notes for the new API and capability-advertisement semantics.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/agents/src/mcp/client-connection.ts
Comment thread packages/agents/src/tests/agents/mcp.ts
Comment thread docs/agents/mcp-client.md Outdated
Comment on lines 123 to 128
this.mcp.configureElicitationHandler(async (request, serverId) => {
// e.g. deliver a url-mode elicitation link out-of-band
return { action: "accept" as const, content: {} };
});
}
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Could we have different handlers for url and form would that make sense. Like grouped tho id like configure callback has custom something. I only want one function tho configureElicitationHandler

Comment thread examples/mcp-client/src/server.ts Outdated
Client.registerCapabilities is merge-only, so a rebuild is the only way
to un-advertise a mode when handlers are cleared before connecting.
Handlers are functions and cannot survive hibernation, but the capabilities
they advertise can: configuring handlers stamps them onto each stored server
row (server_options.capabilities), and the manager consumes the stamp as a
capability seed when recreating a known server. Restored connections
re-advertise the same modes at the handshake before onStart reconfigures the
handlers, so automatic MCP restore keeps its original position before
fiber/chat recovery and recovered turns see MCP tools.

The stamp is valid for one restore: any configure call re-stamps every row,
so a deploy that stops configuring handlers stops advertising stale modes
after a single wake. init() re-entry rebuilds the SDK client so reconnects
pick up handler changes. Seeding is self-contained in createConnection, so
storage restore, the Agent RPC restore, and addMcpServer re-adds all get it
uniformly.
@mattzcarey mattzcarey merged commit 0f47d61 into main Jul 9, 2026
7 checks passed
@mattzcarey mattzcarey deleted the fix/mcp-configure-elicitation-handler branch July 9, 2026 20:31
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.

2 participants