Large Language ModelsGenerate imagesGenerate videos
MCP Elicitation Example: Client Support and Claude Code Setup
A working MCP elicitation example for client support: a TypeScript server that pauses a ticket escalation, asks the agent for priority, product area and outage status through a form, then resumes. Includes the Claude Code setup, decline and cancel handling, and a test checklist.
Your support agent types "escalate SUP-1042" into Claude Code and the tool starts running. Then it stalls, because nobody told it the priority, the product area, or whether customers are locked out. A weak setup guesses and pages the wrong team. A better one asks. That question, sent from the server back to the person at the terminal, is what MCP elicitation does. This MCP elicitation example for a client support desk shows the whole loop: the server code, the form schema, the Claude Code setup, and what to do when the agent says no.
Everything below targets the 2025-11-25 revision of the Model Context Protocol and the TypeScript SDK. The server is small enough to read in one sitting, and every part of it maps to a rule in the spec.
What MCP Elicitation Actually Does
Normally an MCP client calls a tool, the server works, and a result comes back. Elicitation adds one step in the middle. While a tool is running, the server sends an elicitation/create request to the client. The client shows the person a dialog, collects an answer, and returns it. The tool then continues with real data instead of a guess.
That makes elicitation a human in the loop primitive. The server stays in charge of what it needs. The client stays in charge of how the question looks, which servers may ask, and whether the person is allowed to refuse.
Form Mode and URL Mode
The spec defines two modes:
Form mode collects structured data in-band. The server sends a short message plus a requestedSchema, and the client renders a form from it.
URL mode sends the person to an external address for anything sensitive, such as a sign-in or a payment. The data never passes through the client. The 2025-11-25 revision introduced it.
Form schemas are deliberately small. They are flat objects with primitive properties only:
Schema type
Useful options
Typical use
string
minLength, maxLength, pattern, format (email, uri, date, date-time)
Contact email, short note
number or integer
minimum, maximum, default
Customers affected
boolean
default
"Is this an outage?"
single-select enum
enum, or oneOf with titles
Priority, product area
multi-select enum
array with minItems and maxItems
Affected platforms
Nested objects and arrays of objects are left out on purpose, so any client can draw the form without guessing.
Three Possible Answers
Every response carries an action:
accept: the person submitted the form, and content holds the values.
decline: the person said no on purpose.
cancel: the person closed the dialog without choosing.
Your server has to treat all three as normal outcomes. Most elicitation bugs come from handling only the first one.
The Client Support Scenario
Picture a support team with a helpdesk full of tickets and a small on-call rota. Agents work inside Claude Code, and an MCP server called support-desk gives the model one write action: escalate_ticket. It takes a ticket ID and hands the case to the right engineers.
Why Guessing Fails Here
The model can read a ticket and infer a priority. It will often be right. When it is wrong, a billing question lands in the incident channel, or a login outage sits in a slow queue overnight. You could widen the tool's input schema and hope the model fills every field correctly, but a tool call with invented values looks identical to one with real values.
Elicitation moves the decision to the person who owns it. The model supplies the ticket ID. The human supplies the judgment.
The Fields the Server Asks For
Five fields are enough. More than that and agents start dismissing the dialog.
Field
Type
Why it is there
priority
single-select enum
Routes to the right on-call queue
area
single-select enum
Picks the owning team
affectedCustomers
integer, 1 to 10000
Separates one user from a wide incident
customerEmail
string, format email
Lets the engineer follow up
outage
boolean
Opens the incident channel
Only priority and area are required. The others have defaults, so an agent in a hurry can accept and move on.
💡 Keep the form short. Each extra field is a reason for the agent to press cancel.
Build the Support Server
The server is one TypeScript file, a stdio transport, and two small dependencies.
Setting type to module lets the file use top-level await and ES imports.
The Escalation Tool
Save this as src/server.ts:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
const server = new McpServer({ name: "support-desk", version: "1.0.0" });
const EscalationForm = z.object({
priority: z.enum(["p1", "p2", "p3"]),
area: z.enum(["billing", "login", "api", "mobile-app"]),
affectedCustomers: z.number().int().min(1).max(10000).default(1),
customerEmail: z.string().email().optional(),
outage: z.boolean().default(false),
});
// Stub: swap in your helpdesk API call.
async function createEscalation(ticketId: string, data: z.infer<typeof EscalationForm>) {
return `ESC-${Date.now().toString(36).toUpperCase()}`;
}
const reply = (text: string, isError = false) => ({
content: [{ type: "text" as const, text }],
isError,
});
server.registerTool(
"escalate_ticket",
{
description: "Escalate a support ticket to the on-call team. Asks the agent for missing details.",
inputSchema: { ticketId: z.string().describe("Ticket ID, for example SUP-1042") },
},
async ({ ticketId }) => {
if (!server.server.getClientCapabilities()?.elicitation) {
return reply("This client cannot show forms. Ask the agent for priority and area, then retry.", true);
}
const answer = await server.server.elicitInput({
message: `Ticket ${ticketId} needs a few details before it reaches the on-call team.`,
requestedSchema: {
type: "object",
properties: {
priority: {
type: "string",
title: "Priority",
oneOf: [
{ const: "p1", title: "P1 Service down" },
{ const: "p2", title: "P2 Major feature broken" },
{ const: "p3", title: "P3 Minor issue" },
],
},
area: {
type: "string",
title: "Product area",
enum: ["billing", "login", "api", "mobile-app"],
},
affectedCustomers: {
type: "integer",
title: "Customers affected",
minimum: 1,
maximum: 10000,
default: 1,
},
customerEmail: { type: "string", format: "email", title: "Customer email" },
outage: { type: "boolean", title: "Is this an outage?", default: false },
},
required: ["priority", "area"],
},
});
if (answer.action === "decline") {
return reply(`The agent declined to escalate ${ticketId}. The ticket is unchanged.`);
}
if (answer.action === "cancel") {
return reply(`Escalation of ${ticketId} was cancelled. Nothing was changed.`);
}
const parsed = EscalationForm.safeParse(answer.content);
if (!parsed.success) {
return reply("The form answers were invalid. Ask again.", true);
}
const escalationId = await createEscalation(ticketId, parsed.data);
return reply(`Escalated ${ticketId} as ${parsed.data.priority} in ${parsed.data.area}. Reference ${escalationId}.`);
}
);
await server.connect(new StdioServerTransport());
Notice that mode is missing from the elicitInput call. Form mode is the default, which keeps the request readable by older clients.
Check Client Capabilities First
The first lines of the handler matter more than they look. A client that supports elicitation declares an elicitation capability during initialization. An empty elicitation object counts as form mode only, and a server must never send a mode the client did not declare.
If the capability is missing, the server returns a plain-text error instead of hanging. The model reads that message and asks the agent in chat. A tool that fails politely beats a tool that freezes.
💡 In a stdio server, never print to standard output. Stray console.log lines corrupt the protocol stream. Send debug output with console.error.
Claude Code Setup Step by Step
With the server written, Claude Code needs to know it exists.
Register the Server
From any folder, add it with the CLI. Everything after the double dash is the command that launches the server:
claude mcp add support-desk -- npx -y tsx /absolute/path/to/support-desk-mcp/src/server.ts
To share the setup with teammates, add --scope project. Claude Code then writes a .mcp.json file at the repository root:
💡 On native Windows, wrap the launcher: claude mcp add support-desk -- cmd /c npx -y tsx C:\path\to\src\server.ts.
Confirm the Connection
Two checks tell you the server is alive:
claude --version
claude mcp list
Inside a session, /mcp opens the server panel with connection status. You should see support-desk listed as connected.
If a server stops connecting after an update, check the Claude Code changelog. The notes for 2.1.287, which added URL prompts from servers on the 2025-11-25 protocol, say to add "bareElicitationCapability": true to that server's config entry when it no longer connects.
Run the Escalation Flow
Start a session in your project and type a plain request:
Escalate ticket SUP-1042 using the support-desk tool.
Claude calls mcp__support-desk__escalate_ticket. The tool pauses, Claude Code shows the form, and the agent fills in priority, area, customers affected, email and the outage flag.
After the agent submits, the tool resumes and returns something like Escalated SUP-1042 as p1 in login. Reference ESC-LQ3F9A2. The model can quote that reference straight into its reply.
Claude Code also exposes Elicitation and ElicitationResult hooks, matched on the server name. A script can answer a known form on its own, or log every response before it reaches the server. Use that for low-risk forms in automation, and keep people in front of anything that touches customers.
Handle Decline, Cancel and Bad Input
Happy-path demos hide the part that decides whether agents trust the tool. People close dialogs, change their minds and fat-finger values.
What Each Action Should Trigger
Action
What the person did
What the tool should do
accept
Submitted the form
Validate again, then create the escalation
decline
Said no on purpose
Leave the ticket alone, report it, offer a manual route
cancel
Closed the dialog
Change nothing, allow a retry later
Notice the safeParse call in the server. Clients should validate answers against the schema, and servers should do it again. Defaults and formats are hints for the interface, not guarantees about the data that arrives.
Return an isError result for bad input rather than throwing. The model sees the message and can ask the agent to run the tool again.
Mistakes That Break Elicitation
Most failures come from a short list of habits.
Sensitive Data in Forms
The spec is blunt: servers must not request passwords, API tokens or payment credentials through form mode. Form answers pass through the client, so they can end up in logs and transcripts. Anything secret belongs in URL mode, where the person types it into a page the client cannot read.
A name or an email address is different. The server may ask for those, and the person can review and decline.
Nested Schemas
A requestedSchema with a nested object or a list of objects will be rejected or rendered badly. Flatten it. If you need a list of line items, run several small elicitations or accept a multi-select enum.
Other habits that cause trouble:
Treating cancel like decline, so a closed dialog looks like a refusal.
Trusting an identity typed into a form. Identify users through authorization, not through a text field.
Asking for the same details twice in one session.
Forgetting that a remote server must tie its state to the user, not only to a session ID.
A Short Test Checklist
Run these five cases before real tickets touch the tool:
Accept with defaults only, and confirm affectedCustomers becomes 1.
Accept with an email that has a typo, and confirm the server rejects it.
Decline, and confirm the ticket is unchanged.
Cancel with the Escape button, and confirm nothing is written.
Connect from a client without elicitation, and confirm the plain-text fallback appears.
You can also run the server under the MCP Inspector from the project folder with npx @modelcontextprotocol/inspector npx tsx src/server.ts to watch the raw elicitation/create messages go by.
Draft Replies With Claude Sonnet 5
Once the escalation returns a reference, the agent still owes the customer an answer. Claude Sonnet 5 on PicassoIA handles that drafting step, and it reads screenshots too, which helps when a customer attaches an error image.
Open the Claude Sonnet 5 page and paste the ticket summary plus the escalation reference into Prompt.
Set System Prompt once: "You write short, calm support replies. Never promise a fix time."
Leave Effort on low for quick drafts. Raise it to medium or high when the ticket needs real reasoning. Per the model page, low disables thinking for the fastest, cheapest responses.
Lower Max Tokens from the 8192 default to around 600 so replies stay short.
Attach a screenshot in Image if the customer sent one. Max Image Resolution defaults to 0.5 megapixels, which is plenty for an error dialog.
Parameter
Default
Tip for support replies
Effort
low
Raise for tricky bugs only
Max Tokens
8192
Set near 600
System Prompt
empty
Fix tone and limits once
Max Image Resolution
0.5 MP
Keep as is for screenshots
For harder reasoning, Claude Opus 4.7 is listed in the same collection. Start with Sonnet 5 and move up only when a draft misses the point.
Make Your Own Images With Picasso IA
Support docs and help-center articles read better with real pictures. The same desk scenes you saw above, a headset on a table or a clipboard under window light, take one prompt to create.
Try PicassoIA Image for a fast first pass, or Flux 2 Pro when you want finer texture. A prompt that works well:
A support engineer at a wooden desk, 35mm lens, soft window light from the left, shallow depth of field, Kodak Portra 400 film grain, no text.
Pick a model, paste the prompt, change one detail per run, and compare the results. Ten minutes of experiments will teach you more about prompt wording than any list of rules. Open Picasso IA, make your first header image, and drop it into your next support article.