Some tools can't finish without one more fact from the person driving the session: which table to rebook, which branch to deploy to, whether it's really OK to overwrite the file. Elicitation is the Model Context Protocol feature that lets your server pause in the middle of a tool call, ask the user a structured question, and then continue with the answer. In the official SDKs you request it with a single call (ctx.elicit() in Python, server.server.elicitInput() in TypeScript) that sends an elicitation/create request and hands back the user's response. This guide wires it up end to end, including the schema rules that trip most people up.
Key Takeaways
- To add elicitation, call
ctx.elicit(message=..., schema=...)from inside a tool; the SDK sends anelicitation/createrequest and returns an object withactionanddata. - The client, not your server, must declare the
elicitationcapability at initialization, or the request fails. - Responses come back as one of three actions,
accept,decline, orcancel, and you must handle all three distinctly. - The
requestedSchemais a restricted subset of JSON Schema: a flat object of primitive properties only, no nested objects or arrays of objects. - The spec forbids requesting sensitive data (passwords, tokens, secrets) through elicitation.
What You Need
- An existing MCP server. If you don't have one yet, start from a minimal FastMCP server and add elicitation to a tool inside it.
- The official Python SDK (
pip install "mcp[cli]") or the TypeScript SDK (npm install @modelcontextprotocol/sdk). - A host that supports elicitation. Elicitation entered the spec in the 2025-06-18 revision, so the client must implement it; test against a client that advertises the capability.
How Do You Add Elicitation to a Tool?
The whole procedure is one call plus a branch on the result. Here it is with the official Python SDK.
- Define the answer shape as a schema. Use a Pydantic model whose fields are all primitives. This becomes the
requestedSchemathe client renders as a form.from mcp.server.fastmcp import FastMCP, Context from pydantic import BaseModel, Field mcp = FastMCP("Booking") class BookingPreferences(BaseModel): checkAlternative: bool = Field(description="Try another date?") alternativeDate: str = Field(default="", description="Date as YYYY-MM-DD") - Call
ctx.elicit()inside the tool. Pass the prompt asmessageand the model asschema. Await it; execution pauses until the user answers.@mcp.tool() async def book_table(date: str, party_size: int, ctx: Context) -> str: if date != "2026-09-01": return f"Booked table for {party_size} on {date}" result = await ctx.elicit( message=f"No table for {party_size} on {date}. Pick another date?", schema=BookingPreferences, ) - Branch on
result.action. Read the typed answer fromresult.dataonly when the action isaccept.if result.action == "accept" and result.data: if result.data.checkAlternative: return f"Booked for {result.data.alternativeDate}" return "No booking made" if result.action == "decline": return "User declined an alternative date" return "Booking cancelled" - Confirm the client declares the capability. Elicitation only works if the connected client advertised it during initialization. On the wire that is a single object:
{ "capabilities": { "elicitation": {} } } - Run it against a real client and answer the prompt. When the tool hits the
elicitcall, the client shows a form built from your schema; submit it, and the tool resumes with your input.
That five-step loop is the entire feature. Everything below hardens it.
What Field Types Can the Schema Use?
The requestedSchema is deliberately narrow so any client can render it as a plain form. It must be a flat object, and each property is one of four primitive kinds. Nested objects, arrays of objects, and most advanced JSON Schema keywords are not supported by design.
| Type | Keyword | Constraints you can set | Use it for |
|---|---|---|---|
| String | "type": "string" | minLength, maxLength, format (email, uri, date, date-time) | Names, dates, free text |
| Number | "type": "number" or "integer" | minimum, maximum | Counts, amounts, ranges |
| Boolean | "type": "boolean" | default | Yes/no confirmations |
| Enum | "type": "string" + enum | enum, optional enumNames for display labels | Fixed choice lists |
If you need a nested structure, split it across two elicitation calls or restructure the fields into a flat set. Trying to smuggle a nested object through will be dropped or rejected by a spec-compliant client.
How Should You Handle Accept, Decline, and Cancel?
The three-action model exists so you can tell "the user said no" apart from "the user walked away." Treating them as the same thing produces confusing behavior, like re-prompting someone who explicitly declined.
| Action | What the user did | content / data present? | What your server should do |
|---|---|---|---|
accept | Filled the form and submitted | Yes, matching the schema | Validate, then use the data |
decline | Explicitly said no (Reject, Decline) | Usually omitted | Offer an alternative or exit cleanly |
cancel | Dismissed without choosing (Escape, closed dialog) | Usually omitted | Leave state untouched; you may prompt again later |
How Is This Different in the TypeScript SDK?
The shape is identical; only the surface changes. The high-level McpServer exposes the underlying connection as .server, and you call its elicitInput method with a raw JSON requestedSchema. The answer arrives on result.content rather than result.data.
server.registerTool(
"book_table",
{ title: "Book a table", inputSchema: { date: z.string(), partySize: z.number() } },
async ({ date, partySize }) => {
const result = await server.server.elicitInput({
message: `No table for ${partySize} on ${date}. Pick another date?`,
requestedSchema: {
type: "object",
properties: {
alternativeDate: { type: "string", title: "Alternative date", format: "date" },
},
required: ["alternativeDate"],
},
});
if (result.action === "accept") {
return { content: [{ type: "text", text: `Booked for ${result.content.alternativeDate}` }] };
}
return { content: [{ type: "text", text: "No booking made" }] };
},
);
One naming note worth knowing: the standalone FastMCP package documents the parameter as response_type rather than schema and lets you pass a bare scalar like str or a Literal for single-field prompts. If you installed fastmcp on its own instead of the bundled mcp package, use that signature.
What Should You Never Ask For?
Elicitation is for workflow input, not secrets. The specification states servers MUST NOT request sensitive information such as passwords or API tokens through it, and clients should rate-limit requests and make clear which server is asking. Keep the questions to things a person would happily type into a visible form: a date, a choice from a list, a confirmation. Route anything sensitive through a proper auth flow instead.
Elicitation is the user-facing half of a pair. Its sibling, sampling, lets the server ask the client's model for a completion rather than asking the human. Both are server-initiated callbacks that the client mediates, which is why the client, not the server, owns the capability declaration.
Troubleshooting
The Tool Hangs or the Request Fails Immediately
The connected client didn't declare the elicitation capability, so the request has nowhere to go. Guard the call: check the negotiated capabilities before invoking elicit, and fall back to a sensible default or a plain error message when the client can't support it.
You Get a "Method Not Found" (-32601) Error
Same root cause seen from the wire. The client answered elicitation/create with JSON-RPC error -32601 because it doesn't implement the method. Test against a client that advertises elicitation support.
A Nested Field Is Silently Missing
Your schema included a nested object or an array of objects, which the restricted schema forbids. Flatten the fields (for example, address_city and address_zip as separate string properties) or split the collection across multiple calls.
result.data Is None Even on Accept
The content the client returned didn't validate against your Pydantic model, usually a type mismatch (a string where the schema said number) or a missing required field. Confirm your model's field names and types match the requestedSchema exactly, and mark truly optional fields with defaults.
Declined Users Keep Getting Re-Prompted
You're treating decline and cancel as one branch. Handle them separately: decline means stop asking, cancel means the user stepped away and it's fine to ask again later.
What to Do Next
- Read the full elicitation spec, including every schema keyword and security rule, in the MCP 2025-06-18 client specification.
- Study the runnable server example in the official Python SDK repository to see form-mode elicitation in context.
- If you're on the standalone package, follow the FastMCP elicitation guide for scalar and multi-select prompts.
- Add the sibling capability so your server can also reason with the host model, following the MCP sampling guide linked above.