diff --git a/src/content/changelog/agents/2026-07-13-mcp-client-elicitation.mdx b/src/content/changelog/agents/2026-07-13-mcp-client-elicitation.mdx
new file mode 100644
index 00000000000..a5f94937c0a
--- /dev/null
+++ b/src/content/changelog/agents/2026-07-13-mcp-client-elicitation.mdx
@@ -0,0 +1,69 @@
+---
+title: Agents can respond to MCP elicitation requests
+description: Agents SDK MCP clients can now handle form and URL elicitation requests from connected servers.
+products:
+ - agents
+ - workers
+date: 2026-07-13
+---
+
+import { PackageManagers, TypeScriptExample } from "~/components";
+
+Agents connected to Model Context Protocol (MCP) servers with [`addMcpServer`](/agents/model-context-protocol/apis/client-api/) can now handle [elicitation](https://modelcontextprotocol.io/specification/2025-11-25/client/elicitation) requests.
+
+Elicitation lets an MCP server request user input while it handles a tool call. Form mode collects structured, non-sensitive data. URL mode asks for consent before opening an out-of-band flow, such as third-party authorization or payment.
+
+```mermaid
+sequenceDiagram
+ participant User
+ participant Agent as Agent (MCP client)
+ participant Server as MCP server
+ participant Browser
+
+ Server->>Agent: elicitation/create
+ Agent->>User: Show server, reason, and input or URL
+ User->>Agent: Submit, open, decline, or cancel
+ Agent->>Browser: Open URL after consent (URL mode)
+ Agent->>Server: accept, decline, or cancel
+ Server-->>Agent: Optional URL completion notification
+```
+
+Register a handler for each mode your Agent supports in `onStart()`:
+
+
+
+```ts
+import { Agent } from "agents";
+import type { ElicitRequest, ElicitResult } from "agents/mcp";
+
+export class MyAgent extends Agent {
+ onStart() {
+ this.mcp.configureElicitationHandlers({
+ form: (request, serverId) => this.forwardToUser(request, serverId),
+ url: (request, serverId) => this.forwardToUser(request, serverId),
+ });
+ }
+
+ private forwardToUser(
+ request: ElicitRequest,
+ serverId: string,
+ ): Promise {
+ // Show the request in your UI and resolve after the user responds.
+ throw new Error(
+ `Implement elicitation for ${serverId}: ${request.params.message}`,
+ );
+ }
+}
+```
+
+
+
+Connections advertise only the modes with configured handlers. An Agent without handlers advertises no elicitation capability, which lets the server use its fallback. The SDK stores the advertised modes with each MCP server registration so they survive Durable Object hibernation. Callback functions remain in memory and reattach when `onStart()` runs.
+
+For implementation details and a browser forwarding pattern, refer to [MCP client elicitation](/agents/model-context-protocol/apis/client-api/#elicitation). The [`mcp-client`](https://github.com/cloudflare/agents/tree/main/examples/mcp-client) and [`mcp-elicitation`](https://github.com/cloudflare/agents/tree/main/examples/mcp-elicitation) examples implement both sides.
+
+## Upgrade
+
+To update to this release:
+
+
diff --git a/src/content/docs/agents/model-context-protocol/apis/agent-api.mdx b/src/content/docs/agents/model-context-protocol/apis/agent-api.mdx
index 99b11c141e9..c4dbf2ccef4 100644
--- a/src/content/docs/agents/model-context-protocol/apis/agent-api.mdx
+++ b/src/content/docs/agents/model-context-protocol/apis/agent-api.mdx
@@ -257,171 +257,124 @@ export class MyMCP extends McpAgent {
-## Elicitation (human-in-the-loop)
+## Elicitation
-MCP servers can request additional user input during tool execution using **elicitation**. The MCP client (like Claude Desktop) renders a form based on your JSON Schema and returns the user's response.
+[MCP elicitation](https://modelcontextprotocol.io/specification/2025-11-25/client/elicitation) lets a server request user input while handling another request, such as a tool call. The current stable MCP specification defines two modes:
-### When to use elicitation
+- **Form mode** collects structured, non-sensitive data through the client.
+- **URL mode** sends the user to an out-of-band interaction, such as third-party authorization or payment.
-- Request structured input that was not part of the original tool call
-- Confirm high-stakes operations before proceeding
-- Gather additional context or preferences mid-execution
+The client must advertise support for a mode before the server sends it.
-### `elicitInput(options, context)`
+### Form mode
-Request structured input from the user during tool execution.
+Call `this.server.server.elicitInput()` in a tool handler. Pass `extra.requestId` as `relatedRequestId` so the response returns on the stream for the originating tool call:
-**Parameters:**
+
-| Parameter | Type | Description |
-| -------------------------- | ----------- | -------------------------------------------- |
-| `options.message` | string | Message explaining what input is needed |
-| `options.requestedSchema` | JSON Schema | Schema defining the expected input structure |
-| `context.relatedRequestId` | string | The `extra.requestId` from the tool handler |
+```ts
+const result = await this.server.server.elicitInput(
+ {
+ mode: "form",
+ message: "By how much do you want to increase the counter?",
+ requestedSchema: {
+ type: "object",
+ properties: {
+ amount: {
+ type: "number",
+ title: "Amount",
+ minimum: 1,
+ maximum: 100,
+ },
+ },
+ required: ["amount"],
+ },
+ },
+ { relatedRequestId: extra.requestId },
+);
-**Returns:** `Promise<{ action: "accept" | "decline", content?: object }>`
+if (result.action !== "accept" || !result.content) {
+ return { content: [{ type: "text", text: "Counter unchanged." }] };
+}
-
+const amount = Number(result.content.amount);
+```
-```ts
-import { McpAgent } from "agents/mcp";
-import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
-import { z } from "zod";
+
-type State = { counter: number };
+For backwards compatibility, form requests may omit `mode: "form"`. The schema supports flat objects with primitive fields. Do not use form mode to request passwords, API keys, access tokens, payment credentials, or other secrets.
-export class CounterMCP extends McpAgent {
- server = new McpServer({
- name: "counter-server",
- version: "1.0.0",
- });
+### URL mode
- initialState: State = { counter: 0 };
+Use URL mode for interactions that must happen outside the MCP client. The request includes a message, the URL, and a unique `elicitationId`:
- async init() {
- this.server.tool(
- "increase-counter",
- "Increase the counter by a user-specified amount",
- { confirm: z.boolean().describe("Do you want to increase the counter?") },
- async ({ confirm }, extra) => {
- if (!confirm) {
- return { content: [{ type: "text", text: "Cancelled." }] };
- }
-
- // Request additional input from the user
- const userInput = await this.server.server.elicitInput(
- {
- message: "By how much do you want to increase the counter?",
- requestedSchema: {
- type: "object",
- properties: {
- amount: {
- type: "number",
- title: "Amount",
- description: "The amount to increase the counter by",
- },
- },
- required: ["amount"],
- },
- },
- { relatedRequestId: extra.requestId },
- );
-
- // Check if user accepted or cancelled
- if (userInput.action !== "accept" || !userInput.content) {
- return { content: [{ type: "text", text: "Cancelled." }] };
- }
-
- // Use the input
- const amount = Number(userInput.content.amount);
- this.setState({
- ...this.state,
- counter: this.state.counter + amount,
- });
+
- return {
- content: [
- {
- type: "text",
- text: `Counter increased by ${amount}, now at ${this.state.counter}`,
- },
- ],
- };
- },
- );
- }
+```ts
+const elicitationId = crypto.randomUUID();
+const result = await this.server.server.elicitInput(
+ {
+ mode: "url",
+ message: "Connect your account to continue.",
+ url: `https://example.com/connect?elicitationId=${elicitationId}`,
+ elicitationId,
+ },
+ { relatedRequestId: extra.requestId },
+);
+
+if (result.action !== "accept") {
+ return { content: [{ type: "text", text: "Connection cancelled." }] };
}
+
+return {
+ content: [
+ {
+ type: "text",
+ text: "Connection page opened. Complete it in your browser.",
+ },
+ ],
+};
```
-### JSON Schema for forms
+For URL mode, `accept` means the user consented to open the URL. It does not mean the external interaction finished. The server may later send `notifications/elicitation/complete` with the same `elicitationId`.
-The `requestedSchema` defines the form structure shown to the user:
+Do not put secrets, personal information, or a pre-authenticated protected-resource URL in `url`. Production servers should use HTTPS. Bind each request to the authenticated user, and verify that the same user completes the external flow.
-```ts
-const schema = {
- type: "object",
- properties: {
- // Text input
- name: {
- type: "string",
- title: "Name",
- description: "Enter your name",
- },
- // Number input
- amount: {
- type: "number",
- title: "Amount",
- minimum: 1,
- maximum: 100,
- },
- // Boolean (checkbox)
- confirm: {
- type: "boolean",
- title: "I confirm this action",
- },
- // Enum (dropdown)
- priority: {
- type: "string",
- enum: ["low", "medium", "high"],
- title: "Priority",
- },
- },
- required: ["name", "amount"],
-};
-```
+### Handle responses
-### Handling responses
+Both modes return one of three actions:
+
+| Action | Meaning |
+| --------- | ----------------------------------------------------------------- |
+| `accept` | The user submitted the form or consented to open the URL. |
+| `decline` | The user explicitly rejected the request. |
+| `cancel` | The user dismissed the request without making an explicit choice. |
+
+Accepted form responses include `content` that matches `requestedSchema`. URL responses omit `content`. Decline and cancel responses typically omit it.
```ts
-const result = await this.server.server.elicitInput(
- { message: "Confirm action", requestedSchema: schema },
- { relatedRequestId: extra.requestId },
-);
-
switch (result.action) {
case "accept":
- // User submitted the form
- const { name, amount } = result.content as { name: string; amount: number };
- // Process the input...
+ // For form mode, validate and process result.content.
break;
-
case "decline":
- // User cancelled
- return { content: [{ type: "text", text: "Operation cancelled." }] };
+ return { content: [{ type: "text", text: "Request declined." }] };
+ case "cancel":
+ return { content: [{ type: "text", text: "Request dismissed." }] };
}
```
:::note[MCP client support]
-Elicitation requires MCP client support. Not all MCP clients implement the elicitation capability. Check the client documentation for compatibility.
+Not all MCP clients implement elicitation. Check the client before depending on it and provide a fallback when appropriate. Agents acting as MCP clients can handle both modes through [MCP client elicitation handlers](/agents/model-context-protocol/apis/client-api/#elicitation).
:::
-For more human-in-the-loop patterns including workflow-based approval, refer to [Human-in-the-loop patterns](/agents/concepts/agentic-patterns/human-in-the-loop/).
+For more human-in-the-loop patterns, refer to [Human-in-the-loop patterns](/agents/concepts/agentic-patterns/human-in-the-loop/).
## Next steps
diff --git a/src/content/docs/agents/model-context-protocol/apis/client-api.mdx b/src/content/docs/agents/model-context-protocol/apis/client-api.mdx
index 9e788905866..507e5020751 100644
--- a/src/content/docs/agents/model-context-protocol/apis/client-api.mdx
+++ b/src/content/docs/agents/model-context-protocol/apis/client-api.mdx
@@ -314,6 +314,189 @@ export class MyAgent extends Agent {
+## Elicitation
+
+[MCP elicitation](https://modelcontextprotocol.io/specification/2025-11-25/client/elicitation) lets a server request user input while handling another request, such as a tool call. The current stable MCP specification defines form and URL modes.
+
+Register a handler for each mode your Agent supports in `onStart()`:
+
+
+
+```ts
+import { Agent } from "agents";
+import type { ElicitRequest, ElicitResult } from "agents/mcp";
+
+class MyAgent extends Agent {
+ onStart() {
+ this.mcp.configureElicitationHandlers({
+ form: (request, serverId) =>
+ this.forwardElicitationToBrowser(request, serverId),
+ url: (request, serverId) =>
+ this.forwardElicitationToBrowser(request, serverId),
+ });
+ }
+
+ private forwardElicitationToBrowser(
+ request: ElicitRequest,
+ serverId: string,
+ ): Promise {
+ // Forward the request to your UI and resolve after the user responds.
+ // A complete implementation appears in Forward elicitation to a UI.
+ throw new Error(
+ `Implement elicitation for ${serverId}: ${request.params.message}`,
+ );
+ }
+}
+```
+
+
+
+The `serverId` identifies the connection that sent the request. Use it to tell the user which server is asking for input and to apply server-specific policy.
+
+### Capability negotiation and hibernation
+
+At the MCP `initialize` handshake, a connection advertises only the modes with configured handlers. A form-only handler advertises form mode. A URL-only handler advertises URL mode. A connection without handlers advertises no elicitation capability, which lets the server use its fallback.
+
+The SDK stores the advertised modes with each server registration. A connection restored after Durable Object hibernation can therefore advertise the same modes when it reconnects. Callback functions remain in memory and reattach when `onStart()` runs.
+
+You can explicitly narrow the advertised modes when adding a server:
+
+
+
+```ts
+await this.addMcpServer("portal", "https://portal.example.com/mcp", {
+ client: {
+ capabilities: {
+ elicitation: { form: {} },
+ },
+ },
+});
+```
+
+
+
+An explicit `client.capabilities.elicitation` value takes precedence over handler-derived modes and persists with the server registration. Do not advertise a mode without a matching handler. If the server sends that mode, the connection returns an error because it cannot handle the request.
+
+### Form mode
+
+Form mode collects structured, non-sensitive data in the client. The request includes a restricted JSON Schema in `requestedSchema`. If the user submits the form, return `action: "accept"` with matching `content`:
+
+
+
+```ts
+this.mcp.configureElicitationHandlers({
+ form: async (request) => {
+ const content = await showFormToUser(request.params.requestedSchema);
+ return content ? { action: "accept", content } : { action: "cancel" };
+ },
+});
+```
+
+
+
+Allow the user to review and edit values before submission. Validate accepted content against `requestedSchema`. Do not use form mode to request passwords, API keys, access tokens, payment credentials, or other secrets.
+
+### URL mode
+
+URL mode asks the user to open an external page. Use it for out-of-band interactions that may collect secrets, such as third-party authorization or payment. Keep the URL in the dedicated elicitation path and out of model-visible messages and tool-result text.
+
+A URL handler should:
+
+1. Show which MCP server sent the request.
+2. Show the request message, target host, and full URL.
+3. Ask for consent before opening the URL.
+4. Open the page in a browser context the Agent and model cannot inspect.
+5. Return `action: "accept"` without `content` after consent.
+6. Offer distinct decline and cancel controls.
+
+Do not prefetch the URL or its metadata. Treat the URL as untrusted input. Production servers should send HTTPS URLs.
+
+For URL mode, `accept` means the user consented to open the URL. It does not mean the external interaction finished. A server may later send `notifications/elicitation/complete` with the request `elicitationId`.
+
+### Response actions
+
+Both modes support three actions:
+
+| Action | Meaning |
+| --------- | ----------------------------------------------------------------- |
+| `accept` | The user submitted the form or consented to open the URL. |
+| `decline` | The user explicitly rejected the request. |
+| `cancel` | The user dismissed the request without making an explicit choice. |
+
+Include `content` only for an accepted form response. Omit it for URL, decline, and cancel responses.
+
+### Forward elicitation to a UI
+
+A handler returns a promise, but the response often comes from a browser. Broadcast the request to connected clients, then resolve the promise through a `@callable` method:
+
+
+
+```ts
+import { Agent, callable } from "agents";
+import type { ElicitRequest, ElicitResult } from "agents/mcp";
+
+type PendingResolver = {
+ resolve: (result: ElicitResult) => void;
+ timeout: ReturnType;
+};
+
+class MyAgent extends Agent {
+ private pendingElicitations = new Map();
+
+ onStart() {
+ this.mcp.configureElicitationHandlers({
+ form: (request, serverId) => this.forward(request, serverId),
+ url: (request, serverId) => this.forward(request, serverId),
+ });
+ }
+
+ private forward(
+ request: ElicitRequest,
+ serverId: string,
+ ): Promise {
+ const id = crypto.randomUUID();
+
+ const result = new Promise((resolve) => {
+ const timeout = setTimeout(() => {
+ if (this.pendingElicitations.delete(id)) {
+ resolve({ action: "cancel" });
+ }
+ }, 55_000);
+ this.pendingElicitations.set(id, { resolve, timeout });
+ });
+
+ this.broadcast(
+ JSON.stringify({
+ type: "mcp-elicitation",
+ id,
+ serverId,
+ params: request.params,
+ }),
+ );
+
+ return result;
+ }
+
+ @callable()
+ respondToElicitation(id: string, result: ElicitResult) {
+ const pending = this.pendingElicitations.get(id);
+ if (!pending) return;
+
+ this.pendingElicitations.delete(id);
+ clearTimeout(pending.timeout);
+ pending.resolve(result);
+ }
+}
+```
+
+
+
+The example uses a 55-second timeout because MCP SDK requests default to 60 seconds. If your client call sets a longer request timeout, adjust this timeout to finish first.
+
+Refer to the [`mcp-client` example](https://github.com/cloudflare/agents/tree/main/examples/mcp-client) for the browser implementation. The [`mcp-elicitation` example](https://github.com/cloudflare/agents/tree/main/examples/mcp-elicitation) is a server that sends both modes.
+
+To send elicitation requests from an MCP server, refer to [`elicitInput`](/agents/model-context-protocol/apis/agent-api/#elicitinputoptions-context).
+
## Using MCP capabilities
Once connected, access the server's capabilities: