跳转到主要内容
本教程以 @xpert-ai/plugin-echarts-mcp-app 为例,从 MCP Apps 的基本概念、运行原理讲起,带你把一个“展示图表”的 demo 扩展成更贴近真实业务的销售经营分析插件。

目标

我们要构建的不是一张静态图,而是一个可以嵌入 ChatKit 对话的“销售经营驾驶舱”:
  • 用户在对话中提出“展示 2026 年各区域收入,并支持下钻分析”
  • Agent 调用模型可见工具 echarts_sales_overview
  • 工具返回文本摘要、结构化分析数据和 _meta.ui.resourceUri
  • ChatKit 以内联 iframe 渲染 ui://echarts-sales-dashboard
  • 用户在图表中切换指标、年份、维度,点击柱形图继续下钻
  • iframe 通过 MCP Apps bridge 调用 app-only 工具 echarts_sales_drilldown
  • 模型看不到 app-only 工具,但 App 可以安全调用它完成交互分析
这类模式适合经营分析、指标诊断、客户分群、供应链看板、工单排查、审批表单、地图分析等场景:模型负责理解意图和启动工作,MCP App 负责承载高密度、可交互的结果。

基本概念

MCP Apps 可以理解为三件事的组合:
  1. MCP Tool:Agent 可以调用的工具。模型可见工具通常负责启动应用并返回首屏数据。
  2. MCP Resource:由 MCP server 暴露的 HTML 资源。MCP App 的资源 URI 通常是 ui://...,MIME 类型必须是 text/html;profile=mcp-app
  3. MCP Apps Host:Xpert + ChatKit 中负责读取资源、创建 sandbox iframe、注入初始化上下文并代理 tools/call / resources/read 的宿主。
工具和 App 之间通过标准 JSON-RPC bridge 通信。常用方法包括:
  • ui/initialize:App 向宿主初始化,获取 host 能力和上下文
  • ui/notifications/tool-input:宿主把触发工具的输入通知给 App
  • ui/notifications/tool-result:宿主把初始工具结果通知给 App
  • tools/call:App 调用同一 MCP server 中允许 iframe 调用的工具
  • resources/read:App 读取允许访问的 MCP resource
  • ui/notifications/size-changed:App 通知 ChatKit 调整 iframe 高度
插件侧如何注册 MCP server、tool 和 resource 的通用规范见 MCP Tools 和 MCP Apps。ChatKit 宿主的渲染与安全模型见 ChatKit MCP Apps 官方协议参考:

运行原理

关键点是:ChatKit 不把任意 HTML 存进历史消息。历史中只保存 resourceUritoolNametoolsetIdserverName 等安全元数据,以及受大小限制的初始工具结果快照;刷新页面时,后端会优先使用仍然有效的 live app instance,过期后再重新读取 ui:// resource 并恢复 App instance。

真实业务建模

为了让示例插件更像真实项目,我们把它建模成“销售经营分析”能力,而不是“画图工具”。
层次示例能力在插件中的落点
业务问题哪个区域收入最高?利润率是否下降?订单是否集中在某个产品?工具描述和 summary
指标收入、毛利、订单数、毛利率SalesMetrictotalspoints
维度区域、产品、月份SalesGroupByfiltersnextGroupBy
首屏分析默认按区域展示 2026 年收入echarts_sales_overview
交互分析点击 West 后按产品下钻,再按月份看趋势echarts_sales_drilldown
可视化柱形图、折线图、统计卡片、面包屑src/app/main.ts + ECharts
安全边界模型只看到 overview,下钻工具只给 iframe_meta.ui.visibility
真实生产环境中,mock dataset 可以替换成 CRM、ERP、数据仓库、指标平台或语义模型查询。不要让浏览器直接访问敏感数据库;iframe 应只调用受控 MCP tools,由后端执行鉴权、租户隔离、行列权限和审计。

插件结构

@xpert-ai/plugin-echarts-mcp-app 的核心结构如下:
examples/echarts-mcp-app/
├── .xpertai-plugin/
│   └── plugin.json
├── package.json
├── project.json
├── scripts/
│   └── build-app.mjs
├── src/
│   ├── index.ts
│   ├── mcp-server.ts
│   ├── app/
│   │   ├── index.html
│   │   ├── main.ts
│   │   └── styles.css
│   └── lib/
│       ├── app-html.ts
│       ├── mcp-tools.ts
│       └── sales-data.ts
└── tsconfig.lib.json
这份结构有一个长期维护上的好处:MCP server、业务聚合逻辑和浏览器 App 分开开发。src/app 是普通 Vanilla TypeScript 前端工程,构建后生成 dist/app/index.htmlsrc/lib/app-html.ts 只负责读取构建产物并作为 MCP resource 返回。

