Files
claude-plugins-official/plugins/mcp-server-dev/skills/build-mcp-server/references/elicitation.md
Den Delimarsky 48a018f27a fix(plugin): mcp-server-dev — correct APIs against spec, add missing primitives
Corrects fabricated/deprecated APIs: ext-apps App class model (not embedded
resources), real MCPB v0.4 manifest (no permissions block exists), registerTool
(not server.tool), @anthropic-ai/mcpb package name, CIMD preferred over DCR.

Adds missing spec coverage: resources, prompts, elicitation (with capability
check + fallback), sampling, roots, tool annotations, structured output,
instructions field, progress/cancellation.
2026-03-18 22:53:38 +00:00

4.6 KiB

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