Skip to content
Cloudflare Docs

Build an MCP Server with Elicitation

Overview

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:

  • Asking for user confirmation before performing an action
  • Collecting form data with multiple fields
  • Requesting additional context based on the current operation

This guide shows you how to build an MCP server that uses elicitation to request user input during tool execution.

What you'll build

You will create an MCP server with a counter tool that:

  1. Asks the user for confirmation before incrementing
  2. Uses elicitation to request the increment amount
  3. Persists state using Durable Objects

Prerequisites

Before you begin, you will need:

1. Set up your project

Create a new project and install dependencies:

Terminal window
npm create cloudflare@latest my-mcp-elicitation
cd my-mcp-elicitation
npm install agents @modelcontextprotocol/sdk zod

2. Configure your Worker

Update your wrangler.jsonc to configure Durable Objects:

{
"name": "mcp-elicitation",
"main": "src/index.ts",
"compatibility_date": "2025-03-14",
"compatibility_flags": ["nodejs_compat"],
"durable_objects": {
"bindings": [
{
"name": "MyAgent",
"class_name": "MyAgent"
}
]
},
"migrations": [
{
"tag": "v1",
"new_sqlite_classes": ["MyAgent"]
}
]
}

3. Build the MCP server with elicitation

Create your agent with elicitation support in src/index.ts:

JavaScript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { createMcpHandler, WorkerTransport } from "agents/mcp";
import * as z from "zod";
import { Agent, getAgentByName } from "agents";
import { CfWorkerJsonSchemaValidator } from "@modelcontextprotocol/sdk/validation/cfworker-provider.js";
const STATE_KEY = "mcp_transport_state";
export class MyAgent extends Agent {
server = new McpServer(
{
name: "Counter with Elicitation",
version: "1.0.0",
},
{
jsonSchemaValidator: new CfWorkerJsonSchemaValidator(),
},
);
// Configure transport with persistent storage
transport = new WorkerTransport({
sessionIdGenerator: () => this.name,
storage: {
get: () => {
return this.ctx.storage.kv.get(STATE_KEY);
},
set: (state) => {
this.ctx.storage.kv.put(STATE_KEY, state);
},
},
});
initialState = {
counter: 0,
};
onStart() {
// Register a tool that uses elicitation
this.server.registerTool(
"increase-counter",
{
description: "Increase the counter",
inputSchema: {
confirm: z.boolean().describe("Do you want to increase the counter?"),
},
},
async ({ confirm }) => {
if (!confirm) {
return {
content: [{ type: "text", text: "Counter increase cancelled." }],
};
}
try {
// Request additional input using elicitation
const basicInfo = await this.server.server.elicitInput({
message: "By how much do you want to increase the counter?",
requestedSchema: {
type: "object",
properties: {
amount: {
type: "number",
title: "Amount",
description: "The amount to increase the counter by",
minLength: 1,
},
},
required: ["amount"],
},
});
// Handle user response
if (basicInfo.action !== "accept" || !basicInfo.content) {
return {
content: [{ type: "text", text: "Counter increase cancelled." }],
};
}
if (basicInfo.content.amount && Number(basicInfo.content.amount)) {
this.setState({
...this.state,
counter: this.state.counter + Number(basicInfo.content.amount),
});
return {
content: [
{
type: "text",
text: `Counter increased by ${basicInfo.content.amount}, current value is ${this.state.counter}`,
},
],
};
}
return {
content: [
{
type: "text",
text: "Counter increase failed, invalid amount.",
},
],
};
} catch (error) {
console.log(error);
return {
content: [{ type: "text", text: "Counter increase failed." }],
};
}
},
);
}
async onMcpRequest(request) {
return createMcpHandler(this.server, {
transport: this.transport,
})(request, this.env, {});
}
}
export default {
async fetch(request, env, _ctx) {
const sessionId =
request.headers.get("mcp-session-id") ?? crypto.randomUUID();
const agent = await getAgentByName(env.MyAgent, sessionId);
return await agent.onMcpRequest(request);
},
};

4. Deploy your MCP server

Deploy your MCP server to Cloudflare:

Terminal window
npx wrangler deploy

After deployment, you will see your server URL:

https://mcp-elicitation.YOUR_SUBDOMAIN.workers.dev

5. Test your MCP server

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.

Key concepts

Elicitation API

The elicitInput method requests additional information from the user during tool execution:

TypeScript
const result = await this.server.server.elicitInput({
message: "By how much do you want to increase the counter?",
requestedSchema: {
type: "object",
properties: {
amount: {
type: "number",
title: "Amount",
description: "The amount to increase the counter by"
}
},
required: ["amount"]
}
});

The method returns a result with:

  • action: One of "accept", "decline", or "cancel"
  • content: The user's input (only present when action is "accept")

Persistent transport state

The example uses WorkerTransport with Durable Object storage to persist transport state across requests:

TypeScript
transport = new WorkerTransport({
sessionIdGenerator: () => this.name,
storage: {
get: () => {
return this.ctx.storage.kv.get<TransportState>(STATE_KEY);
},
set: (state: TransportState) => {
this.ctx.storage.kv.put<TransportState>(STATE_KEY, state);
}
}
});

This ensures that elicitation requests maintain continuity even after worker restarts.

Schema-based forms

The requestedSchema parameter defines the structure of the form presented to the user. You can specify:

  • Simple fields: Text, numbers, booleans
  • Enums: Dropdown selections
  • Required fields: Fields that must be filled
  • Descriptions: Help text for each field

Example with multiple field types:

TypeScript
const userInfo = await this.server.server.elicitInput({
message: "Create user account:",
requestedSchema: {
type: "object",
properties: {
email: {
type: "string",
format: "email",
title: "Email Address"
},
role: {
type: "string",
title: "Role",
enum: ["viewer", "editor", "admin"],
enumNames: ["Viewer", "Editor", "Admin"]
},
sendWelcome: {
type: "boolean",
title: "Send Welcome Email"
}
},
required: ["email", "role"]
}
});

Next steps