声明 Plugin-Managed MCP Server

插件通过 .xpertai-plugin/plugin.json 把 MCP server 声明为可安装资源:
{
  "mcpServers": {
    "echarts-drilldown": {
      "type": "stdio",
      "command": "node",
      "args": ["${PLUGIN_ROOT}/dist/mcp-server.js"],
      "policy": {
        "enabled": true,
        "defaultToolsApprovalMode": "approve",
        "enabledTools": ["echarts_sales_overview", "echarts_sales_drilldown"]
      }
    }
  }
}
"${PLUGIN_ROOT}" 很重要。插件安装到运行时目录后,路径会变化;不要把本机开发目录或某次 @runtime__... 目录写死到 manifest 或数据库中。

设计工具可见性

真实 MCP App 往往至少有两类工具:
  • 模型可见工具:让 Agent 知道什么时候应该打开 App,例如 echarts_sales_overview
  • App-only 工具:只允许 iframe 调用,用于刷新、下钻、分页、导出预检等交互,例如 echarts_sales_drilldown
在这个销售分析插件中,overview 工具同时对模型和 App 可见:
export const OVERVIEW_TOOL_META = {
  ui: {
    resourceUri: 'ui://echarts-sales-dashboard',
    visibility: ['model', 'app']
  },
  'openai/outputTemplate': 'ui://echarts-sales-dashboard'
}
下钻工具只给 App:
export const DRILLDOWN_TOOL_META = {
  ui: {
    visibility: ['app']
  }
}
这样设计有两个收益。第一,模型工具列表保持简洁,不会把交互细节暴露给 LLM。第二,iframe 的能力仍然受 Xpert MCP Apps Host 管控,disabled tool、跨 toolset 调用或不可见工具都会被拒绝。

返回结构化业务结果

MCP App 不应该依赖解析自然语言摘要。工具结果应同时提供:
  • content:给模型和不支持 UI 的客户端看的文本摘要
  • structuredContent:给 App 渲染的稳定 JSON 数据
  • _meta.ui.resourceUri:告诉 ChatKit 应渲染哪个 App resource
示例插件的 structuredContent 包含完整分析对象和图表友好字段:
function buildStructuredContent(analysis: SalesAnalysis) {
  return {
    kind: analysis.kind,
    analysis,
    chart: {
      title: `${analysis.metric} by ${analysis.groupBy}`,
      labels: analysis.points.map((point) => point.label),
      values: analysis.points.map((point) => point.value),
      metric: analysis.metric,
      groupBy: analysis.groupBy,
      year: analysis.year,
      filters: analysis.filters
    }
  }
}
生产系统建议为 structuredContent 制定版本字段,例如 kind: 'sales-performance-analysis.v1',并在 App 中兼容一个版本窗口。这样后端分析结构演进时,不会轻易打破历史消息中的 App。 同时要控制初始结果大小。ChatKit 会优先从 live app instance 读取完整 toolResult,但聊天历史只内联小型结果快照;超过宿主阈值的大结果只记录大小和截断标记。销售经营分析类 App 应把初始 structuredContent 控制为首屏聚合、摘要和必要筛选条件,明细、长列表和更多层级通过 app-only 工具分页或按需下钻加载。

注册 MCP App Resource

MCP App 的 HTML 通过 MCP resource 返回。安全相关配置应放在 resource metadata,而不是 tool metadata:
registerAppResource(
  server,
  'echarts-sales-dashboard',
  'ui://echarts-sales-dashboard',
  {
    title: 'ECharts Sales Dashboard',
    description: 'Interactive ECharts dashboard for sales drilldown analysis.',
    mimeType: RESOURCE_MIME_TYPE,
    _meta: {
      ui: {
        csp: {
          resourceDomains: ['https://cdn.jsdelivr.net'],
          connectDomains: [],
          frameDomains: [],
          baseUriDomains: []
        },
        prefersBorder: true
      }
    }
  },
  () => buildDashboardResource(RESOURCE_MIME_TYPE)
)
如果 App 需要加载 ECharts CDN,就把 https://cdn.jsdelivr.net 放进 resource CSP。不要用宽泛的 *。生产 App 如果可以离线 bundle 第三方库,优先把外部依赖打进构建产物,CSP 会更简单。

集中开发 App 前端

