跳转到主要内容
在 ChatKit 对话中把 MCP 工具调用结果渲染为可交互的内联应用。
MCP Apps 允许 MCP server 在工具结果中返回一个 HTML 应用。在 Xpert + ChatKit 中,只要 MCP 工具通过 _meta.ui.resourceUri 声明 UI 资源,ChatKit 就会把该资源渲染为对话中的内联 iframe,并通过标准 MCP Apps JSON-RPC bridge 与 Xpert 后端通信。 适合使用 MCP Apps 的场景包括图表、仪表盘、地图、表单、媒体浏览器、下钻分析等需要交互的工具结果。如果只是静态结构化卡片,优先使用 ChatKit Widgets。如果是工作台页面、集成配置页或长期存在的平台页面,继续使用 Xpert extension view。

Xpert 支持的能力

Xpert MCP Apps Host 支持:
  • 通过 _meta.ui.resourceUri 发现工具 UI 元数据
  • ChatKit McpApp 消息组件
  • ui:// MCP resource,MIME 类型为 text/html;profile=mcp-app
  • JSON-RPC bridge 方法,包括 ui/initializeui/notifications/tool-inputui/notifications/tool-resulttools/callresources/readui/open-linkui/messageui/update-model-contextui/request-display-modeui/notifications/size-changed
  • 通过 _meta.ui.visibility = ['app'] 暴露 app-only 工具
  • 从 MCP App resource metadata 读取 _meta.ui.csp_meta.ui.permissionsdomainprefersBorder
  • 短生命周期 app instance,并支持刷新历史消息后用安全元数据恢复
  • 通过 Xpert plugin 安装的 plugin-managed MCP server
ChatKit 实现的是标准 MCP Apps bridge,不实现 ChatGPT 专属的 window.openai API。为了保持跨 host 兼容,MCP App 应优先使用标准 bridge。

架构

聊天历史只保存安全元数据,例如 appInstanceIdresourceUritoolNametoolsetIdserverName,以及受大小限制的初始工具输入/结果快照。原始 HTML 不写入对话历史。页面刷新后,如果内存中的 app instance 已经过期,后端可以根据这些元数据重新连接 Toolset 并恢复 instance。

历史回放和初始工具结果

MCP App 首次渲染时需要收到触发工具调用的 tool-inputtool-result。Xpert + ChatKit 的读取顺序是:
  1. 优先使用 live app instance:只要内存中的 app instance 仍有效,resource 响应会直接返回 live instance 中保存的完整初始 toolResult
  2. live instance 不存在时 revive:刷新历史、Agent Toolset 已关闭或后端进程重启后,MCP Apps Host 会重新连接 Toolset,读取 ui:// resource,并创建新的 app instance。
  3. 最后使用聊天历史中的小型快照:如果消息中保存了受控大小的初始 toolResult,ChatKit 会用它回放 ui/notifications/tool-result;如果没有可用结果,则只发送 ui/notifications/tool-input,不会伪造空结果。
为避免把聊天消息变成大型数据存储,历史消息不会无条件保存完整工具结果。默认情况下,只有序列化后不超过 128KB 的标准 CallToolResult 会内联保存;超过阈值时,消息只保存 toolResultSizetoolResultTruncated: true。阈值可通过后端环境变量调整:
XPERT_MCP_APP_HISTORY_TOOL_RESULT_MAX_BYTES=131072
这意味着实时运行或 live instance 未过期时,App 可以拿到完整结果;历史回放时,小结果可以直接复原,大结果需要 App 提供降级体验,例如展示摘要、提示重新运行,或通过 app-visible 工具按需重新读取/分页加载数据。长期需要完整历史回放的大数据结果,应进入专门的 artifact store,而不是继续扩大 message content。

运行时元数据约定

