Skip to content

Commit 7f0b4d1

Browse files
docs: sync changes from cloudflare/agents#620
- Add persistent transport state documentation to transport.mdx - Add new guide for MCP elicitation support - Update chatgpt-app.mdx with clarifying comment Synced from: cloudflare/agents#620
1 parent c91c3c5 commit 7f0b4d1

File tree

3 files changed

+391
-0
lines changed

3 files changed

+391
-0
lines changed

src/content/docs/agents/guides/chatgpt-app.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,7 @@ server.registerTool(
352352
}
353353
);
354354

355+
// Create MCP handler - automatically handles transport
355356
const mcpHandler = createMcpHandler(server);
356357

357358
export default {
Lines changed: 332 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,332 @@
1+
---
2+
pcx_content_type: tutorial
3+
title: Build an MCP Server with Elicitation
4+
sidebar:
5+
order: 7
6+
---
7+
8+
import { Render, TypeScriptExample } from "~/components";
9+
10+
## Overview
11+
12+
MCP elicitation allows your MCP server to request additional input from users during tool execution. This is useful when you need to collect information dynamically, such as:
13+
14+
- Asking for user confirmation before performing an action
15+
- Collecting form data with multiple fields
16+
- Requesting additional context based on the current operation
17+
18+
This guide shows you how to build an MCP server that uses elicitation to request user input during tool execution.
19+
20+
## What you'll build
21+
22+
You will create an MCP server with a counter tool that:
23+
1. Asks the user for confirmation before incrementing
24+
2. Uses elicitation to request the increment amount
25+
3. Persists state using Durable Objects
26+
27+
## Prerequisites
28+
29+
Before you begin, you will need:
30+
31+
- A [Cloudflare account](https://dash.cloudflare.com/sign-up)
32+
- [Node.js](https://nodejs.org/) installed (v18 or later)
33+
- Basic knowledge of TypeScript
34+
35+
## 1. Set up your project
36+
37+
Create a new project and install dependencies:
38+
39+
```sh
40+
npm create cloudflare@latest my-mcp-elicitation
41+
cd my-mcp-elicitation
42+
npm install agents @modelcontextprotocol/sdk zod
43+
```
44+
45+
## 2. Configure your Worker
46+
47+
Update your `wrangler.jsonc` to configure Durable Objects:
48+
49+
```jsonc
50+
{
51+
"name": "mcp-elicitation",
52+
"main": "src/index.ts",
53+
"compatibility_date": "2025-03-14",
54+
"compatibility_flags": ["nodejs_compat"],
55+
"durable_objects": {
56+
"bindings": [
57+
{
58+
"name": "MyAgent",
59+
"class_name": "MyAgent"
60+
}
61+
]
62+
},
63+
"migrations": [
64+
{
65+
"tag": "v1",
66+
"new_sqlite_classes": ["MyAgent"]
67+
}
68+
]
69+
}
70+
```
71+
72+
## 3. Build the MCP server with elicitation
73+
74+
Create your agent with elicitation support in `src/index.ts`:
75+
76+
<TypeScriptExample>
77+
78+
```ts title="src/index.ts"
79+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
80+
import {
81+
createMcpHandler,
82+
type TransportState,
83+
WorkerTransport
84+
} from "agents/mcp";
85+
import * as z from "zod";
86+
import { Agent, getAgentByName } from "agents";
87+
import { CfWorkerJsonSchemaValidator } from "@modelcontextprotocol/sdk/validation/cfworker-provider.js";
88+
89+
const STATE_KEY = "mcp_transport_state";
90+
91+
type Env = {
92+
MyAgent: DurableObjectNamespace<MyAgent>;
93+
};
94+
95+
interface State {
96+
counter: number;
97+
}
98+
99+
export class MyAgent extends Agent<Env, State> {
100+
server = new McpServer(
101+
{
102+
name: "Counter with Elicitation",
103+
version: "1.0.0"
104+
},
105+
{
106+
jsonSchemaValidator: new CfWorkerJsonSchemaValidator()
107+
}
108+
);
109+
110+
// Configure transport with persistent storage
111+
transport = new WorkerTransport({
112+
sessionIdGenerator: () => this.name,
113+
storage: {
114+
get: () => {
115+
return this.ctx.storage.kv.get<TransportState>(STATE_KEY);
116+
},
117+
set: (state: TransportState) => {
118+
this.ctx.storage.kv.put<TransportState>(STATE_KEY, state);
119+
}
120+
}
121+
});
122+
123+
initialState = {
124+
counter: 0
125+
};
126+
127+
onStart(): void | Promise<void> {
128+
// Register a tool that uses elicitation
129+
this.server.registerTool(
130+
"increase-counter",
131+
{
132+
description: "Increase the counter",
133+
inputSchema: {
134+
confirm: z.boolean().describe("Do you want to increase the counter?")
135+
}
136+
},
137+
async ({ confirm }) => {
138+
if (!confirm) {
139+
return {
140+
content: [{ type: "text", text: "Counter increase cancelled." }]
141+
};
142+
}
143+
144+
try {
145+
// Request additional input using elicitation
146+
const basicInfo = await this.server.server.elicitInput({
147+
message: "By how much do you want to increase the counter?",
148+
requestedSchema: {
149+
type: "object",
150+
properties: {
151+
amount: {
152+
type: "number",
153+
title: "Amount",
154+
description: "The amount to increase the counter by",
155+
minLength: 1
156+
}
157+
},
158+
required: ["amount"]
159+
}
160+
});
161+
162+
// Handle user response
163+
if (basicInfo.action !== "accept" || !basicInfo.content) {
164+
return {
165+
content: [{ type: "text", text: "Counter increase cancelled." }]
166+
};
167+
}
168+
169+
if (basicInfo.content.amount && Number(basicInfo.content.amount)) {
170+
this.setState({
171+
...this.state,
172+
counter: this.state.counter + Number(basicInfo.content.amount)
173+
});
174+
175+
return {
176+
content: [
177+
{
178+
type: "text",
179+
text: `Counter increased by ${basicInfo.content.amount}, current value is ${this.state.counter}`
180+
}
181+
]
182+
};
183+
}
184+
185+
return {
186+
content: [
187+
{ type: "text", text: "Counter increase failed, invalid amount." }
188+
]
189+
};
190+
} catch (error) {
191+
console.log(error);
192+
return {
193+
content: [{ type: "text", text: "Counter increase failed." }]
194+
};
195+
}
196+
}
197+
);
198+
}
199+
200+
async onMcpRequest(request: Request) {
201+
return createMcpHandler(this.server as any, {
202+
transport: this.transport
203+
})(request, this.env, {} as ExecutionContext);
204+
}
205+
}
206+
207+
export default {
208+
async fetch(request: Request, env: Env, _ctx: ExecutionContext) {
209+
const sessionId =
210+
request.headers.get("mcp-session-id") ?? crypto.randomUUID();
211+
const agent = await getAgentByName(env.MyAgent, sessionId);
212+
return await agent.onMcpRequest(request);
213+
}
214+
};
215+
```
216+
217+
</TypeScriptExample>
218+
219+
## 4. Deploy your MCP server
220+
221+
Deploy your MCP server to Cloudflare:
222+
223+
```sh
224+
npx wrangler deploy
225+
```
226+
227+
After deployment, you will see your server URL:
228+
229+
```
230+
https://mcp-elicitation.YOUR_SUBDOMAIN.workers.dev
231+
```
232+
233+
## 5. Test your MCP server
234+
235+
You can test your MCP server using any MCP client that supports elicitation. When you call the `increase-counter` tool with `confirm: true`, the server will prompt you for the increment amount through the elicitation API.
236+
237+
## Key concepts
238+
239+
### Elicitation API
240+
241+
The `elicitInput` method requests additional information from the user during tool execution:
242+
243+
```ts
244+
const result = await this.server.server.elicitInput({
245+
message: "By how much do you want to increase the counter?",
246+
requestedSchema: {
247+
type: "object",
248+
properties: {
249+
amount: {
250+
type: "number",
251+
title: "Amount",
252+
description: "The amount to increase the counter by"
253+
}
254+
},
255+
required: ["amount"]
256+
}
257+
});
258+
```
259+
260+
The method returns a result with:
261+
- `action`: One of `"accept"`, `"decline"`, or `"cancel"`
262+
- `content`: The user's input (only present when `action` is `"accept"`)
263+
264+
### Persistent transport state
265+
266+
The example uses `WorkerTransport` with Durable Object storage to persist transport state across requests:
267+
268+
```ts
269+
transport = new WorkerTransport({
270+
sessionIdGenerator: () => this.name,
271+
storage: {
272+
get: () => {
273+
return this.ctx.storage.kv.get<TransportState>(STATE_KEY);
274+
},
275+
set: (state: TransportState) => {
276+
this.ctx.storage.kv.put<TransportState>(STATE_KEY, state);
277+
}
278+
}
279+
});
280+
```
281+
282+
This ensures that elicitation requests maintain continuity even after worker restarts.
283+
284+
### Schema-based forms
285+
286+
The `requestedSchema` parameter defines the structure of the form presented to the user. You can specify:
287+
288+
- **Simple fields**: Text, numbers, booleans
289+
- **Enums**: Dropdown selections
290+
- **Required fields**: Fields that must be filled
291+
- **Descriptions**: Help text for each field
292+
293+
Example with multiple field types:
294+
295+
```ts
296+
const userInfo = await this.server.server.elicitInput({
297+
message: "Create user account:",
298+
requestedSchema: {
299+
type: "object",
300+
properties: {
301+
email: {
302+
type: "string",
303+
format: "email",
304+
title: "Email Address"
305+
},
306+
role: {
307+
type: "string",
308+
title: "Role",
309+
enum: ["viewer", "editor", "admin"],
310+
enumNames: ["Viewer", "Editor", "Admin"]
311+
},
312+
sendWelcome: {
313+
type: "boolean",
314+
title: "Send Welcome Email"
315+
}
316+
},
317+
required: ["email", "role"]
318+
}
319+
});
320+
```
321+
322+
## Next steps
323+
324+
- Learn more about [MCP tools](/agents/model-context-protocol/tools)
325+
- Explore [MCP transport options](/agents/model-context-protocol/transport)
326+
- Build an [MCP client](/agents/guides/build-mcp-client)
327+
328+
## Related resources
329+
330+
- [MCP Specification - Elicitation](https://spec.modelcontextprotocol.io/specification/draft/client/elicitation)
331+
- [Agents SDK](/agents/api-reference/agents-api)
332+
- [Example code on GitHub](https://github.com/cloudflare/agents/tree/main/examples/mcp-elicitation)

0 commit comments

Comments
 (0)