Skip to content

Commit 9d66392

Browse files
Sync docs for PR #620: feat: elicit-me example
This commit syncs documentation changes from cloudflare/agents PR #620. Changes: - Add new elicitation.mdx documentation covering MCP elicitation feature - Update transport.mdx with storage and authContext configuration options - Update authorization.mdx to reflect new API where auth is accessed via extra.authInfo Related PR: cloudflare/agents#620
1 parent 2cbf4dc commit 9d66392

File tree

3 files changed

+333
-10
lines changed

3 files changed

+333
-10
lines changed

src/content/docs/agents/model-context-protocol/authorization.mdx

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -204,9 +204,9 @@ You can implement fine-grained authorization controls for your MCP tools based o
204204
```js
205205
// Create a wrapper function to check permissions
206206
function requirePermission(permission, handler) {
207-
return async (request, context) => {
207+
return async (args, extra) => {
208208
// Check if user has the required permission
209-
const userPermissions = context.props.permissions || [];
209+
const userPermissions = extra.authInfo?.extra?.permissions || [];
210210
if (!userPermissions.includes(permission)) {
211211
return {
212212
content: [{ type: "text", text: `Permission denied: requires ${permission}` }],
@@ -215,7 +215,7 @@ function requirePermission(permission, handler) {
215215
}
216216

217217
// If permission check passes, execute the handler
218-
return handler(request, context);
218+
return handler(args, extra);
219219
};
220220
}
221221

@@ -231,20 +231,23 @@ async init() {
231231
"adminAction",
232232
"Administrative action requiring special permission",
233233
{ /* parameters */ },
234-
requirePermission("admin", async (req) => {
234+
requirePermission("admin", async (args, extra) => {
235235
// Only executes if user has "admin" permission
236236
return {
237237
content: [{ type: "text", text: "Admin action completed" }]
238238
};
239239
})
240240
);
241241

242-
// Conditionally register tools based on user permissions
243-
if (this.props.permissions?.includes("special_feature")) {
244-
this.server.tool("specialTool", "Special feature", {}, async () => {
245-
// This tool only appears for users with the special_feature permission
246-
});
247-
}
242+
// Access auth context directly in tools
243+
this.server.tool("whoami", "Get current user", {}, async (_, extra) => {
244+
const auth = extra.authInfo;
245+
const username = auth?.extra?.username || "Unknown";
246+
247+
return {
248+
content: [{ type: "text", text: `Current user: ${username}` }]
249+
};
250+
});
248251
}
249252
```
250253
Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
---
2+
pcx_content_type: concept
3+
title: Elicitation
4+
tags:
5+
- MCP
6+
sidebar:
7+
order: 7
8+
---
9+
10+
import { Render, TypeScriptExample } from "~/components";
11+
12+
Elicitation is an MCP feature that allows servers to request additional input from users during tool execution. This enables interactive workflows where tools can prompt users for confirmation, collect form data, or gather additional information needed to complete an operation.
13+
14+
## Overview
15+
16+
The [MCP Elicitation specification](https://modelcontextprotocol.io/specification/draft/client/elicitation) defines a standard way for MCP servers to request user input through a structured schema. When a tool needs additional information, it can use `elicitInput()` to:
17+
18+
- Request user confirmation before performing sensitive operations
19+
- Collect structured form data (text fields, dropdowns, checkboxes)
20+
- Validate input through JSON Schema
21+
- Handle user responses (accept, decline, or cancel)
22+
23+
## Basic example
24+
25+
Here's a simple example of an MCP server that uses elicitation to request confirmation before incrementing a counter:
26+
27+
<TypeScriptExample>
28+
29+
```ts title="src/index.ts"
30+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
31+
import { createMcpHandler, WorkerTransport } from "agents/mcp";
32+
import { Agent, getAgentByName } from "agents";
33+
import { z } from "zod";
34+
35+
interface State {
36+
counter: number;
37+
}
38+
39+
export class MyAgent extends Agent<Env, State> {
40+
server = new McpServer({
41+
name: "Elicitation Demo",
42+
version: "1.0.0"
43+
});
44+
45+
transport = new WorkerTransport({
46+
sessionIdGenerator: () => this.name,
47+
storage: this.ctx.storage
48+
});
49+
50+
initialState = { counter: 0 };
51+
52+
onStart() {
53+
this.server.registerTool(
54+
"increase-counter",
55+
{
56+
description: "Increase the counter",
57+
inputSchema: z.object({
58+
amount: z.number().default(1)
59+
}).shape
60+
},
61+
async (args) => {
62+
// Request user input via elicitation
63+
const result = await this.server.server.elicitInput({
64+
message: "By how much do you want to increase the counter?",
65+
requestedSchema: {
66+
type: "object",
67+
properties: {
68+
amount: {
69+
type: "number",
70+
title: "Amount",
71+
description: "The amount to increase the counter by"
72+
}
73+
},
74+
required: ["amount"]
75+
}
76+
});
77+
78+
if (result.action !== "accept" || !result.content) {
79+
return {
80+
content: [{ type: "text", text: "Counter increase cancelled." }]
81+
};
82+
}
83+
84+
this.state.counter += Number(result.content.amount);
85+
86+
return {
87+
content: [
88+
{
89+
type: "text",
90+
text: `Counter increased by ${result.content.amount}, current value is ${this.state.counter}`
91+
}
92+
]
93+
};
94+
}
95+
);
96+
}
97+
98+
async onMcpRequest(request: Request) {
99+
return createMcpHandler(this.server, {
100+
transport: this.transport
101+
})(request, this.env, {} as ExecutionContext);
102+
}
103+
}
104+
105+
export default {
106+
async fetch(request: Request, env: Env, _ctx: ExecutionContext) {
107+
const sessionId = request.headers.get("mcp-session-id") ?? crypto.randomUUID();
108+
const agent = await getAgentByName(env.MyAgent, sessionId);
109+
return await agent.onMcpRequest(request);
110+
}
111+
};
112+
```
113+
114+
</TypeScriptExample>
115+
116+
## Elicitation response types
117+
118+
When you call `elicitInput()`, the user can respond in three ways:
119+
120+
- **accept** — User confirmed and provided the requested data
121+
- **decline** — User explicitly rejected the request
122+
- **cancel** — User dismissed without making a choice
123+
124+
Your tool should handle all three responses appropriately:
125+
126+
```ts
127+
const result = await this.server.server.elicitInput({
128+
message: "Confirm this action?",
129+
requestedSchema: {
130+
type: "object",
131+
properties: {
132+
confirmed: { type: "boolean" }
133+
},
134+
required: ["confirmed"]
135+
}
136+
});
137+
138+
if (result.action === "accept" && result.content?.confirmed) {
139+
// User confirmed - proceed with operation
140+
return { content: [{ type: "text", text: "Operation completed!" }] };
141+
} else if (result.action === "decline") {
142+
// User explicitly declined
143+
return { content: [{ type: "text", text: "Operation declined." }] };
144+
} else {
145+
// User cancelled or provided invalid data
146+
return { content: [{ type: "text", text: "Operation cancelled." }] };
147+
}
148+
```
149+
150+
## Form schemas
151+
152+
Elicitation supports various input types through JSON Schema:
153+
154+
### Text input
155+
```ts
156+
properties: {
157+
username: {
158+
type: "string",
159+
title: "Username",
160+
description: "Enter your username"
161+
}
162+
}
163+
```
164+
165+
### Email input
166+
```ts
167+
properties: {
168+
email: {
169+
type: "string",
170+
format: "email",
171+
title: "Email Address"
172+
}
173+
}
174+
```
175+
176+
### Boolean/checkbox
177+
```ts
178+
properties: {
179+
confirmed: {
180+
type: "boolean",
181+
title: "I agree to the terms"
182+
}
183+
}
184+
```
185+
186+
### Dropdown/select
187+
```ts
188+
properties: {
189+
role: {
190+
type: "string",
191+
title: "Role",
192+
enum: ["viewer", "editor", "admin"],
193+
enumNames: ["Viewer", "Editor", "Admin"]
194+
}
195+
}
196+
```
197+
198+
## Complex forms
199+
200+
You can combine multiple fields to create complex forms:
201+
202+
```ts
203+
const userInfo = await this.server.server.elicitInput({
204+
message: "Create new user account:",
205+
requestedSchema: {
206+
type: "object",
207+
properties: {
208+
username: {
209+
type: "string",
210+
title: "Username"
211+
},
212+
email: {
213+
type: "string",
214+
format: "email",
215+
title: "Email Address"
216+
},
217+
role: {
218+
type: "string",
219+
title: "Role",
220+
enum: ["viewer", "editor", "admin"],
221+
enumNames: ["Viewer", "Editor", "Admin"]
222+
},
223+
sendWelcome: {
224+
type: "boolean",
225+
title: "Send Welcome Email"
226+
}
227+
},
228+
required: ["username", "email", "role"]
229+
}
230+
});
231+
```
232+
233+
## Persistent sessions with storage
234+
235+
To maintain elicitation state across server hibernation and restarts, use the `storage` option in `WorkerTransport`:
236+
237+
```ts
238+
transport = new WorkerTransport({
239+
sessionIdGenerator: () => this.name,
240+
storage: this.ctx.storage // Persist transport state
241+
});
242+
```
243+
244+
This ensures that:
245+
- Session IDs survive hibernation
246+
- Pending elicitation requests are restored after restart
247+
- Client connections can resume seamlessly
248+
249+
## Working example
250+
251+
For a complete working example, see the [mcp-elicitation example](https://github.com/cloudflare/agents/tree/main/examples/mcp-elicitation) in the Agents SDK repository.
252+
253+
## Client support
254+
255+
Elicitation support varies by MCP client:
256+
257+
- **Claude Desktop** — Full support for elicitation
258+
- **Cursor** — Supported via [mcp-remote](https://www.npmjs.com/package/mcp-remote)
259+
- **Windsurf** — Supported via [mcp-remote](https://www.npmjs.com/package/mcp-remote)
260+
261+
Check your MCP client's documentation for specific elicitation capabilities.

src/content/docs/agents/model-context-protocol/transport.mdx

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,65 @@ To use apiHandlers, update to @cloudflare/workers-oauth-provider v0.0.4 or later
105105
106106
With these few changes, your MCP server will support both transport methods, making it compatible with both existing and new clients.
107107
108+
## Advanced transport configuration
109+
110+
### Persistent sessions with storage
111+
112+
When using `createMcpHandler` with `WorkerTransport`, you can enable session persistence across server hibernation and restarts by providing the `storage` option:
113+
114+
```ts
115+
import { createMcpHandler, WorkerTransport } from "agents/mcp";
116+
117+
const transport = new WorkerTransport({
118+
sessionIdGenerator: () => this.name,
119+
storage: this.ctx.storage // Persist transport state in Durable Object storage
120+
});
121+
122+
async onMcpRequest(request: Request) {
123+
return createMcpHandler(this.server, {
124+
transport: this.transport
125+
})(request, this.env, {} as ExecutionContext);
126+
}
127+
```
128+
129+
This ensures that:
130+
- Session IDs survive server hibernation
131+
- Protocol version negotiation is preserved
132+
- Elicitation state persists across restarts
133+
- Connections can resume seamlessly
134+
135+
### Custom authentication context
136+
137+
When using `createMcpHandler`, you can provide custom authentication context that will be available to your tools:
138+
139+
```ts
140+
import { createMcpHandler } from "agents/mcp";
141+
142+
return createMcpHandler(server, {
143+
authContext: {
144+
props: {
145+
userId: "user123",
146+
username: "john",
147+
148+
}
149+
}
150+
})(request, env, ctx);
151+
```
152+
153+
Your tools can then access this context via the `extra` parameter:
154+
155+
```ts
156+
server.tool("whoami", "Get current user", {}, async (_, extra) => {
157+
const auth = extra.authInfo;
158+
return {
159+
content: [{
160+
type: "text",
161+
text: `User: ${auth?.extra?.username}`
162+
}]
163+
};
164+
});
165+
```
166+
108167
### Testing with MCP clients
109168
While most MCP clients have not yet adopted the new Streamable HTTP transport, you can start testing it today using [`mcp-remote`](https://www.npmjs.com/package/mcp-remote), an adapter that lets MCP clients that otherwise only support local connections work with remote MCP servers.
110169

0 commit comments

Comments
 (0)