Skip to content

Commit e6d64d0

Browse files
authored
Agents: document MCP client and server elicitation (#31974)
* 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 * clarify: both url and form mode * polish elicitation docs and changelog wording * restore technical register per repo style guide, plain-language security explanation * docs: point elicitation spec links at /specification/latest * docs: clarify that onElicitRequest handles both modes and is required * docs: describe final elicitation capability behavior * docs: elicitation via configureElicitationHandler({ form, url }) * docs: finish grouped elicitation handler docs * docs: describe persisted elicitation capability restore behavior * docs: rename configureElicitationHandler to configureElicitationHandlers * [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.
1 parent 13618c9 commit e6d64d0

3 files changed

Lines changed: 331 additions & 126 deletions

File tree

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
---
2+
title: Agents can respond to MCP elicitation requests
3+
description: Agents SDK MCP clients can now handle form and URL elicitation requests from connected servers.
4+
products:
5+
- agents
6+
- workers
7+
date: 2026-07-13
8+
---
9+
10+
import { PackageManagers, TypeScriptExample } from "~/components";
11+
12+
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.
13+
14+
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.
15+
16+
```mermaid
17+
sequenceDiagram
18+
participant User
19+
participant Agent as Agent (MCP client)
20+
participant Server as MCP server
21+
participant Browser
22+
23+
Server->>Agent: elicitation/create
24+
Agent->>User: Show server, reason, and input or URL
25+
User->>Agent: Submit, open, decline, or cancel
26+
Agent->>Browser: Open URL after consent (URL mode)
27+
Agent->>Server: accept, decline, or cancel
28+
Server-->>Agent: Optional URL completion notification
29+
```
30+
31+
Register a handler for each mode your Agent supports in `onStart()`:
32+
33+
<TypeScriptExample>
34+
35+
```ts
36+
import { Agent } from "agents";
37+
import type { ElicitRequest, ElicitResult } from "agents/mcp";
38+
39+
export class MyAgent extends Agent<Env> {
40+
onStart() {
41+
this.mcp.configureElicitationHandlers({
42+
form: (request, serverId) => this.forwardToUser(request, serverId),
43+
url: (request, serverId) => this.forwardToUser(request, serverId),
44+
});
45+
}
46+
47+
private forwardToUser(
48+
request: ElicitRequest,
49+
serverId: string,
50+
): Promise<ElicitResult> {
51+
// Show the request in your UI and resolve after the user responds.
52+
throw new Error(
53+
`Implement elicitation for ${serverId}: ${request.params.message}`,
54+
);
55+
}
56+
}
57+
```
58+
59+
</TypeScriptExample>
60+
61+
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.
62+
63+
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.
64+
65+
## Upgrade
66+
67+
To update to this release:
68+
69+
<PackageManagers pkg="agents@latest" />

src/content/docs/agents/model-context-protocol/apis/agent-api.mdx

Lines changed: 79 additions & 126 deletions
Original file line numberDiff line numberDiff line change
@@ -257,171 +257,124 @@ export class MyMCP extends McpAgent<Env, State, {}> {
257257

258258
</TypeScriptExample>
259259

260-
## Elicitation (human-in-the-loop)
260+
## Elicitation
261261

262-
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.
262+
[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:
263263

264-
### When to use elicitation
264+
- **Form mode** collects structured, non-sensitive data through the client.
265+
- **URL mode** sends the user to an out-of-band interaction, such as third-party authorization or payment.
265266

266-
- Request structured input that was not part of the original tool call
267-
- Confirm high-stakes operations before proceeding
268-
- Gather additional context or preferences mid-execution
267+
The client must advertise support for a mode before the server sends it.
269268

270-
### `elicitInput(options, context)`
269+
### Form mode
271270

272-
Request structured input from the user during tool execution.
271+
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:
273272

274-
**Parameters:**
273+
<TypeScriptExample>
275274

276-
| Parameter | Type | Description |
277-
| -------------------------- | ----------- | -------------------------------------------- |
278-
| `options.message` | string | Message explaining what input is needed |
279-
| `options.requestedSchema` | JSON Schema | Schema defining the expected input structure |
280-
| `context.relatedRequestId` | string | The `extra.requestId` from the tool handler |
275+
```ts
276+
const result = await this.server.server.elicitInput(
277+
{
278+
mode: "form",
279+
message: "By how much do you want to increase the counter?",
280+
requestedSchema: {
281+
type: "object",
282+
properties: {
283+
amount: {
284+
type: "number",
285+
title: "Amount",
286+
minimum: 1,
287+
maximum: 100,
288+
},
289+
},
290+
required: ["amount"],
291+
},
292+
},
293+
{ relatedRequestId: extra.requestId },
294+
);
281295

282-
**Returns:** `Promise<{ action: "accept" | "decline", content?: object }>`
296+
if (result.action !== "accept" || !result.content) {
297+
return { content: [{ type: "text", text: "Counter unchanged." }] };
298+
}
283299

284-
<TypeScriptExample>
300+
const amount = Number(result.content.amount);
301+
```
285302

286-
```ts
287-
import { McpAgent } from "agents/mcp";
288-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
289-
import { z } from "zod";
303+
</TypeScriptExample>
290304

291-
type State = { counter: number };
305+
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.
292306

293-
export class CounterMCP extends McpAgent<Env, State, {}> {
294-
server = new McpServer({
295-
name: "counter-server",
296-
version: "1.0.0",
297-
});
307+
### URL mode
298308

299-
initialState: State = { counter: 0 };
309+
Use URL mode for interactions that must happen outside the MCP client. The request includes a message, the URL, and a unique `elicitationId`:
300310

301-
async init() {
302-
this.server.tool(
303-
"increase-counter",
304-
"Increase the counter by a user-specified amount",
305-
{ confirm: z.boolean().describe("Do you want to increase the counter?") },
306-
async ({ confirm }, extra) => {
307-
if (!confirm) {
308-
return { content: [{ type: "text", text: "Cancelled." }] };
309-
}
310-
311-
// Request additional input from the user
312-
const userInput = await this.server.server.elicitInput(
313-
{
314-
message: "By how much do you want to increase the counter?",
315-
requestedSchema: {
316-
type: "object",
317-
properties: {
318-
amount: {
319-
type: "number",
320-
title: "Amount",
321-
description: "The amount to increase the counter by",
322-
},
323-
},
324-
required: ["amount"],
325-
},
326-
},
327-
{ relatedRequestId: extra.requestId },
328-
);
329-
330-
// Check if user accepted or cancelled
331-
if (userInput.action !== "accept" || !userInput.content) {
332-
return { content: [{ type: "text", text: "Cancelled." }] };
333-
}
334-
335-
// Use the input
336-
const amount = Number(userInput.content.amount);
337-
this.setState({
338-
...this.state,
339-
counter: this.state.counter + amount,
340-
});
311+
<TypeScriptExample>
341312

342-
return {
343-
content: [
344-
{
345-
type: "text",
346-
text: `Counter increased by ${amount}, now at ${this.state.counter}`,
347-
},
348-
],
349-
};
350-
},
351-
);
352-
}
313+
```ts
314+
const elicitationId = crypto.randomUUID();
315+
const result = await this.server.server.elicitInput(
316+
{
317+
mode: "url",
318+
message: "Connect your account to continue.",
319+
url: `https://example.com/connect?elicitationId=${elicitationId}`,
320+
elicitationId,
321+
},
322+
{ relatedRequestId: extra.requestId },
323+
);
324+
325+
if (result.action !== "accept") {
326+
return { content: [{ type: "text", text: "Connection cancelled." }] };
353327
}
328+
329+
return {
330+
content: [
331+
{
332+
type: "text",
333+
text: "Connection page opened. Complete it in your browser.",
334+
},
335+
],
336+
};
354337
```
355338

356339
</TypeScriptExample>
357340

358-
### JSON Schema for forms
341+
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`.
359342

360-
The `requestedSchema` defines the form structure shown to the user:
343+
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.
361344

362-
```ts
363-
const schema = {
364-
type: "object",
365-
properties: {
366-
// Text input
367-
name: {
368-
type: "string",
369-
title: "Name",
370-
description: "Enter your name",
371-
},
372-
// Number input
373-
amount: {
374-
type: "number",
375-
title: "Amount",
376-
minimum: 1,
377-
maximum: 100,
378-
},
379-
// Boolean (checkbox)
380-
confirm: {
381-
type: "boolean",
382-
title: "I confirm this action",
383-
},
384-
// Enum (dropdown)
385-
priority: {
386-
type: "string",
387-
enum: ["low", "medium", "high"],
388-
title: "Priority",
389-
},
390-
},
391-
required: ["name", "amount"],
392-
};
393-
```
345+
### Handle responses
394346

395-
### Handling responses
347+
Both modes return one of three actions:
348+
349+
| Action | Meaning |
350+
| --------- | ----------------------------------------------------------------- |
351+
| `accept` | The user submitted the form or consented to open the URL. |
352+
| `decline` | The user explicitly rejected the request. |
353+
| `cancel` | The user dismissed the request without making an explicit choice. |
354+
355+
Accepted form responses include `content` that matches `requestedSchema`. URL responses omit `content`. Decline and cancel responses typically omit it.
396356

397357
<TypeScriptExample>
398358

399359
```ts
400-
const result = await this.server.server.elicitInput(
401-
{ message: "Confirm action", requestedSchema: schema },
402-
{ relatedRequestId: extra.requestId },
403-
);
404-
405360
switch (result.action) {
406361
case "accept":
407-
// User submitted the form
408-
const { name, amount } = result.content as { name: string; amount: number };
409-
// Process the input...
362+
// For form mode, validate and process result.content.
410363
break;
411-
412364
case "decline":
413-
// User cancelled
414-
return { content: [{ type: "text", text: "Operation cancelled." }] };
365+
return { content: [{ type: "text", text: "Request declined." }] };
366+
case "cancel":
367+
return { content: [{ type: "text", text: "Request dismissed." }] };
415368
}
416369
```
417370

418371
</TypeScriptExample>
419372

420373
:::note[MCP client support]
421-
Elicitation requires MCP client support. Not all MCP clients implement the elicitation capability. Check the client documentation for compatibility.
374+
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).
422375
:::
423376

424-
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/).
377+
For more human-in-the-loop patterns, refer to [Human-in-the-loop patterns](/agents/concepts/agentic-patterns/human-in-the-loop/).
425378

426379
## Next steps
427380

0 commit comments

Comments
 (0)