From 77ffa84d4f240dfe3bf6b3a4d90cb03da2ad1118 Mon Sep 17 00:00:00 2001 From: Matt Carey Date: Thu, 9 Jul 2026 17:55:53 +0100 Subject: [PATCH 01/12] Agents: document MCP client elicitation and add url-elicitation changelog - New Elicitation section in the McpClient API docs: onElicitRequest, handler-driven capability advertisement (form+url with an override, form-only without), explicit capability narrowing, and the broadcast/respond pattern for forwarding elicitation to a UI - Cross-link from the server-side elicitInput docs - Changelog: URL elicitations are in the Agents SDK --- .../agents/2026-07-09-url-elicitation.mdx | 57 ++++++++ .../model-context-protocol/apis/agent-api.mdx | 2 +- .../apis/client-api.mdx | 122 ++++++++++++++++++ 3 files changed, 180 insertions(+), 1 deletion(-) create mode 100644 src/content/changelog/agents/2026-07-09-url-elicitation.mdx diff --git a/src/content/changelog/agents/2026-07-09-url-elicitation.mdx b/src/content/changelog/agents/2026-07-09-url-elicitation.mdx new file mode 100644 index 00000000000..5a8f182faa7 --- /dev/null +++ b/src/content/changelog/agents/2026-07-09-url-elicitation.mdx @@ -0,0 +1,57 @@ +--- +title: URL elicitations are in the Agents SDK +description: "Agents acting as MCP clients can now respond to elicitation requests, including url-mode elicitation for sensitive links like OAuth URLs, by overriding a single method." +products: + - agents + - workers +date: 2026-07-09 +--- + +import { PackageManagers, TypeScriptExample } from "~/components"; + +Agents connected to MCP servers with [`addMcpServer`](/agents/model-context-protocol/apis/client-api/) can now respond to [elicitation](https://modelcontextprotocol.io/specification/2025-11-25/client/elicitation) requests — including **url-mode elicitation**, the MCP specification's mechanism for sending sensitive URLs to the user. + +## Why url-mode elicitation + +Some tool calls need the user to visit a URL — an OAuth authorization page, a checkout, a verification link. Before url-mode elicitation, servers had to put those URLs into tool-result text, which lands them in the model's context: a bearer-capability link exposed to prompt-injection exfiltration. With url mode, the server sends the URL through a dedicated request that your agent delivers to the user out-of-band — a DM, an email, a UI notification — and the link never enters model context. + +## How to use it + +Override `onElicitRequest` on your agent. That is the whole integration: when the method is overridden, connections automatically advertise both form- and url-mode elicitation to servers at the `initialize` handshake. + + + +```ts +import { Agent } from "agents"; +import type { ElicitRequest, ElicitResult } from "agents/mcp"; + +class MyAgent extends Agent { + async onElicitRequest( + request: ElicitRequest, + serverId: string, + ): Promise { + if (request.params.mode === "url") { + // Deliver the link to the user out-of-band + await this.notifyUser(request.params.url, request.params.message); + return { action: "accept", content: {} }; + } + + // Form mode: collect the fields described by requestedSchema + return { action: "decline", content: {} }; + } +} +``` + + + +Because it is a class method, the handler survives Durable Object hibernation and applies to connections restored from storage — there is no callback to re-register. Agents without an override keep the previous behavior: connections advertise form mode only, so servers fall back to their non-elicitation flows. + +Explicitly declared `client.capabilities.elicitation` on `addMcpServer` is now honored instead of being overwritten, is persisted with the server registration, and survives hibernation — so you can narrow the advertised modes if you need to. + +See [Elicitation in the MCP client docs](/agents/model-context-protocol/apis/client-api/#elicitation) for the full guide, including a pattern for forwarding elicitation requests to a browser UI. 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 end to end. + +### Upgrade + +To update to the latest version: + + 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..b6ece17c703 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 @@ -418,7 +418,7 @@ switch (result.action) { :::note[MCP client support] -Elicitation requires MCP client support. Not all MCP clients implement the elicitation capability. Check the client documentation for compatibility. +Elicitation requires MCP client support. Not all MCP clients implement the elicitation capability. Check the client documentation for compatibility. Agents acting as MCP clients can respond to elicitation (including url mode) by overriding [`onElicitRequest`](/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/). 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..cd4275287c3 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,128 @@ export class MyAgent extends Agent { +## Elicitation + +MCP servers can request input from your agent during a tool call using [elicitation](https://modelcontextprotocol.io/specification/2025-11-25/client/elicitation). To respond, override `onElicitRequest` on your agent: + + + +```ts +import { Agent } from "agents"; +import type { ElicitRequest, ElicitResult } from "agents/mcp"; + +class MyAgent extends Agent { + async onElicitRequest( + request: ElicitRequest, + serverId: string, + ): Promise { + if (request.params.mode === "url") { + // Deliver the URL to the user out-of-band (a DM, an email, a UI + // notification) — it never enters model context. + await this.notifyUser(request.params.url, request.params.message); + return { action: "accept", content: {} }; + } + + // Form mode: collect the fields described by requestedSchema, + // or decline if your surface cannot prompt the user. + return { action: "decline", content: {} }; + } +} +``` + + + +Because `onElicitRequest` is a class method, it survives Durable Object hibernation and applies to connections restored from storage. The `serverId` argument identifies which server sent the request, so one agent can respond differently per server. Without an override, elicitation requests are rejected with an error. + +### Elicitation modes + +Servers only send elicitation modes your client advertised during the `initialize` handshake. The Agents SDK advertises modes based on what your agent can actually handle: + +- **With an `onElicitRequest` override**, connections advertise both **form** and **url** mode. +- **Without one**, connections advertise form mode only, matching previous behavior. + +**Form mode** asks the user for structured data. The request carries a `requestedSchema` describing the fields, and an accepting response returns matching `content`. + +**URL mode** asks the user to visit a URL — for example an authorization or checkout page. The MCP specification requires servers to use url mode for sensitive URLs so that they stay out of the model's context instead of appearing in tool-result text. Your handler delivers the URL to the user and returns `{ action: "accept" }` once it has done so. + +To narrow the advertised modes, declare them explicitly when adding the server — an explicit declaration always wins and is persisted with the server options: + + + +```ts +await this.addMcpServer("portal", "https://portal.example.com/mcp", { + client: { + capabilities: { + // Form mode only, even though onElicitRequest is overridden + elicitation: { form: {} }, + }, + }, +}); +``` + + + +### Forwarding elicitation to a UI + +`onElicitRequest` returns a promise, but the answer usually comes from a human. A common pattern is to broadcast the request to connected clients and resolve the promise when one of them responds: + + + +```ts +import { Agent, callable } from "agents"; +import type { ElicitRequest, ElicitResult } from "agents/mcp"; + +class MyAgent extends Agent { + private pendingElicitations = new Map< + string, + (result: ElicitResult) => void + >(); + + async onElicitRequest( + request: ElicitRequest, + serverId: string, + ): Promise { + const id = crypto.randomUUID(); + + const result = new Promise((resolve) => { + this.pendingElicitations.set(id, resolve); + // Do not hold the tool call open forever if nobody answers + setTimeout(() => { + if (this.pendingElicitations.delete(id)) { + resolve({ action: "cancel", content: {} }); + } + }, 300_000); + }); + + // The UI renders a form from request.params.requestedSchema + // (or a link for url mode) and calls respondToElicitation + this.broadcast( + JSON.stringify({ + type: "mcp-elicitation", + id, + serverId, + params: request.params, + }), + ); + + return result; + } + + @callable() + respondToElicitation(id: string, result: ElicitResult) { + const resolve = this.pendingElicitations.get(id); + if (resolve) { + this.pendingElicitations.delete(id); + resolve(result); + } + } +} +``` + + + +See the [`mcp-client` example](https://github.com/cloudflare/agents/tree/main/examples/mcp-client) for a complete browser UI implementing this pattern, and the [`mcp-elicitation` example](https://github.com/cloudflare/agents/tree/main/examples/mcp-elicitation) for a server that sends both elicitation modes. To send elicitation requests from your own MCP server, see [`elicitInput`](/agents/model-context-protocol/apis/agent-api/#elicitinputoptions-context). + ## Using MCP capabilities Once connected, access the server's capabilities: From 1742f006483840e2adc3e2114c47eb17f512b0e1 Mon Sep 17 00:00:00 2001 From: Matt Carey Date: Thu, 9 Jul 2026 17:56:25 +0100 Subject: [PATCH 02/12] clarify: both url and form mode --- .../docs/agents/model-context-protocol/apis/agent-api.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 b6ece17c703..71e45550492 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 @@ -418,7 +418,7 @@ switch (result.action) { :::note[MCP client support] -Elicitation requires MCP client support. Not all MCP clients implement the elicitation capability. Check the client documentation for compatibility. Agents acting as MCP clients can respond to elicitation (including url mode) by overriding [`onElicitRequest`](/agents/model-context-protocol/apis/client-api/#elicitation). +Elicitation requires MCP client support. Not all MCP clients implement the elicitation capability. Check the client documentation for compatibility. Agents acting as MCP clients can respond to elicitation (both url and form mode) by overriding [`onElicitRequest`](/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/). From 53627e45ca052818928c8f24f36c76d2a7d77c23 Mon Sep 17 00:00:00 2001 From: Matt Carey Date: Thu, 9 Jul 2026 17:58:17 +0100 Subject: [PATCH 03/12] polish elicitation docs and changelog wording --- .../changelog/agents/2026-07-09-url-elicitation.mdx | 10 +++++----- .../agents/model-context-protocol/apis/client-api.mdx | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/content/changelog/agents/2026-07-09-url-elicitation.mdx b/src/content/changelog/agents/2026-07-09-url-elicitation.mdx index 5a8f182faa7..d8aec0f8b15 100644 --- a/src/content/changelog/agents/2026-07-09-url-elicitation.mdx +++ b/src/content/changelog/agents/2026-07-09-url-elicitation.mdx @@ -9,11 +9,11 @@ date: 2026-07-09 import { PackageManagers, TypeScriptExample } from "~/components"; -Agents connected to MCP servers with [`addMcpServer`](/agents/model-context-protocol/apis/client-api/) can now respond to [elicitation](https://modelcontextprotocol.io/specification/2025-11-25/client/elicitation) requests — including **url-mode elicitation**, the MCP specification's mechanism for sending sensitive URLs to the user. +Agents connected to MCP servers with [`addMcpServer`](/agents/model-context-protocol/apis/client-api/) can now respond to [elicitation](https://modelcontextprotocol.io/specification/2025-11-25/client/elicitation) requests, including url-mode elicitation, which the MCP specification uses to send sensitive URLs to the user. ## Why url-mode elicitation -Some tool calls need the user to visit a URL — an OAuth authorization page, a checkout, a verification link. Before url-mode elicitation, servers had to put those URLs into tool-result text, which lands them in the model's context: a bearer-capability link exposed to prompt-injection exfiltration. With url mode, the server sends the URL through a dedicated request that your agent delivers to the user out-of-band — a DM, an email, a UI notification — and the link never enters model context. +Some tool calls need the user to visit a URL, most commonly an OAuth authorization page. Before url-mode elicitation, servers had to put that URL into tool-result text, which lands it in the model's context. An authorization link in model context can be exfiltrated by prompt injection. With url mode, the server sends the URL through a dedicated request instead, and your agent delivers it to the user out-of-band (in a chat message or a UI notification, for example). The link never enters model context. ## How to use it @@ -31,7 +31,7 @@ class MyAgent extends Agent { serverId: string, ): Promise { if (request.params.mode === "url") { - // Deliver the link to the user out-of-band + // Deliver the link to the user outside the model's context await this.notifyUser(request.params.url, request.params.message); return { action: "accept", content: {} }; } @@ -44,9 +44,9 @@ class MyAgent extends Agent { -Because it is a class method, the handler survives Durable Object hibernation and applies to connections restored from storage — there is no callback to re-register. Agents without an override keep the previous behavior: connections advertise form mode only, so servers fall back to their non-elicitation flows. +Because it is a class method, the handler survives Durable Object hibernation and applies to connections restored from storage. There is no callback to re-register. Agents without an override keep the previous behavior: connections advertise form mode only, so servers fall back to their non-elicitation flows. -Explicitly declared `client.capabilities.elicitation` on `addMcpServer` is now honored instead of being overwritten, is persisted with the server registration, and survives hibernation — so you can narrow the advertised modes if you need to. +Explicitly declared `client.capabilities.elicitation` on `addMcpServer` is now honored instead of being overwritten. It is persisted with the server registration and survives hibernation, so you can narrow the advertised modes if you need to. See [Elicitation in the MCP client docs](/agents/model-context-protocol/apis/client-api/#elicitation) for the full guide, including a pattern for forwarding elicitation requests to a browser UI. 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 end to end. 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 cd4275287c3..2b0d3d507e9 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 @@ -330,8 +330,8 @@ class MyAgent extends Agent { serverId: string, ): Promise { if (request.params.mode === "url") { - // Deliver the URL to the user out-of-band (a DM, an email, a UI - // notification) — it never enters model context. + // Deliver the URL to the user out-of-band, for example in a chat + // message or a UI notification. It never enters model context. await this.notifyUser(request.params.url, request.params.message); return { action: "accept", content: {} }; } @@ -356,9 +356,9 @@ Servers only send elicitation modes your client advertised during the `initializ **Form mode** asks the user for structured data. The request carries a `requestedSchema` describing the fields, and an accepting response returns matching `content`. -**URL mode** asks the user to visit a URL — for example an authorization or checkout page. The MCP specification requires servers to use url mode for sensitive URLs so that they stay out of the model's context instead of appearing in tool-result text. Your handler delivers the URL to the user and returns `{ action: "accept" }` once it has done so. +**URL mode** asks the user to visit a URL, for example an authorization or checkout page. The MCP specification requires servers to use url mode for sensitive URLs so that they stay out of the model's context instead of appearing in tool-result text. Your handler delivers the URL to the user and returns `{ action: "accept" }` once it has done so. -To narrow the advertised modes, declare them explicitly when adding the server — an explicit declaration always wins and is persisted with the server options: +To narrow the advertised modes, declare them explicitly when adding the server. An explicit declaration always wins and is persisted with the server options: From f71090bb2d7859aea2cb1d802772dd0a746db246 Mon Sep 17 00:00:00 2001 From: Matt Carey Date: Thu, 9 Jul 2026 18:01:26 +0100 Subject: [PATCH 04/12] restore technical register per repo style guide, plain-language security explanation --- .../changelog/agents/2026-07-09-url-elicitation.mdx | 10 +++++----- .../agents/model-context-protocol/apis/client-api.mdx | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/content/changelog/agents/2026-07-09-url-elicitation.mdx b/src/content/changelog/agents/2026-07-09-url-elicitation.mdx index d8aec0f8b15..1d3e69c7a33 100644 --- a/src/content/changelog/agents/2026-07-09-url-elicitation.mdx +++ b/src/content/changelog/agents/2026-07-09-url-elicitation.mdx @@ -9,11 +9,11 @@ date: 2026-07-09 import { PackageManagers, TypeScriptExample } from "~/components"; -Agents connected to MCP servers with [`addMcpServer`](/agents/model-context-protocol/apis/client-api/) can now respond to [elicitation](https://modelcontextprotocol.io/specification/2025-11-25/client/elicitation) requests, including url-mode elicitation, which the MCP specification uses to send sensitive URLs to the user. +Agents connected to MCP servers with [`addMcpServer`](/agents/model-context-protocol/apis/client-api/) can now respond to [elicitation](https://modelcontextprotocol.io/specification/2025-11-25/client/elicitation) requests, including url-mode elicitation, which the MCP specification uses to deliver sensitive URLs to the user. ## Why url-mode elicitation -Some tool calls need the user to visit a URL, most commonly an OAuth authorization page. Before url-mode elicitation, servers had to put that URL into tool-result text, which lands it in the model's context. An authorization link in model context can be exfiltrated by prompt injection. With url mode, the server sends the URL through a dedicated request instead, and your agent delivers it to the user out-of-band (in a chat message or a UI notification, for example). The link never enters model context. +Some tool calls require the user to visit a URL, most commonly an OAuth authorization page. Without url-mode elicitation, servers surface that URL in tool-result text, which places it in the model's context. These links grant access to whoever opens them, and anything in model context can be leaked through prompt injection. With url mode, the server sends the URL through a dedicated request and your agent delivers it to the user out-of-band, in a chat message or a UI notification, so the link never enters model context. ## How to use it @@ -44,11 +44,11 @@ class MyAgent extends Agent { -Because it is a class method, the handler survives Durable Object hibernation and applies to connections restored from storage. There is no callback to re-register. Agents without an override keep the previous behavior: connections advertise form mode only, so servers fall back to their non-elicitation flows. +Because it is a class method, the handler survives Durable Object hibernation and applies to connections restored from storage, with no callback to re-register after a wake-up. Agents without an override keep the previous behavior: connections advertise form mode only, so servers fall back to their non-elicitation flows. -Explicitly declared `client.capabilities.elicitation` on `addMcpServer` is now honored instead of being overwritten. It is persisted with the server registration and survives hibernation, so you can narrow the advertised modes if you need to. +An explicitly declared `client.capabilities.elicitation` on `addMcpServer` is now honored instead of being overwritten. The declaration is persisted with the server registration and survives hibernation, so you can narrow the advertised modes when your agent should only receive specific ones. -See [Elicitation in the MCP client docs](/agents/model-context-protocol/apis/client-api/#elicitation) for the full guide, including a pattern for forwarding elicitation requests to a browser UI. 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 end to end. +Refer to [Elicitation in the MCP client documentation](/agents/model-context-protocol/apis/client-api/#elicitation) for the full guide, including a pattern for forwarding elicitation requests to a browser UI. 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 end to end. ### Upgrade 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 2b0d3d507e9..e217f10dac8 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 @@ -330,8 +330,8 @@ class MyAgent extends Agent { serverId: string, ): Promise { if (request.params.mode === "url") { - // Deliver the URL to the user out-of-band, for example in a chat - // message or a UI notification. It never enters model context. + // Deliver the URL to the user out-of-band (chat message, email, + // UI notification) so it never enters model context await this.notifyUser(request.params.url, request.params.message); return { action: "accept", content: {} }; } @@ -434,7 +434,7 @@ class MyAgent extends Agent { -See the [`mcp-client` example](https://github.com/cloudflare/agents/tree/main/examples/mcp-client) for a complete browser UI implementing this pattern, and the [`mcp-elicitation` example](https://github.com/cloudflare/agents/tree/main/examples/mcp-elicitation) for a server that sends both elicitation modes. To send elicitation requests from your own MCP server, see [`elicitInput`](/agents/model-context-protocol/apis/agent-api/#elicitinputoptions-context). +Refer to the [`mcp-client` example](https://github.com/cloudflare/agents/tree/main/examples/mcp-client) for a complete browser UI implementing this pattern, and the [`mcp-elicitation` example](https://github.com/cloudflare/agents/tree/main/examples/mcp-elicitation) for a server that sends both elicitation modes. To send elicitation requests from your own MCP server, refer to [`elicitInput`](/agents/model-context-protocol/apis/agent-api/#elicitinputoptions-context). ## Using MCP capabilities From f94501f680d64064ec2242660a4ce1250f3d814f Mon Sep 17 00:00:00 2001 From: Matt Carey Date: Thu, 9 Jul 2026 18:09:42 +0100 Subject: [PATCH 05/12] docs: point elicitation spec links at /specification/latest --- src/content/changelog/agents/2026-07-09-url-elicitation.mdx | 2 +- .../docs/agents/model-context-protocol/apis/client-api.mdx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/content/changelog/agents/2026-07-09-url-elicitation.mdx b/src/content/changelog/agents/2026-07-09-url-elicitation.mdx index 1d3e69c7a33..8f05c676fd3 100644 --- a/src/content/changelog/agents/2026-07-09-url-elicitation.mdx +++ b/src/content/changelog/agents/2026-07-09-url-elicitation.mdx @@ -9,7 +9,7 @@ date: 2026-07-09 import { PackageManagers, TypeScriptExample } from "~/components"; -Agents connected to MCP servers with [`addMcpServer`](/agents/model-context-protocol/apis/client-api/) can now respond to [elicitation](https://modelcontextprotocol.io/specification/2025-11-25/client/elicitation) requests, including url-mode elicitation, which the MCP specification uses to deliver sensitive URLs to the user. +Agents connected to MCP servers with [`addMcpServer`](/agents/model-context-protocol/apis/client-api/) can now respond to [elicitation](https://modelcontextprotocol.io/specification/latest/client/elicitation) requests, including url-mode elicitation, which the MCP specification uses to deliver sensitive URLs to the user. ## Why url-mode elicitation 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 e217f10dac8..d867edd5949 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 @@ -316,7 +316,7 @@ export class MyAgent extends Agent { ## Elicitation -MCP servers can request input from your agent during a tool call using [elicitation](https://modelcontextprotocol.io/specification/2025-11-25/client/elicitation). To respond, override `onElicitRequest` on your agent: +MCP servers can request input from your agent during a tool call using [elicitation](https://modelcontextprotocol.io/specification/latest/client/elicitation). To respond, override `onElicitRequest` on your agent: From b38ebc05962fda52f61266809304fa3771f542dc Mon Sep 17 00:00:00 2001 From: Matt Carey Date: Thu, 9 Jul 2026 18:11:43 +0100 Subject: [PATCH 06/12] docs: clarify that onElicitRequest handles both modes and is required --- .../apis/client-api.mdx | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) 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 d867edd5949..56a6e92ab27 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 @@ -349,15 +349,26 @@ Because `onElicitRequest` is a class method, it survives Durable Object hibernat ### Elicitation modes -Servers only send elicitation modes your client advertised during the `initialize` handshake. The Agents SDK advertises modes based on what your agent can actually handle: +Elicitation requests arrive in one of two modes, and the same `onElicitRequest` override handles both: -- **With an `onElicitRequest` override**, connections advertise both **form** and **url** mode. -- **Without one**, connections advertise form mode only, matching previous behavior. +**Form mode** asks the user for structured data. The request carries a `requestedSchema` describing the fields, and an accepting response returns matching `content`: -**Form mode** asks the user for structured data. The request carries a `requestedSchema` describing the fields, and an accepting response returns matching `content`. + + +```ts +async onElicitRequest(request: ElicitRequest): Promise { + // Collect values matching request.params.requestedSchema from the user, + // then accept with the collected fields + return { action: "accept", content: { amount: 5 } }; +} +``` + + **URL mode** asks the user to visit a URL, for example an authorization or checkout page. The MCP specification requires servers to use url mode for sensitive URLs so that they stay out of the model's context instead of appearing in tool-result text. Your handler delivers the URL to the user and returns `{ action: "accept" }` once it has done so. +Servers only send elicitation modes your client advertised during the `initialize` handshake. Connections with an `onElicitRequest` override advertise both modes. Connections without one advertise form mode only and reject any elicitation request that arrives, so overriding `onElicitRequest` is required to handle either mode. + To narrow the advertised modes, declare them explicitly when adding the server. An explicit declaration always wins and is persisted with the server options: From d6958f9ce5e07a3519c47640249ff1b8881c50a7 Mon Sep 17 00:00:00 2001 From: Matt Carey Date: Thu, 9 Jul 2026 18:37:08 +0100 Subject: [PATCH 07/12] docs: describe final elicitation capability behavior --- src/content/changelog/agents/2026-07-09-url-elicitation.mdx | 4 ++-- .../docs/agents/model-context-protocol/apis/client-api.mdx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/content/changelog/agents/2026-07-09-url-elicitation.mdx b/src/content/changelog/agents/2026-07-09-url-elicitation.mdx index 8f05c676fd3..c6c3f3be7b3 100644 --- a/src/content/changelog/agents/2026-07-09-url-elicitation.mdx +++ b/src/content/changelog/agents/2026-07-09-url-elicitation.mdx @@ -44,9 +44,9 @@ class MyAgent extends Agent { -Because it is a class method, the handler survives Durable Object hibernation and applies to connections restored from storage, with no callback to re-register after a wake-up. Agents without an override keep the previous behavior: connections advertise form mode only, so servers fall back to their non-elicitation flows. +Because it is a class method, the handler survives Durable Object hibernation and applies to connections restored from storage, with no callback to re-register after a wake-up. Agents without an override advertise no elicitation capability, so servers use their non-elicitation fallbacks instead of sending requests the agent cannot answer. -An explicitly declared `client.capabilities.elicitation` on `addMcpServer` is now honored instead of being overwritten. The declaration is persisted with the server registration and survives hibernation, so you can narrow the advertised modes when your agent should only receive specific ones. +An explicitly declared `client.capabilities.elicitation` on `addMcpServer` is honored, persisted with the server registration, and survives hibernation, so you can narrow the advertised modes when your agent should only receive specific ones. Refer to [Elicitation in the MCP client documentation](/agents/model-context-protocol/apis/client-api/#elicitation) for the full guide, including a pattern for forwarding elicitation requests to a browser UI. 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 end to end. 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 56a6e92ab27..736c3af31c0 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 @@ -345,7 +345,7 @@ class MyAgent extends Agent { -Because `onElicitRequest` is a class method, it survives Durable Object hibernation and applies to connections restored from storage. The `serverId` argument identifies which server sent the request, so one agent can respond differently per server. Without an override, elicitation requests are rejected with an error. +Because `onElicitRequest` is a class method, it survives Durable Object hibernation and applies to connections restored from storage. The `serverId` argument identifies which server sent the request, so one agent can respond differently per server. ### Elicitation modes @@ -367,7 +367,7 @@ async onElicitRequest(request: ElicitRequest): Promise { **URL mode** asks the user to visit a URL, for example an authorization or checkout page. The MCP specification requires servers to use url mode for sensitive URLs so that they stay out of the model's context instead of appearing in tool-result text. Your handler delivers the URL to the user and returns `{ action: "accept" }` once it has done so. -Servers only send elicitation modes your client advertised during the `initialize` handshake. Connections with an `onElicitRequest` override advertise both modes. Connections without one advertise form mode only and reject any elicitation request that arrives, so overriding `onElicitRequest` is required to handle either mode. +Servers only send elicitation modes your client advertised during the `initialize` handshake. Connections with an `onElicitRequest` override advertise both modes. Connections without one advertise no elicitation capability, so servers use their non-elicitation fallbacks instead. Overriding `onElicitRequest` is how an agent opts into elicitation. To narrow the advertised modes, declare them explicitly when adding the server. An explicit declaration always wins and is persisted with the server options: From 6b6b924a1f311fd2e2550da6d1e928701213695d Mon Sep 17 00:00:00 2001 From: Matt Carey Date: Thu, 9 Jul 2026 20:08:13 +0100 Subject: [PATCH 08/12] docs: elicitation via configureElicitationHandler({ form, url }) --- .../agents/2026-07-09-url-elicitation.mdx | 36 +++--- .../model-context-protocol/apis/agent-api.mdx | 2 +- .../apis/client-api.mdx | 120 ++++++++++-------- 3 files changed, 88 insertions(+), 70 deletions(-) diff --git a/src/content/changelog/agents/2026-07-09-url-elicitation.mdx b/src/content/changelog/agents/2026-07-09-url-elicitation.mdx index c6c3f3be7b3..3d40791d6dc 100644 --- a/src/content/changelog/agents/2026-07-09-url-elicitation.mdx +++ b/src/content/changelog/agents/2026-07-09-url-elicitation.mdx @@ -1,6 +1,6 @@ --- title: URL elicitations are in the Agents SDK -description: "Agents acting as MCP clients can now respond to elicitation requests, including url-mode elicitation for sensitive links like OAuth URLs, by overriding a single method." +description: "Agents acting as MCP clients can now respond to elicitation requests, including url-mode elicitation for sensitive links like OAuth URLs, by registering an MCP elicitation handler." products: - agents - workers @@ -17,7 +17,7 @@ Some tool calls require the user to visit a URL, most commonly an OAuth authoriz ## How to use it -Override `onElicitRequest` on your agent. That is the whole integration: when the method is overridden, connections automatically advertise both form- and url-mode elicitation to servers at the `initialize` handshake. +Register elicitation handlers in `onStart()` with `this.mcp.configureElicitationHandler({ form, url })`. Connections advertise exactly the modes with configured handlers to servers at the `initialize` handshake. @@ -26,25 +26,31 @@ import { Agent } from "agents"; import type { ElicitRequest, ElicitResult } from "agents/mcp"; class MyAgent extends Agent { - async onElicitRequest( - request: ElicitRequest, - serverId: string, - ): Promise { - if (request.params.mode === "url") { - // Deliver the link to the user outside the model's context - await this.notifyUser(request.params.url, request.params.message); - return { action: "accept", content: {} }; - } - - // Form mode: collect the fields described by requestedSchema - return { action: "decline", content: {} }; + onStart() { + this.mcp.configureElicitationHandler({ + url: async ( + request: ElicitRequest, + serverId: string, + ): Promise => { + // Deliver the link to the user outside the model's context + await this.notifyUser(request.params.url, request.params.message); + return { action: "accept", content: {} }; + }, + form: async ( + request: ElicitRequest, + serverId: string, + ): Promise => { + // Collect the fields described by request.params.requestedSchema + return { action: "decline", content: {} }; + }, + }); } } ``` -Because it is a class method, the handler survives Durable Object hibernation and applies to connections restored from storage, with no callback to re-register after a wake-up. Agents without an override advertise no elicitation capability, so servers use their non-elicitation fallbacks instead of sending requests the agent cannot answer. +Because `onStart()` runs before stored MCP connections are restored, configured handlers apply to connections restored after Durable Object hibernation. Agents without configured handlers advertise no elicitation capability, so servers use their non-elicitation fallbacks instead of sending requests the agent cannot answer. An explicitly declared `client.capabilities.elicitation` on `addMcpServer` is honored, persisted with the server registration, and survives hibernation, so you can narrow the advertised modes when your agent should only receive specific ones. 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 71e45550492..2b67f24fe58 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 @@ -418,7 +418,7 @@ switch (result.action) { :::note[MCP client support] -Elicitation requires MCP client support. Not all MCP clients implement the elicitation capability. Check the client documentation for compatibility. Agents acting as MCP clients can respond to elicitation (both url and form mode) by overriding [`onElicitRequest`](/agents/model-context-protocol/apis/client-api/#elicitation). +Elicitation requires MCP client support. Not all MCP clients implement the elicitation capability. Check the client documentation for compatibility. Agents acting as MCP clients can respond to elicitation (both url and form mode) by registering an [MCP elicitation handler](/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/). 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 736c3af31c0..9aa771509df 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 @@ -316,7 +316,7 @@ export class MyAgent extends Agent { ## Elicitation -MCP servers can request input from your agent during a tool call using [elicitation](https://modelcontextprotocol.io/specification/latest/client/elicitation). To respond, override `onElicitRequest` on your agent: +MCP servers can request input from your agent during a tool call using [elicitation](https://modelcontextprotocol.io/specification/latest/client/elicitation). To respond, register an elicitation handler with `this.mcp.configureElicitationHandler()` in `onStart()`: @@ -325,49 +325,57 @@ import { Agent } from "agents"; import type { ElicitRequest, ElicitResult } from "agents/mcp"; class MyAgent extends Agent { - async onElicitRequest( - request: ElicitRequest, - serverId: string, - ): Promise { - if (request.params.mode === "url") { - // Deliver the URL to the user out-of-band (chat message, email, - // UI notification) so it never enters model context - await this.notifyUser(request.params.url, request.params.message); - return { action: "accept", content: {} }; - } - - // Form mode: collect the fields described by requestedSchema, - // or decline if your surface cannot prompt the user. - return { action: "decline", content: {} }; + onStart() { + this.mcp.configureElicitationHandler({ + url: async ( + request: ElicitRequest, + serverId: string, + ): Promise => { + // Deliver the URL to the user out-of-band (chat message, email, + // UI notification) so it never enters model context + await this.notifyUser(request.params.url, request.params.message); + return { action: "accept", content: {} }; + }, + form: async ( + request: ElicitRequest, + serverId: string, + ): Promise => { + // Collect the fields described by request.params.requestedSchema, + // or decline if your surface cannot prompt the user + return { action: "decline", content: {} }; + }, + }); } } ``` -Because `onElicitRequest` is a class method, it survives Durable Object hibernation and applies to connections restored from storage. The `serverId` argument identifies which server sent the request, so one agent can respond differently per server. +Register handlers in `onStart()` so they are installed before stored MCP connections are restored and before the `initialize` handshake for new connections. The `serverId` argument identifies which server sent the request, so one agent can respond differently per server. ### Elicitation modes -Elicitation requests arrive in one of two modes, and the same `onElicitRequest` override handles both: +Elicitation requests arrive in one of two modes, each with its own handler: -**Form mode** asks the user for structured data. The request carries a `requestedSchema` describing the fields, and an accepting response returns matching `content`: +**Form mode** (the `form` handler) asks the user for structured data. The request carries a `requestedSchema` describing the fields, and an accepting response returns matching `content`: ```ts -async onElicitRequest(request: ElicitRequest): Promise { - // Collect values matching request.params.requestedSchema from the user, - // then accept with the collected fields - return { action: "accept", content: { amount: 5 } }; -} +this.mcp.configureElicitationHandler({ + form: async (request) => { + // Collect values matching request.params.requestedSchema from the user, + // then accept with the collected fields + return { action: "accept", content: { amount: 5 } }; + }, +}); ``` -**URL mode** asks the user to visit a URL, for example an authorization or checkout page. The MCP specification requires servers to use url mode for sensitive URLs so that they stay out of the model's context instead of appearing in tool-result text. Your handler delivers the URL to the user and returns `{ action: "accept" }` once it has done so. +**URL mode** (the `url` handler) asks the user to visit a URL, for example an authorization or checkout page. The MCP specification requires servers to use url mode for sensitive URLs so that they stay out of the model's context instead of appearing in tool-result text. Your handler delivers the URL to the user and returns `{ action: "accept" }` once it has done so. -Servers only send elicitation modes your client advertised during the `initialize` handshake. Connections with an `onElicitRequest` override advertise both modes. Connections without one advertise no elicitation capability, so servers use their non-elicitation fallbacks instead. Overriding `onElicitRequest` is how an agent opts into elicitation. +Servers only send elicitation modes your client advertised during the `initialize` handshake, and connections advertise exactly the modes with configured handlers: configure `form` only and servers will not send url-mode requests, and the other way around. Connections without any handlers advertise no elicitation capability, so servers use their non-elicitation fallbacks instead. Configuring handlers is how an agent opts into elicitation. To narrow the advertised modes, declare them explicitly when adding the server. An explicit declaration always wins and is persisted with the server options: @@ -377,7 +385,7 @@ To narrow the advertised modes, declare them explicitly when adding the server. await this.addMcpServer("portal", "https://portal.example.com/mcp", { client: { capabilities: { - // Form mode only, even though onElicitRequest is overridden + // Form mode only, regardless of which handlers are configured elicitation: { form: {} }, }, }, @@ -388,7 +396,7 @@ await this.addMcpServer("portal", "https://portal.example.com/mcp", { ### Forwarding elicitation to a UI -`onElicitRequest` returns a promise, but the answer usually comes from a human. A common pattern is to broadcast the request to connected clients and resolve the promise when one of them responds: +Elicitation handlers return a promise, but the answer usually comes from a human. A common pattern is to broadcast the request to connected clients and resolve the promise when one of them responds — the same function can serve both modes: @@ -402,34 +410,38 @@ class MyAgent extends Agent { (result: ElicitResult) => void >(); - async onElicitRequest( - request: ElicitRequest, - serverId: string, - ): Promise { - const id = crypto.randomUUID(); - - const result = new Promise((resolve) => { - this.pendingElicitations.set(id, resolve); - // Do not hold the tool call open forever if nobody answers - setTimeout(() => { - if (this.pendingElicitations.delete(id)) { - resolve({ action: "cancel", content: {} }); - } - }, 300_000); - }); - - // The UI renders a form from request.params.requestedSchema - // (or a link for url mode) and calls respondToElicitation - this.broadcast( - JSON.stringify({ - type: "mcp-elicitation", - id, - serverId, - params: request.params, - }), - ); + onStart() { + const forwardToBrowser = (request: ElicitRequest, serverId: string) => { + const id = crypto.randomUUID(); + + const result = new Promise((resolve) => { + this.pendingElicitations.set(id, resolve); + // Do not hold the tool call open forever if nobody answers + setTimeout(() => { + if (this.pendingElicitations.delete(id)) { + resolve({ action: "cancel", content: {} }); + } + }, 300_000); + }); + + // The UI renders a form from request.params.requestedSchema + // (or a link for url mode) and calls respondToElicitation + this.broadcast( + JSON.stringify({ + type: "mcp-elicitation", + id, + serverId, + params: request.params, + }), + ); + + return result; + }; - return result; + this.mcp.configureElicitationHandler({ + form: forwardToBrowser, + url: forwardToBrowser, + }); } @callable() From a84b34b4187ce13289e9b3880cde5dcc381179aa Mon Sep 17 00:00:00 2001 From: Matt Carey Date: Thu, 9 Jul 2026 20:11:41 +0100 Subject: [PATCH 09/12] docs: finish grouped elicitation handler docs --- .../docs/agents/model-context-protocol/apis/client-api.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 9aa771509df..5e3c9270803 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 @@ -396,7 +396,7 @@ await this.addMcpServer("portal", "https://portal.example.com/mcp", { ### Forwarding elicitation to a UI -Elicitation handlers return a promise, but the answer usually comes from a human. A common pattern is to broadcast the request to connected clients and resolve the promise when one of them responds — the same function can serve both modes: +Elicitation handlers return a promise, but the answer usually comes from a human. A common pattern is to broadcast the request to connected clients and resolve the promise when one of them responds. The same function can serve both modes: From 5656420a9ce6b8a9235e5b2b810e3c57a4087d7b Mon Sep 17 00:00:00 2001 From: Matt Carey Date: Thu, 9 Jul 2026 21:32:24 +0100 Subject: [PATCH 10/12] docs: describe persisted elicitation capability restore behavior --- src/content/changelog/agents/2026-07-09-url-elicitation.mdx | 2 +- .../docs/agents/model-context-protocol/apis/client-api.mdx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/content/changelog/agents/2026-07-09-url-elicitation.mdx b/src/content/changelog/agents/2026-07-09-url-elicitation.mdx index 3d40791d6dc..73f263283dc 100644 --- a/src/content/changelog/agents/2026-07-09-url-elicitation.mdx +++ b/src/content/changelog/agents/2026-07-09-url-elicitation.mdx @@ -50,7 +50,7 @@ class MyAgent extends Agent { -Because `onStart()` runs before stored MCP connections are restored, configured handlers apply to connections restored after Durable Object hibernation. Agents without configured handlers advertise no elicitation capability, so servers use their non-elicitation fallbacks instead of sending requests the agent cannot answer. +The advertised modes are persisted with each MCP server, so connections restored after Durable Object hibernation advertise the same modes and the handlers reattach when `onStart()` runs. Agents without configured handlers advertise no elicitation capability, so servers use their non-elicitation fallbacks instead of sending requests the agent cannot answer. An explicitly declared `client.capabilities.elicitation` on `addMcpServer` is honored, persisted with the server registration, and survives hibernation, so you can narrow the advertised modes when your agent should only receive specific ones. 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 5e3c9270803..ac8670e7083 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 @@ -351,7 +351,7 @@ class MyAgent extends Agent { -Register handlers in `onStart()` so they are installed before stored MCP connections are restored and before the `initialize` handshake for new connections. The `serverId` argument identifies which server sent the request, so one agent can respond differently per server. +Register handlers in `onStart()`. The advertised modes are persisted with each MCP server, so connections restored after hibernation advertise the same modes at the `initialize` handshake and the handlers reattach when `onStart()` runs. The `serverId` argument identifies which server sent the request, so one agent can respond differently per server. ### Elicitation modes From a861bc1813bd0a55f3cb27437dcf73374ab6f323 Mon Sep 17 00:00:00 2001 From: Matt Carey Date: Fri, 10 Jul 2026 11:20:29 +0100 Subject: [PATCH 11/12] docs: rename configureElicitationHandler to configureElicitationHandlers --- .../changelog/agents/2026-07-09-url-elicitation.mdx | 4 ++-- .../agents/model-context-protocol/apis/client-api.mdx | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/content/changelog/agents/2026-07-09-url-elicitation.mdx b/src/content/changelog/agents/2026-07-09-url-elicitation.mdx index 73f263283dc..2f550ccd319 100644 --- a/src/content/changelog/agents/2026-07-09-url-elicitation.mdx +++ b/src/content/changelog/agents/2026-07-09-url-elicitation.mdx @@ -17,7 +17,7 @@ Some tool calls require the user to visit a URL, most commonly an OAuth authoriz ## How to use it -Register elicitation handlers in `onStart()` with `this.mcp.configureElicitationHandler({ form, url })`. Connections advertise exactly the modes with configured handlers to servers at the `initialize` handshake. +Register elicitation handlers in `onStart()` with `this.mcp.configureElicitationHandlers({ form, url })`. Connections advertise exactly the modes with configured handlers to servers at the `initialize` handshake. @@ -27,7 +27,7 @@ import type { ElicitRequest, ElicitResult } from "agents/mcp"; class MyAgent extends Agent { onStart() { - this.mcp.configureElicitationHandler({ + this.mcp.configureElicitationHandlers({ url: async ( request: ElicitRequest, serverId: string, 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 ac8670e7083..40d698539e2 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 @@ -316,7 +316,7 @@ export class MyAgent extends Agent { ## Elicitation -MCP servers can request input from your agent during a tool call using [elicitation](https://modelcontextprotocol.io/specification/latest/client/elicitation). To respond, register an elicitation handler with `this.mcp.configureElicitationHandler()` in `onStart()`: +MCP servers can request input from your agent during a tool call using [elicitation](https://modelcontextprotocol.io/specification/latest/client/elicitation). To respond, register an elicitation handler with `this.mcp.configureElicitationHandlers()` in `onStart()`: @@ -326,7 +326,7 @@ import type { ElicitRequest, ElicitResult } from "agents/mcp"; class MyAgent extends Agent { onStart() { - this.mcp.configureElicitationHandler({ + this.mcp.configureElicitationHandlers({ url: async ( request: ElicitRequest, serverId: string, @@ -362,7 +362,7 @@ Elicitation requests arrive in one of two modes, each with its own handler: ```ts -this.mcp.configureElicitationHandler({ +this.mcp.configureElicitationHandlers({ form: async (request) => { // Collect values matching request.params.requestedSchema from the user, // then accept with the collected fields @@ -438,7 +438,7 @@ class MyAgent extends Agent { return result; }; - this.mcp.configureElicitationHandler({ + this.mcp.configureElicitationHandlers({ form: forwardToBrowser, url: forwardToBrowser, }); From eac97f96657500e878678954e7d8d99e364cf3fd Mon Sep 17 00:00:00 2001 From: Matt Carey Date: Mon, 13 Jul 2026 17:52:44 +0100 Subject: [PATCH 12/12] [Agents] Finalize MCP elicitation documentation Document form and URL modes against MCP 2025-11-25, correct consent and completion semantics, add server and client implementation guidance, and publish the changelog on the release date. --- .../agents/2026-07-09-url-elicitation.mdx | 63 ------ .../2026-07-13-mcp-client-elicitation.mdx | 69 ++++++ .../model-context-protocol/apis/agent-api.mdx | 205 +++++++----------- .../apis/client-api.mdx | 192 +++++++++------- 4 files changed, 263 insertions(+), 266 deletions(-) delete mode 100644 src/content/changelog/agents/2026-07-09-url-elicitation.mdx create mode 100644 src/content/changelog/agents/2026-07-13-mcp-client-elicitation.mdx diff --git a/src/content/changelog/agents/2026-07-09-url-elicitation.mdx b/src/content/changelog/agents/2026-07-09-url-elicitation.mdx deleted file mode 100644 index 2f550ccd319..00000000000 --- a/src/content/changelog/agents/2026-07-09-url-elicitation.mdx +++ /dev/null @@ -1,63 +0,0 @@ ---- -title: URL elicitations are in the Agents SDK -description: "Agents acting as MCP clients can now respond to elicitation requests, including url-mode elicitation for sensitive links like OAuth URLs, by registering an MCP elicitation handler." -products: - - agents - - workers -date: 2026-07-09 ---- - -import { PackageManagers, TypeScriptExample } from "~/components"; - -Agents connected to MCP servers with [`addMcpServer`](/agents/model-context-protocol/apis/client-api/) can now respond to [elicitation](https://modelcontextprotocol.io/specification/latest/client/elicitation) requests, including url-mode elicitation, which the MCP specification uses to deliver sensitive URLs to the user. - -## Why url-mode elicitation - -Some tool calls require the user to visit a URL, most commonly an OAuth authorization page. Without url-mode elicitation, servers surface that URL in tool-result text, which places it in the model's context. These links grant access to whoever opens them, and anything in model context can be leaked through prompt injection. With url mode, the server sends the URL through a dedicated request and your agent delivers it to the user out-of-band, in a chat message or a UI notification, so the link never enters model context. - -## How to use it - -Register elicitation handlers in `onStart()` with `this.mcp.configureElicitationHandlers({ form, url })`. Connections advertise exactly the modes with configured handlers to servers at the `initialize` handshake. - - - -```ts -import { Agent } from "agents"; -import type { ElicitRequest, ElicitResult } from "agents/mcp"; - -class MyAgent extends Agent { - onStart() { - this.mcp.configureElicitationHandlers({ - url: async ( - request: ElicitRequest, - serverId: string, - ): Promise => { - // Deliver the link to the user outside the model's context - await this.notifyUser(request.params.url, request.params.message); - return { action: "accept", content: {} }; - }, - form: async ( - request: ElicitRequest, - serverId: string, - ): Promise => { - // Collect the fields described by request.params.requestedSchema - return { action: "decline", content: {} }; - }, - }); - } -} -``` - - - -The advertised modes are persisted with each MCP server, so connections restored after Durable Object hibernation advertise the same modes and the handlers reattach when `onStart()` runs. Agents without configured handlers advertise no elicitation capability, so servers use their non-elicitation fallbacks instead of sending requests the agent cannot answer. - -An explicitly declared `client.capabilities.elicitation` on `addMcpServer` is honored, persisted with the server registration, and survives hibernation, so you can narrow the advertised modes when your agent should only receive specific ones. - -Refer to [Elicitation in the MCP client documentation](/agents/model-context-protocol/apis/client-api/#elicitation) for the full guide, including a pattern for forwarding elicitation requests to a browser UI. 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 end to end. - -### Upgrade - -To update to the latest version: - - 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 2b67f24fe58..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. Agents acting as MCP clients can respond to elicitation (both url and form mode) by registering an [MCP elicitation handler](/agents/model-context-protocol/apis/client-api/#elicitation). +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 40d698539e2..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 @@ -316,7 +316,9 @@ export class MyAgent extends Agent { ## Elicitation -MCP servers can request input from your agent during a tool call using [elicitation](https://modelcontextprotocol.io/specification/latest/client/elicitation). To respond, register an elicitation handler with `this.mcp.configureElicitationHandlers()` in `onStart()`: +[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()`: @@ -327,76 +329,105 @@ import type { ElicitRequest, ElicitResult } from "agents/mcp"; class MyAgent extends Agent { onStart() { this.mcp.configureElicitationHandlers({ - url: async ( - request: ElicitRequest, - serverId: string, - ): Promise => { - // Deliver the URL to the user out-of-band (chat message, email, - // UI notification) so it never enters model context - await this.notifyUser(request.params.url, request.params.message); - return { action: "accept", content: {} }; - }, - form: async ( - request: ElicitRequest, - serverId: string, - ): Promise => { - // Collect the fields described by request.params.requestedSchema, - // or decline if your surface cannot prompt the user - return { action: "decline", content: {} }; - }, + 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}`, + ); + } } ``` -Register handlers in `onStart()`. The advertised modes are persisted with each MCP server, so connections restored after hibernation advertise the same modes at the `initialize` handshake and the handlers reattach when `onStart()` runs. The `serverId` argument identifies which server sent the request, so one agent can respond differently per server. +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. -### Elicitation modes +### Capability negotiation and hibernation -Elicitation requests arrive in one of two modes, each with its own handler: +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. -**Form mode** (the `form` handler) asks the user for structured data. The request carries a `requestedSchema` describing the fields, and an accepting response returns matching `content`: +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 -this.mcp.configureElicitationHandlers({ - form: async (request) => { - // Collect values matching request.params.requestedSchema from the user, - // then accept with the collected fields - return { action: "accept", content: { amount: 5 } }; +await this.addMcpServer("portal", "https://portal.example.com/mcp", { + client: { + capabilities: { + elicitation: { form: {} }, + }, }, }); ``` -**URL mode** (the `url` handler) asks the user to visit a URL, for example an authorization or checkout page. The MCP specification requires servers to use url mode for sensitive URLs so that they stay out of the model's context instead of appearing in tool-result text. Your handler delivers the URL to the user and returns `{ action: "accept" }` once it has done so. +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. -Servers only send elicitation modes your client advertised during the `initialize` handshake, and connections advertise exactly the modes with configured handlers: configure `form` only and servers will not send url-mode requests, and the other way around. Connections without any handlers advertise no elicitation capability, so servers use their non-elicitation fallbacks instead. Configuring handlers is how an agent opts into elicitation. +### Form mode -To narrow the advertised modes, declare them explicitly when adding the server. An explicit declaration always wins and is persisted with the server options: +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 -await this.addMcpServer("portal", "https://portal.example.com/mcp", { - client: { - capabilities: { - // Form mode only, regardless of which handlers are configured - elicitation: { form: {} }, - }, +this.mcp.configureElicitationHandlers({ + form: async (request) => { + const content = await showFormToUser(request.params.requestedSchema); + return content ? { action: "accept", content } : { action: "cancel" }; }, }); ``` -### Forwarding elicitation to a UI +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: -Elicitation handlers return a promise, but the answer usually comes from a human. A common pattern is to broadcast the request to connected clients and resolve the promise when one of them responds. The same function can serve both modes: +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: @@ -404,60 +435,67 @@ Elicitation handlers return a promise, but the answer usually comes from a human 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< - string, - (result: ElicitResult) => void - >(); + private pendingElicitations = new Map(); onStart() { - const forwardToBrowser = (request: ElicitRequest, serverId: string) => { - const id = crypto.randomUUID(); - - const result = new Promise((resolve) => { - this.pendingElicitations.set(id, resolve); - // Do not hold the tool call open forever if nobody answers - setTimeout(() => { - if (this.pendingElicitations.delete(id)) { - resolve({ action: "cancel", content: {} }); - } - }, 300_000); - }); - - // The UI renders a form from request.params.requestedSchema - // (or a link for url mode) and calls respondToElicitation - this.broadcast( - JSON.stringify({ - type: "mcp-elicitation", - id, - serverId, - params: request.params, - }), - ); - - return result; - }; - this.mcp.configureElicitationHandlers({ - form: forwardToBrowser, - url: forwardToBrowser, + 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 resolve = this.pendingElicitations.get(id); - if (resolve) { - this.pendingElicitations.delete(id); - resolve(result); - } + const pending = this.pendingElicitations.get(id); + if (!pending) return; + + this.pendingElicitations.delete(id); + clearTimeout(pending.timeout); + pending.resolve(result); } } ``` -Refer to the [`mcp-client` example](https://github.com/cloudflare/agents/tree/main/examples/mcp-client) for a complete browser UI implementing this pattern, and the [`mcp-elicitation` example](https://github.com/cloudflare/agents/tree/main/examples/mcp-elicitation) for a server that sends both elicitation modes. To send elicitation requests from your own MCP server, refer to [`elicitInput`](/agents/model-context-protocol/apis/agent-api/#elicitinputoptions-context). +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