ChatKit 不直接理解任意 MCP server 的实现细节,而是依赖 Xpert 后端保留从当前 Toolset 中发现的 MCP App 元数据。 运行时,当 Xpert 看到 _meta.ui.resourceUri,并且 resource URI 使用 ui:// scheme 时,该 MCP 工具就具备 App 能力。工具 _meta.ui 只应该承载 resourceUrivisibility。CSP、浏览器权限、domainprefersBorder 等资源安全与呈现元数据应放在 MCP App resource 的 _meta.ui 上;Xpert 会优先读取 resources/read content item metadata,并在缺失时使用 resources/list metadata 作为 fallback。 visibility 控制谁可以调用工具:
Visibility含义
model工具可以暴露给 LLM。
appiframe 可以通过 tools/call 调用该工具。
常见设计是:一个 model-visible 工具负责打开 MCP App,另有一个或多个 app-only 工具供 iframe 交互使用。 Xpert 会把非 model-visible 的工具从 LLM 工具列表中过滤掉。MCP Apps Host 也会拒绝 iframe 调用不可见于 app 或在 Toolset 中被禁用的工具。 插件侧 metadata 和工具注册示例请参考 MCP Tools 和 MCP Apps

Resource 要求

MCP App resource 必须返回带 MCP App profile 的 HTML,MIME 类型为 text/html;profile=mcp-app。注册 resource 是 MCP server 的职责;校验和 sandbox 则由 host 负责。 resource 也可以在 _meta.ui 中声明展示 metadata:titledescriptionicontitledescription 可以是字符串,也可以是 Xpert 风格的 I18nObjecticon 使用共享的 IconDefinition 结构。ChatKit 只把这些安全描述符写入消息历史,并根据当前 ChatKit 语言解析文本,在 MCP App 消息头部渲染 icon/title/description。 安全默认值是严格的:
  • 初始 App HTML 只接受 ui:// resource
  • 原始 HTML 在渲染时读取,不写入聊天历史
  • CSP 默认 deny-by-default,只允许 resource _meta.ui.csp 中声明的域名
  • camera、microphone、geolocation、clipboard-write 默认拒绝,只有 resource _meta.ui.permissions 显式请求时才会通过 iframe allow 放行
  • iframe 内通过 resources/read 读取的资源限定在同一个 MCP server 内,拒绝 http://https://javascript://data://blob:// 等浏览器或脚本 scheme
  • resource domain 当前不会创建 dedicated origin;v1 中视为 host 暂不支持的 metadata
  • iframe 的所有工具调用都经过 Xpert 后端,并执行租户、组织、工作区、Toolset、工具启用状态等校验

主题变量

ChatKit 会在 MCP App HTML 写入 iframe 之前,向 <head> 注入一段宿主主题样式。变量名使用通用 --mcp-app-* 前缀,其他 MCP Apps host 也可以复用同一契约:
<style id="mcp-app-host-theme">
  :root {
    color-scheme: light;
    --mcp-app-color-background: hsl(0 0% 100%);
    --mcp-app-color-foreground: hsl(222.2 84% 4.9%);
    --mcp-app-color-primary: hsl(221.2 83.2% 53.3%);
  }
