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