mirror of
https://github.com/anthropics/claude-plugins-official.git
synced 2026-03-19 23:23:07 +00:00
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.
130 lines
4.6 KiB
Markdown
130 lines
4.6 KiB
Markdown
# 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
|
|
|
|
```typescript
|
|
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." }] };
|
|
});
|
|
```
|
|
|
|
```python
|
|
# 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 only** — `string`, `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
|
|
|
|
```python
|
|
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
|