|
| 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. |
0 commit comments