ui:// MCP-Apps 卡片

更新于 · 在 sijie.xyz 查看原条目 ↗

聊天里每个工具的交互卡片,以 MCP-Apps 的 ui:// 资源形式下发,渲染在一个通过 postMessage 与宿主通信的沙箱 iframe 里。所有单工具卡片都已迁移完毕——ask_visitor、corpus_search/list、summarize、日历相关工具,甚至 calendar_book(通过 booker 插件自带的 ui:// 卡片);后端都是通过工具的 _meta.ui_resource 来提供这些卡片的。

握手过程

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)

(线上还跑着 mcp-ui:tool / mcp-ui:tool-result——卡片回调工具——以及卡片状态用的 mcp-ui:state-set / mcp-ui:state-ack;app/src/components/page/McpAppCard.tsx 及其 hook。)

类图视角——前端的分发逻辑

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)

精确地说:CardKind 只有 'dump' | 'none' 两种取值——ui:// 这条路径不是一种 kind:只要工具结果里带了 _meta.ui_resource,uiHtmlForTool 就会一路调用 McpAppCard 去渲染它,与 kind 无关;cardKindFor 只负责为其余情况决定兜底渲染方式。

唯一剩下的例外:通用 dump 卡片

NON_SANDBOX_CARDS(app/src/components/page/ToolCallCards.tsx)已经缩减到只剩一条:给 skill_* / ext_* 工具结果用的通用 dump 卡片(一种调试风格的文本渲染)。除此之外的一切都是沙箱化的 ui:// 卡片。

计划中(🚧——截至 2026-09-07 仍未建:mcpplugin.Manifest 没有 renderer 字段)

打算把这最后的残留也替换掉,改成manifest 声明的渲染字段(renderer: ui | dump)——这与 mcp-capability-plugins 的 Origin/manifest 哲学一致:每个能力自己声明如何渲染,不再有任何硬编码列表。

相关笔记