本教程以
@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 Apps 可以理解为三件事的组合:- MCP Tool:Agent 可以调用的工具。模型可见工具通常负责启动应用并返回首屏数据。
- MCP Resource:由 MCP server 暴露的 HTML 资源。MCP App 的资源 URI 通常是
ui://...,MIME 类型必须是text/html;profile=mcp-app。 - MCP Apps Host:Xpert + ChatKit 中负责读取资源、创建 sandbox iframe、注入初始化上下文并代理
tools/call/resources/read的宿主。
ui/initialize:App 向宿主初始化,获取 host 能力和上下文ui/notifications/tool-input:宿主把触发工具的输入通知给 Appui/notifications/tool-result:宿主把初始工具结果通知给 Apptools/call:App 调用同一 MCP server 中允许 iframe 调用的工具resources/read:App 读取允许访问的 MCP resourceui/notifications/size-changed:App 通知 ChatKit 调整 iframe 高度
运行原理
关键点是:ChatKit 不把任意 HTML 存进历史消息。历史中只保存resourceUri、toolName、toolsetId、serverName 等安全元数据,以及受大小限制的初始工具结果快照;刷新页面时,后端会优先使用仍然有效的 live app instance,过期后再重新读取 ui:// resource 并恢复 App instance。
真实业务建模
为了让示例插件更像真实项目,我们把它建模成“销售经营分析”能力,而不是“画图工具”。
真实生产环境中,mock dataset 可以替换成 CRM、ERP、数据仓库、指标平台或语义模型查询。不要让浏览器直接访问敏感数据库;iframe 应只调用受控 MCP tools,由后端执行鉴权、租户隔离、行列权限和审计。
插件结构
@xpert-ai/plugin-echarts-mcp-app 的核心结构如下:
src/app 是普通 Vanilla TypeScript 前端工程,构建后生成 dist/app/index.html;src/lib/app-html.ts 只负责读取构建产物并作为 MCP resource 返回。
声明 Plugin-Managed MCP Server
插件通过.xpertai-plugin/plugin.json 把 MCP server 声明为可安装资源:
"${PLUGIN_ROOT}" 很重要。插件安装到运行时目录后,路径会变化;不要把本机开发目录或某次 @runtime__... 目录写死到 manifest 或数据库中。
设计工具可见性
真实 MCP App 往往至少有两类工具:- 模型可见工具:让 Agent 知道什么时候应该打开 App,例如
echarts_sales_overview - App-only 工具:只允许 iframe 调用,用于刷新、下钻、分页、导出预检等交互,例如
echarts_sales_drilldown
返回结构化业务结果
MCP App 不应该依赖解析自然语言摘要。工具结果应同时提供:content:给模型和不支持 UI 的客户端看的文本摘要structuredContent:给 App 渲染的稳定 JSON 数据_meta.ui.resourceUri:告诉 ChatKit 应渲染哪个 App resource
structuredContent 包含完整分析对象和图表友好字段:
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:https://cdn.jsdelivr.net 放进 resource CSP。不要用宽泛的 *。生产 App 如果可以离线 bundle 第三方库,优先把外部依赖打进构建产物,CSP 会更简单。
集中开发 App 前端
不要把生产 App 写成几百行 TypeScript 模板字符串。推荐把 App 作为普通前端源码维护:scripts/build-app.mjs 使用 esbuild 打包浏览器 TypeScript,并把 CSS 和 JS 内联到单个 HTML:
dist/app/index.html。这让前端开发者可以按熟悉的方式维护 DOM、样式、状态和交互,也让测试可以直接检查构建产物里是否包含 ui/initialize、tools/call 等 bridge 调用。
App Bridge 生命周期
App 启动时应先初始化,再等待宿主发送首屏工具输入和工具结果:ui/notifications/tool-result 后,App 从 structuredContent.analysis 中提取数据并渲染图表。用户点击图表时,App 调用 app-only 工具:
ui/notifications/size-changed,避免 iframe 高度和内容不匹配。
构建和验证
开发时先构建 App asset,再构建插件 server: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
从示例走向生产
把这个插件用于真实业务时,可以沿着以下方向升级:
一个成熟的 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 中提供非常接近真实业务系统的交互体验。