Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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()`:

<TypeScriptExample>

```ts
import { Agent } from "agents";
import type { ElicitRequest, ElicitResult } from "agents/mcp";

export class MyAgent extends Agent<Env> {
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<ElicitResult> {
// Show the request in your UI and resolve after the user responds.
throw new Error(
`Implement elicitation for ${serverId}: ${request.params.message}`,
);
}
}
```

</TypeScriptExample>

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:

<PackageManagers pkg="agents@latest" />
205 changes: 79 additions & 126 deletions src/content/docs/agents/model-context-protocol/apis/agent-api.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -257,171 +257,124 @@ export class MyMCP extends McpAgent<Env, State, {}> {

</TypeScriptExample>

## 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:**
<TypeScriptExample>

| 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." }] };
}

<TypeScriptExample>
const amount = Number(result.content.amount);
```

```ts
import { McpAgent } from "agents/mcp";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
</TypeScriptExample>

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<Env, State, {}> {
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,
});
<TypeScriptExample>

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.",
},
],
};
```

</TypeScriptExample>

### 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.

<TypeScriptExample>

```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." }] };
}
```

</TypeScriptExample>

:::note[MCP client support]
Elicitation requires MCP client support. Not all MCP clients implement the elicitation capability. Check the client documentation for compatibility.
Not all MCP clients implement elicitation. Check the client before depending on it and provide a fallback when appropriate. Agents acting as MCP clients can handle both modes through [MCP client elicitation handlers](/agents/model-context-protocol/apis/client-api/#elicitation).
:::

For more human-in-the-loop patterns including workflow-based approval, refer to [Human-in-the-loop patterns](/agents/concepts/agentic-patterns/human-in-the-loop/).
For more human-in-the-loop patterns, refer to [Human-in-the-loop patterns](/agents/concepts/agentic-patterns/human-in-the-loop/).

## Next steps

Expand Down
Loading