Skip to content

Commit c7de600

Browse files
author
Cloudflare Docs Bot
committed
docs: add MCP elicitation guide and transport configuration
- Add comprehensive guide for MCP elicitation (user input) - Document new createMcpHandler options (storage, authContext, transport) - Add examples for state persistence and custom transport configuration - Cover elicitation schema types and response handling Related to cloudflare/agents#620
1 parent 4f2f821 commit c7de600

File tree

2 files changed

+583
-0
lines changed

2 files changed

+583
-0
lines changed
Lines changed: 392 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,392 @@
1+
---
2+
pcx_content_type: concept
3+
title: User Input with MCP Elicitation
4+
tags:
5+
- MCP
6+
sidebar:
7+
order: 12
8+
---
9+
10+
import { Render, TypeScriptExample, Aside } from "~/components";
11+
12+
MCP Elicitation allows your MCP server to request input from users through interactive forms during tool execution. This is useful when you need user confirmation, additional parameters, or structured data that wasn't provided in the initial tool call.
13+
14+
## What is Elicitation?
15+
16+
Elicitation is part of the [MCP specification](https://spec.modelcontextprotocol.io/specification/draft/client/elicitation/) that enables servers to pause tool execution and request structured input from users. Common use cases include:
17+
18+
- **User confirmation** — Ask for approval before performing sensitive operations
19+
- **Additional parameters** — Request information not provided in the initial tool call
20+
- **Dynamic forms** — Collect structured data based on the execution context
21+
- **Multi-step workflows** — Guide users through complex operations with interactive steps
22+
23+
## Basic Example
24+
25+
Here's a simple MCP server that uses elicitation to confirm user actions:
26+
27+
<TypeScriptExample>
28+
29+
```ts title="src/index.ts"
30+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
31+
import { Agent } from "agents";
32+
import { createMcpHandler } from "agents/mcp";
33+
import { z } from "zod";
34+
35+
type Env = {
36+
MyAgent: DurableObjectNamespace<MyAgent>;
37+
};
38+
39+
interface State {
40+
counter: number;
41+
}
42+
43+
export class MyAgent extends Agent<Env, State> {
44+
server = new McpServer({
45+
name: "Elicitation Demo",
46+
version: "1.0.0"
47+
});
48+
49+
initialState = {
50+
counter: 0
51+
};
52+
53+
onStart(): void {
54+
this.server.registerTool(
55+
"increase-counter",
56+
{
57+
description: "Increase the counter",
58+
inputSchema: {
59+
confirm: z.boolean().describe("Do you want to increase the counter?")
60+
}
61+
},
62+
async ({ confirm }) => {
63+
if (!confirm) {
64+
return {
65+
content: [{ type: "text", text: "Counter increase cancelled." }]
66+
};
67+
}
68+
69+
// Request amount via elicitation
70+
const result = await this.server.server.elicitInput({
71+
message: "By how much do you want to increase the counter?",
72+
requestedSchema: {
73+
type: "object",
74+
properties: {
75+
amount: {
76+
type: "number",
77+
title: "Amount",
78+
description: "The amount to increase the counter by",
79+
minLength: 1
80+
}
81+
},
82+
required: ["amount"]
83+
}
84+
});
85+
86+
if (result.action !== "accept" || !result.content) {
87+
return {
88+
content: [{ type: "text", text: "Counter increase cancelled." }]
89+
};
90+
}
91+
92+
const amount = Number(result.content.amount);
93+
this.setState({
94+
...this.state,
95+
counter: this.state.counter + amount
96+
});
97+
98+
return {
99+
content: [
100+
{
101+
type: "text",
102+
text: `Counter increased by ${amount}, current value is ${this.state.counter}`
103+
}
104+
]
105+
};
106+
}
107+
);
108+
}
109+
110+
async onMcpRequest(request: Request) {
111+
return createMcpHandler(this.server)(request, this.env, {} as ExecutionContext);
112+
}
113+
}
114+
115+
export default {
116+
async fetch(request: Request, env: Env, ctx: ExecutionContext) {
117+
const sessionId = request.headers.get("mcp-session-id") ?? crypto.randomUUID();
118+
const agentId = env.MyAgent.idFromName(sessionId);
119+
const agent = env.MyAgent.get(agentId);
120+
121+
return await agent.fetch(request);
122+
}
123+
};
124+
```
125+
126+
</TypeScriptExample>
127+
128+
## Elicitation Schema
129+
130+
The `requestedSchema` follows [JSON Schema](https://json-schema.org/) format and supports various input types:
131+
132+
### Text Input
133+
134+
```ts
135+
{
136+
type: "object",
137+
properties: {
138+
username: {
139+
type: "string",
140+
title: "Username",
141+
description: "Enter your username"
142+
}
143+
},
144+
required: ["username"]
145+
}
146+
```
147+
148+
### Email Input
149+
150+
```ts
151+
{
152+
type: "object",
153+
properties: {
154+
email: {
155+
type: "string",
156+
format: "email",
157+
title: "Email Address",
158+
description: "Your email address"
159+
}
160+
},
161+
required: ["email"]
162+
}
163+
```
164+
165+
### Boolean (Checkbox)
166+
167+
```ts
168+
{
169+
type: "object",
170+
properties: {
171+
confirmed: {
172+
type: "boolean",
173+
title: "Confirm Action",
174+
description: "Check to confirm"
175+
}
176+
},
177+
required: ["confirmed"]
178+
}
179+
```
180+
181+
### Select Dropdown
182+
183+
```ts
184+
{
185+
type: "object",
186+
properties: {
187+
role: {
188+
type: "string",
189+
title: "User Role",
190+
enum: ["viewer", "editor", "admin"],
191+
enumNames: ["Viewer", "Editor", "Administrator"]
192+
}
193+
},
194+
required: ["role"]
195+
}
196+
```
197+
198+
### Number Input
199+
200+
```ts
201+
{
202+
type: "object",
203+
properties: {
204+
amount: {
205+
type: "number",
206+
title: "Amount",
207+
description: "Enter a number"
208+
}
209+
}
210+
}
211+
```
212+
213+
## Handling User Responses
214+
215+
Elicitation returns an `ElicitResult` with three possible actions:
216+
217+
- **`accept`** — User submitted the form with data in `content`
218+
- **`decline`** — User explicitly rejected the request
219+
- **`cancel`** — User dismissed the form without making a choice
220+
221+
<TypeScriptExample>
222+
223+
```ts
224+
const result = await this.server.server.elicitInput({
225+
message: "Please provide information:",
226+
requestedSchema: { /* schema */ }
227+
});
228+
229+
if (result.action === "accept" && result.content) {
230+
// User submitted data
231+
const userData = result.content;
232+
// Process the data...
233+
} else if (result.action === "decline") {
234+
// User explicitly declined
235+
return { content: [{ type: "text", text: "Action declined." }] };
236+
} else {
237+
// User cancelled (closed without choice)
238+
return { content: [{ type: "text", text: "Action cancelled." }] };
239+
}
240+
```
241+
242+
</TypeScriptExample>
243+
244+
## Multi-Field Forms
245+
246+
You can request multiple fields at once:
247+
248+
<TypeScriptExample>
249+
250+
```ts
251+
const userInfo = await this.server.server.elicitInput({
252+
message: "Create user account:",
253+
requestedSchema: {
254+
type: "object",
255+
properties: {
256+
username: {
257+
type: "string",
258+
title: "Username",
259+
description: "Choose a username"
260+
},
261+
email: {
262+
type: "string",
263+
format: "email",
264+
title: "Email Address"
265+
},
266+
role: {
267+
type: "string",
268+
title: "Role",
269+
enum: ["viewer", "editor", "admin"],
270+
enumNames: ["Viewer", "Editor", "Administrator"]
271+
},
272+
sendWelcome: {
273+
type: "boolean",
274+
title: "Send Welcome Email",
275+
description: "Send welcome email to user"
276+
}
277+
},
278+
required: ["username", "email", "role"]
279+
}
280+
});
281+
282+
if (userInfo.action === "accept" && userInfo.content) {
283+
const { username, email, role, sendWelcome } = userInfo.content;
284+
// Create user with provided information...
285+
}
286+
```
287+
288+
</TypeScriptExample>
289+
290+
## State Persistence
291+
292+
When using elicitation with [Durable Objects hibernation](/durable-objects/best-practices/websockets/#websocket-hibernation-api), you may need to persist transport state to survive hibernation periods. Use the `storage` option with `createMcpHandler`:
293+
294+
<TypeScriptExample>
295+
296+
```ts
297+
import { createMcpHandler, type TransportState } from "agents/mcp";
298+
299+
const STATE_KEY = "mcp_transport_state";
300+
301+
async onMcpRequest(request: Request) {
302+
return createMcpHandler(this.server, {
303+
storage: {
304+
get: () => {
305+
return this.ctx.storage.kv.get<TransportState>(STATE_KEY);
306+
},
307+
set: (state: TransportState) => {
308+
this.ctx.storage.kv.put<TransportState>(STATE_KEY, state);
309+
}
310+
}
311+
})(request, this.env, {} as ExecutionContext);
312+
}
313+
```
314+
315+
</TypeScriptExample>
316+
317+
This ensures that session information persists across hibernation, allowing elicitation requests to resume correctly.
318+
319+
## Best Practices
320+
321+
### 1. Provide Clear Messages
322+
323+
Make your elicitation messages descriptive and actionable:
324+
325+
```ts
326+
// Good
327+
message: "By how much do you want to increase the counter?"
328+
329+
// Less clear
330+
message: "Enter amount"
331+
```
332+
333+
### 2. Use Descriptive Field Titles
334+
335+
Help users understand what each field is for:
336+
337+
```ts
338+
{
339+
type: "string",
340+
title: "Email Address", // Clear title
341+
description: "We'll send a confirmation to this address" // Helpful context
342+
}
343+
```
344+
345+
### 3. Mark Required Fields
346+
347+
Use the `required` array to indicate mandatory fields:
348+
349+
```ts
350+
{
351+
type: "object",
352+
properties: {
353+
email: { type: "string", format: "email" },
354+
phone: { type: "string" }
355+
},
356+
required: ["email"] // Email is required, phone is optional
357+
}
358+
```
359+
360+
### 4. Handle All Response Types
361+
362+
Always handle all three response actions (`accept`, `decline`, `cancel`):
363+
364+
```ts
365+
if (result.action === "accept" && result.content) {
366+
// Process data
367+
} else if (result.action === "decline") {
368+
// Handle explicit decline
369+
} else {
370+
// Handle cancellation
371+
}
372+
```
373+
374+
### 5. Validate User Input
375+
376+
Even though the schema validates basic types, add business logic validation:
377+
378+
```ts
379+
const amount = Number(result.content.amount);
380+
if (amount <= 0) {
381+
return {
382+
content: [{ type: "text", text: "Amount must be positive." }]
383+
};
384+
}
385+
```
386+
387+
## Related Resources
388+
389+
- [MCP Specification — Elicitation](https://spec.modelcontextprotocol.io/specification/draft/client/elicitation/)
390+
- [MCP Tools](/agents/model-context-protocol/tools/)
391+
- [McpAgent API Reference](/agents/model-context-protocol/mcp-agent-api/)
392+
- [Build a Remote MCP Server](/agents/guides/remote-mcp-server/)

0 commit comments

Comments
 (0)