MCP Apps
Interactive, app-like surfaces exposed through MCP.
Most MCP tools return text or JSON for a model to read. MCP Apps are the emerging MCP
extension (@modelcontextprotocol/ext-apps) that lets a tool call instead return an
interactive, HTML-based UI: rendered by the host in a sandboxed iframe, so a human
watching the conversation (or the model itself, via updateModelContext) sees something real
instead of a JSON blob.
Handoff's three MCP Apps today
Three tools return an interactive surface rather than JSON, each bundled from its own client
entry point by scripts/build-mcp-apps.mjs:
| Tool | App resource | What it renders |
|---|---|---|
handoff_preview_component | ui://handoff/component-preview | The actual built component, live, with responsive width controls. |
handoff_browse_tokens | ui://handoff/token-palette | A palette of color swatches, type specimens and the spacing scale; clicking a token hands it back to the model. |
handoff_browse_components | ui://handoff/component-gallery | A searchable card grid of the component catalog; select a component to hand it back, or open it in the registry. |
Component preview
handoff_preview_component renders the actual built component, live, with responsive
width controls:
- The tool looks up the component and its available preview keys (falling back to
"generic"or the first available preview). - It registers a
ui://handoff/component-previewresource, an HTML shell with a toolbar and an inner<iframe>. - That inner iframe points at the registry's own built preview HTML
(
/api/component/<id>/<id>-<previewKey>.html), the same rendering path the UI surface uses, not a re-implementation. - The tool result carries
_meta.ui.resourceUripointing at that resource, so a host that understands MCP Apps renders it inline instead of just showing the JSON.
This tool is registry-mode only, it needs a real component row and a built preview HTML file, neither of which exist in workspace mode.
Build process
Each app's browser-side code lives at src/app/lib/mcp/apps/<name>.client.ts
(component-preview, token-palette, component-gallery). scripts/build-mcp-apps.mjs bundles
each with esbuild (minified, ESM, browser target) and base64-encodes the result into a generated
<name>.bundle.ts, the server inlines that string directly into the resource HTML at request
time. There's no separate static
host or CDN involved; the whole app ships inside the MCP response. Regenerate the bundle after
editing the client file; it's committed so next build/typecheck sees it without needing an
extra build step in CI.
Client support & fallback
MCP Apps require a host that implements the ui:// resource type, resources/read, and the
app iframe sandbox (Claude's clients are the primary target). Hosts that don't support MCP Apps
still get a usable result: handoff_preview_component's handler always returns a plain
content: [{ type: 'text', ... }] block alongside the structuredContent and the app
registration, so the tool degrades to "here's the preview URL and metadata as JSON" rather than
failing outright.