Build an MCP App (Interactive UI Widgets)
An MCP app is a standard MCP server that **also serves UI resources** — interactive components rendered inline in the chat surface. Build once, runs in Claude *and* ChatGPT and any other host that implements the apps surface.
The UI layer is **additive**. Under the hood it's still tools, resources, and the same wire protocol. If you haven't built a plain MCP server before, the `build-mcp-server` skill covers the base layer. This skill adds widgets on top.
> **Testing in Claude:** Add the server as a custom connector in claude.ai (via a Cloudflare tunnel for local dev) — this exercises the real iframe sandbox and `hostContext`. See https://claude.com/docs/connectors/building/testing.
Claude host specifics
- `_meta.ui.prefersBorder: false` on a `ui://` resource removes the outer card border (mobile).
- `hostContext.safeAreaInsets: {top, right, bottom, left}` (px) — honor these for notches and the composer overlay.
- `_meta.ui.csp.{connectDomains, resourceDomains, baseUriDomains}` — declare external origins per resource; default is block-all. `frameDomains` is currently restricted in Claude.
- Directory submission for MCP Apps requires 3–5 PNG screenshots, ≥1000px wide, cropped to the app response only (no prompt in the image). See https://claude.com/docs/connectors/building/submission#asset-specifications.
---
When a widget beats plain text
Don't add UI for its own sake — most tools are fine returning text or JSON. Add a widget when one of these is true:
| Signal | Widget type | |---|---| | Tool needs structured input Claude can't reliably infer | Form | | User must pick from a list Claude can't rank (files, contacts, records) | Picker / table | | Destructive or billable action needs explicit confirmation | Confirm dialog | | Output is spatial or visual (charts, maps, diffs, previews) | Display widget | | Long-running job the user wants to watch | Progress / live status |
If none apply, skip the widget. Text is faster to build and faster for the user.
---
Widgets vs Elicitation — route correctly
Before building a widget, check if **elicitation** covers it. Elicitation is spec-native, zero UI code, works in any compliant host.
| Need | Elicitation | Widget | |---|---|---| | Confirm yes/no | ✅ | overkill | | Pick from short enum | ✅ | overkill | | Fill a flat form (name, email, date) | ✅ | overkill | | Pick from a large/searchable list | ❌ (no scroll/search) | ✅ | | Visual preview before choosing | ❌ | ✅ | | Chart / map / diff view | ❌ | ✅ | | Live-updating progress | ❌ | ✅ |
If elicitation covers it, use it. See `../build-mcp-server/references/elicitation.md`.
---
Architecture: two deployment shapes
Remote MCP app (most common)
Hosted streamable-HTTP server. Widget templates are served as **resources**; tool results reference them. The host fetches the resource, renders it in an iframe sandbox, and brokers messages between the widget and Claude.
┌──────────┐ tools/call ┌────────────┐
│ Claude │─────────────> │ MCP server │
│ host │<── result ────│ (remote) │
│ │ + widget ref │ │
│ │ │ │
│ │ resources/read│ │
│ │─────────────> │ widget │
│ ┌──────┐ │<── template ──│ HTML/JS │
│ │iframe│ │ └────────────┘
│ │widget│ │
│ └──────┘ │
└──────────┘MCPB-packaged MCP app (local + UI)
Same widget mechanism, but the server runs local
<!-- truncated -->