ui:// MCP-Apps cards
Per-tool interactive cards in the chat, delivered as MCP-Apps ui:// resources, rendered in a sandboxed iframe talking to the host via postMessage. All per-tool cards are migrated — ask_visitor, corpus_search/list, summarize, the calendar tools, and even calendar_book (via the booker plugin's own ui:// card); the backend serves them through the tool's _meta.ui_resource.
The handshake
sequenceDiagram participant T as tool result (backend) participant A as app chat (frontend) participant I as sandbox iframe (untrusted frontend) T->>A: result carries ui:// resource A->>I: mount iframe (sandboxed) I->>A: mcp-ui:ready A->>I: card data I->>A: mcp-ui:height (resize loop) I->>A: mcp-ui:submit (user acted) A->>T: continuation back into the tool I->>A: mcp-ui:link (navigation escapes via host)
(The wire also carries mcp-ui:tool / mcp-ui:tool-result — a card calling back into a tool — and mcp-ui:state-set / mcp-ui:state-ack for card state; app/src/components/page/McpAppCard.tsx + its hook.)
Class view — the frontend dispatch
classDiagram
class cardKindFor {
<<func in tool-call-shape>>
takes tool name
returns CardKind: dump or none
}
class ToolCallCards {
NON_SANDBOX_CARDS: Record of kind to renderer
single entry: dump to GenericDumpCard
}
class McpAppCard {
<<props>>
call: ToolCallView
html: string
onAsk?: (q) => void
conversationID?: string
}
class GenericDumpCard {
call: ToolCallView
debug-style text rendering
}
ToolCallCards ..> cardKindFor : classify skill_* and ext_*
ToolCallCards --> GenericDumpCard : kind = dump
ToolCallCards --> McpAppCard : ui_resource present (separate path)
Precision: CardKind is only 'dump' | 'none' — the ui:// path is not a kind: when the tool result carries _meta.ui_resource, uiHtmlForTool → McpAppCard renders it regardless, and cardKindFor only decides the fallback for the rest.
The one remaining exception: the generic dump
NON_SANDBOX_CARDS (app/src/components/page/ToolCallCards.tsx) has shrunk to a single entry: the generic dump card used for skill_* / ext_* tool results (a debug-style text rendering). Everything else is a sandboxed ui:// card.
Proposed (🚧 — still unbuilt as of 2026-09-07: mcpplugin.Manifest has no renderer field)
Replace even that residue with a manifest-declared renderer field (renderer: ui | dump) — consistent with the Origin/manifest philosophy of mcp-capability-plugins: each capability declares its rendering, no hardcoded list at all.