不要把生产 App 写成几百行 TypeScript 模板字符串。推荐把 App 作为普通前端源码维护:
src/app/
├── index.html
├── main.ts
└── styles.css
scripts/build-app.mjs 使用 esbuild 打包浏览器 TypeScript,并把 CSS 和 JS 内联到单个 HTML:
{
  "scripts": {
    "build:app": "node scripts/build-app.mjs"
  }
}
最终 MCP resource 读取的是 dist/app/index.html。这让前端开发者可以按熟悉的方式维护 DOM、样式、状态和交互,也让测试可以直接检查构建产物里是否包含 ui/initializetools/call 等 bridge 调用。

App Bridge 生命周期

App 启动时应先初始化,再等待宿主发送首屏工具输入和工具结果:
request('ui/initialize', {
  protocolVersion: '2026-01-26',
  appInfo: {
    name: 'xpert-echarts-sales-dashboard',
    version: '0.0.1'
  },
  appCapabilities: {
    availableDisplayModes: ['inline']
  }
})
当收到 ui/notifications/tool-result 后,App 从 structuredContent.analysis 中提取数据并渲染图表。用户点击图表时,App 调用 app-only 工具:
request('tools/call', {
  name: 'echarts_sales_drilldown',
  arguments: {
    metric: state.metric,
    year: state.year,
    groupBy: state.groupBy,
    filters: state.filters
  }
})
每次图表重绘、错误态展开或移动端布局变化后,App 应发送 ui/notifications/size-changed,避免 iframe 高度和内容不匹配。

构建和验证

开发时先构建 App asset,再构建插件 server:
pnpm -C examples/echarts-mcp-app run build:app
pnpm -C examples/echarts-mcp-app exec tsc --build tsconfig.lib.json
如果通过 workspace Nx 构建,确保 nx build @xpert-ai/plugin-echarts-mcp-app 同时执行 server 编译和 App 构建。 发布前至少验证:
  • stdio MCP server 启动时不向 stdout 写普通日志
  • tools/list 中模型可见工具包含 echarts_sales_overview
  • app-only 工具 echarts_sales_drilldown 不暴露给模型
  • overview 工具结果包含 _meta.ui.resourceUri
  • resource 返回 text/html;profile=mcp-app
  • resource metadata 包含最小 CSP
  • iframe 能收到初始 tool input / tool result
  • 点击图表后能通过 tools/call 下钻
  • 刷新历史消息不会因为旧 app instance 过期而泄露或丢失原始 HTML

从示例走向生产

把这个插件用于真实业务时,可以沿着以下方向升级:
方向生产化建议
数据来源将 mock dataset 替换为指标平台、数据仓库、CRM 或语义模型查询
权限在 MCP server 中使用 Xpert 租户、组织、用户上下文执行行列权限
指标治理使用稳定指标 ID,不要只依赖 UI 文案
性能对聚合查询做缓存,限制下钻层级和返回点数
解释能力增加 app-only explain_variance 工具解释同比、环比和异常点
行动闭环增加 create_followup_taskexport_snapshot 等需要审批的工具
模型上下文ui/update-model-context 把用户当前筛选条件反馈给下一轮对话
可观测性记录每次 app-only 工具调用、拒绝原因和 resource 读取错误
一个成熟的 MCP App 插件不只是“HTML + 图表”。它应该把业务语义、工具权限、交互状态、审计和降级体验一起设计好。

常见问题

为什么不用 ChatKit Widget? Widget 适合声明式、受控的结构化 UI。MCP App 适合由插件提供完整 HTML 应用,需要复杂状态、第三方可视化库、图表交互或多次 app-only 工具调用的场景。 为什么不用 extension view? Extension view 是平台槽位,适合长期存在的工作台页面、配置页和集成详情页。MCP App 是工具调用结果,生命周期跟随对话和工具调用,更适合“问一个问题,得到一个可交互结果”。 模型为什么不能直接看到所有下钻工具? 下钻、分页、刷新这类工具是 UI 内部交互细节。把它们设为 app-only 可以减少模型工具选择负担,也能降低误调用风险。 HTML 会写入聊天历史吗? 不会。Xpert 只保存安全元数据,HTML 在渲染时由 MCP Apps Host 重新读取 resource。

下一步

你可以从 @xpert-ai/plugin-echarts-mcp-app 开始,把示例中的销售数据替换为自己的业务查询,把 ECharts 图表替换为更贴近业务的分析视图。只要保持 MCP tool、MCP resource 和标准 bridge 的边界清晰,同一个插件就可以在 ChatKit 中提供非常接近真实业务系统的交互体验。