</style>
MCP App 应使用 --mcp-app-* 公共变量,而不是依赖 ChatKit 内部 CSS class 或私有 token。当前 host 提供:
变量用途
--mcp-app-color-background / --mcp-app-color-foreground页面背景与正文文本
--mcp-app-color-card / --mcp-app-color-card-foreground卡片、面板、图表容器
--mcp-app-color-popover / --mcp-app-color-popover-foreground下拉层、浮层
--mcp-app-color-primary / --mcp-app-color-primary-foreground主按钮、重点数据、图表主色
--mcp-app-color-secondary / --mcp-app-color-secondary-foreground次级操作
--mcp-app-color-muted / --mcp-app-color-muted-foreground弱背景、辅助文字
--mcp-app-color-accent / --mcp-app-color-accent-foregroundhover、选中、强调区域
--mcp-app-color-destructive / --mcp-app-color-destructive-foreground危险操作和错误状态
--mcp-app-color-border--mcp-app-color-input--mcp-app-color-ring边框、输入框、焦点环
--mcp-app-color-chart-1--mcp-app-color-chart-5host 提供的图表色提示
--mcp-app-radius圆角基准
--mcp-app-font-sans--mcp-app-font-mono正文和等宽字体
--mcp-app-color-schemelightdark
推荐 App 样式:
body {
  margin: 0;
  font-family: var(--mcp-app-font-sans, system-ui, sans-serif);
  color: var(--mcp-app-color-foreground, #0f172a);
  background: var(--mcp-app-color-background, #fff);
}

.panel {
  background: var(--mcp-app-color-card, #fff);
  color: var(--mcp-app-color-card-foreground, #0f172a);
  border: 1px solid var(--mcp-app-color-border, #e2e8f0);
  border-radius: var(--mcp-app-radius, 8px);
}

.primary {
  background: var(--mcp-app-color-primary, #2563eb);
  color: var(--mcp-app-color-primary-foreground, #fff);
}
如果图表库需要在 JavaScript 中读取颜色,可以直接读取 CSS 变量:
const styles = getComputedStyle(document.documentElement);
const primaryColor = styles
  .getPropertyValue('--mcp-app-color-primary')
  .trim();
注意:--mcp-app-color-chart-* 是 host 提供的图表色提示,不保证一定适合具体业务图表。如果 host 主题的图表 token 偏灰或仅用于弱化 UI,MCP App 可以定义自己的语义数据色板,例如 --sales-chart-revenue--sales-chart-margin--risk-chart-high,同时继续使用 --mcp-app-* 控制背景、文本、边框、字体和圆角。 ui/initializehostContext.theme 仍返回 light / dark 字符串;同一份变量也会出现在 hostContext.themeCssVariables 中,供 App 初始化图表主题或生成 canvas 配色。 ChatKit 也会通过 hostContext.localehostContext.languagehostContext.direction 传递当前 UI 语言。在 iframe 文档运行前,ChatKit 会把同样的值写入 App HTML 的 langdir 属性。MCP App 应在自己的前端资源中使用这些字段完成标签、数字/日期格式、图表标题和校验消息的本地化。

Bridge 方法

iframe 内部通过 postMessage 发送 JSON-RPC 消息。App 应先初始化并读取 host 能力。初始化请求需要包含 App 信息、能力和协议版本:
const id = 1;
window.parent.postMessage({
  jsonrpc: '2.0',
  id,
  method: 'ui/initialize',
  params: {
    protocolVersion: '2026-01-26',
    appInfo: {
      name: 'sales-dashboard',
      version: '0.1.0',
    },
    appCapabilities: {
      availableDisplayModes: ['inline'],
    },
  },
}, '*');
ChatKit 会返回标准 McpUiInitializeResult
{
  jsonrpc: '2.0',
  id: 1,
  result: {
    protocolVersion: '2026-01-26',
    hostInfo: {
      name: 'xpert-chatkit',
      version: '1.0.0',
      title: 'Xpert ChatKit',
    },
    hostCapabilities: {
      serverTools: {},
      serverResources: {},
      openLinks: {},
      message: { text: {} },
      updateModelContext: { text: {}, structuredContent: {} },
    },
    hostContext: {
      displayMode: 'inline',
      availableDisplayModes: ['inline'],
      theme: 'light',
      themeCssVariables: {
        '--mcp-app-color-background': 'hsl(0 0% 100%)',
        '--mcp-app-color-foreground': 'hsl(222.2 84% 4.9%)',
      },
      locale: 'zh-Hans',
      language: 'zh',
      direction: 'ltr',
      toolInfo: {
        tool: {
          name: 'sales_overview',
          inputSchema: {
            type: 'object',
            properties: {},
          },
        },
      },
    },
  },
}
初始化后,ChatKit 先发送原始工具输入,再发送标准 MCP CallToolResult 形状的工具结果:
{
  jsonrpc: '2.0',
  method: 'ui/notifications/tool-input',
  params: {
    arguments: {
      year: 2026,
      groupBy: 'region',
    },
  },
}
{
  jsonrpc: '2.0',
  method: 'ui/notifications/tool-result',
  params: {
    content: [
      { type: 'text', text: 'Revenue by region for 2026.' },
    ],
    structuredContent: {
      chart: {
        labels: ['West', 'East'],
        values: [7600000, 6500000],
      },
    },
    toolName: 'sales_overview',
    toolCallId: 'call_123',
    // legacy compatibility only; new apps should read params.content and params.structuredContent.
    result: {},
  }
}
App 可以继续调用 app-visible 工具:
window.parent.postMessage({
  jsonrpc: '2.0',
  id: 2,
  method: 'tools/call',
  params: {
    name: 'sales_drilldown',
    arguments: {
      year: 2026,
      groupBy: 'product',
      filters: { region: 'West' },
    },
  },
}, '*');
App 可以请求 host 安全打开外链:
window.parent.postMessage({
  jsonrpc: '2.0',
  id: 3,
  method: 'ui/open-link',
  params: {
    url: 'https://example.com/report',
  },
}, '*');
App 也可以把后续用户消息或模型上下文交回 host:
window.parent.postMessage({
  jsonrpc: '2.0',
  id: 4,
  method: 'ui/update-model-context',
  params: {
    content: [{ type: 'text', text: 'User selected West region.' }],
    structuredContent: {
      filters: { region: 'West' },
    },
  },
}, '*');
如果内容高度变化,渲染后通知 ChatKit:
window.parent.postMessage({
  jsonrpc: '2.0',
  method: 'ui/notifications/size-changed',
  params: { height: document.body.scrollHeight },
}, '*');

编写和打包

在 Xpert 中推荐用 plugin-managed MCP server 交付 MCP App。插件负责 MCP server 入口、工具元数据、ui:// resource、app-only 工具和安装策略;ChatKit 只负责托管工具调用产生的 app instance。 插件侧实现流程、manifest schema、包结构和本地测试清单请参考 MCP Tools 和 MCP Apps

与其他 Xpert UI 能力的关系

MCP Apps 是 Xpert 的 UI 扩展点之一:
能力适合场景运行模型
ChatKit Widgets声明式卡片和结构化 UI 数据ChatKit 数据驱动 renderer
MCP Apps工具调用结果中的交互式 HTML 应用会话内 iframe + MCP bridge
Extension Views工作台页面、集成详情页、长期平台页面Xpert plugin manifest + view host
Middleware注册工具、拦截工作流、注入运行时行为Agent runtime behavior
不要把任意 HTML 塞进 widget renderer,也不要让 middleware 承担 resource host 的职责。MCP Apps 应走 MCP resource 和 bridge 流程;extension view 仍然走 Xpert view manifest 以及平台 data/action provider。

启用和运维

生产环境需要显式启用:
XPERT_MCP_APPS_ENABLED=true
XPERT_MCP_APP_TOKEN_SECRET=<long-random-secret>
ChatKit 只把安全的 MCP App component metadata 写入聊天历史。后端还会为每个 app instance 签发 appInstanceToken,ChatKit 在 resource 和 RPC 请求中带上它。生产环境中,如果签名 token 缺失、过期,或与 tenant、workspace、Toolset、server、tool、resource URI 不匹配,revive、tools/callresources/read 都会被拒绝。 本地非生产环境默认启用 MCP Apps,并兼容没有 appInstanceToken 的旧消息。插件构建、安装、受控 stdio runtime 和运行副本检查请参考 MCP Tools 和 MCP Apps

故障排查

现象可能原因处理方式
刷新后 resource 返回 404前端旧 bundle 没有发送恢复元数据,或 Toolset 已删除重新构建/加载 ChatKit 前端,并确认消息中包含 toolsetIdresourceUri
App 能渲染但工具调用失败目标工具不是 app-visible,或在 Toolset 中被禁用设置 _meta.ui.visibility = ['app'],并在 Toolset policy 中启用该工具。
Resource MIME 错误resource 没有返回 text/html;profile=mcp-app修正 MCP resource 的 MIME 类型。
外部脚本被 CSP 阻止resource metadata 的 CSP 未声明对应域名把域名加入 resource _meta.ui.csp.resourceDomains,不要放在 tool _meta.ui.csp

相关文档