ui:// MCP-Apps cards

Updated · View the entry on sijie.xyz ↗

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.

Related notes