elicitation.md 4.6 KB

Elicitation — spec-native user input

Elicitation lets a server pause mid-tool-call and ask the user for structured input. The client renders a native form (no iframe, no HTML). User fills it, server continues.

This is the right answer for simple input. Widgets (build-mcp-app) are for when you need rich UI — charts, searchable lists, visual previews. If you just need a confirmation, a picked option, or a few form fields, elicitation is simpler, spec-native, and works in any compliant host.


⚠️ Check capability first — support is new

Host support is very recent:

Host Status
Claude Code ✅ since v2.1.76 (both form and url modes)
Claude Desktop Unconfirmed — likely not yet or very recent
claude.ai Unknown

The SDK throws CapabilityNotSupported if the client doesn't advertise elicitation. There is no graceful degradation built in. You MUST check and have a fallback.

The canonical pattern

server.registerTool("delete_all", {
  description: "Delete all items after confirmation",
  inputSchema: {},
}, async ({}, extra) => {
  const caps = server.getClientCapabilities();
  if (caps?.elicitation) {
    const r = await server.elicitInput({
      mode: "form",
      message: "Delete all items? This cannot be undone.",
      requestedSchema: {
        type: "object",
        properties: { confirm: { type: "boolean", title: "Confirm deletion" } },
        required: ["confirm"],
      },
    });
    if (r.action === "accept" && r.content?.confirm) {
      await deleteAll();
      return { content: [{ type: "text", text: "Deleted." }] };
    }
    return { content: [{ type: "text", text: "Cancelled." }] };
  }
  // Fallback: return text asking Claude to relay the question
  return { content: [{ type: "text", text: "Confirmation required. Please ask the user: 'Delete all items? This cannot be undone.' Then call this tool again with their answer." }] };
});
# fastmcp
from fastmcp import Context
from fastmcp.exceptions import CapabilityNotSupported

@mcp.tool
async def delete_all(ctx: Context) -> str:
    try:
        result = await ctx.elicit("Delete all items? This cannot be undone.", response_type=bool)
        if result.action == "accept" and result.data:
            await do_delete()
            return "Deleted."
        return "Cancelled."
    except CapabilityNotSupported:
        return "Confirmation required. Ask the user to confirm deletion, then retry."

Schema constraints

Elicitation schemas are deliberately limited — keep forms simple:

  • Flat objects only — no nesting, no arrays of objects
  • Primitives onlystring, number, integer, boolean, enum
  • String formats limited to: email, uri, date, date-time
  • Use title and description on each property — they become form labels

If your data doesn't fit these constraints, that's the signal to escalate to a widget.


Three-state response

Action Meaning content present?
accept User submitted the form ✅ validated against your schema
decline User explicitly said no
cancel User dismissed (escape, clicked away)

Treat decline and cancel differently if it matters — decline is intentional, cancel might be accidental.

The TS SDK's server.elicitInput() auto-validates accept responses against your schema via Ajv. fastmcp's ctx.elicit() returns a typed discriminated union (AcceptedElicitation[T] | DeclinedElicitation | CancelledElicitation).


fastmcp response_type shorthand

await ctx.elicit("Pick a color", response_type=["red", "green", "blue"])  # enum
await ctx.elicit("Enter email", response_type=str)                         # string
await ctx.elicit("Confirm?", response_type=bool)                           # boolean

@dataclass
class ContactInfo:
    name: str
    email: str
await ctx.elicit("Contact details", response_type=ContactInfo)             # flat dataclass

Accepts: primitives, list[str] (becomes enum), dataclass, TypedDict, Pydantic BaseModel. All must be flat.


Security

MUST NOT request passwords, API keys, or tokens via elicitation — spec requirement. Those go through OAuth or user_config with sensitive: true (MCPB), not runtime forms.


When to escalate to widgets

Elicitation handles: confirm dialogs, enum pickers, short flat forms.

Reach for build-mcp-app widgets when you need:

  • Nested or complex data structures
  • Scrollable/searchable lists (100+ items)
  • Visual preview before choosing (image thumbnails, file tree)
  • Live-updating progress or streaming content
  • Custom layouts, charts, maps