跳转到主要内容
插件可以把 MCP server 作为可安装资源交付。这是为 Xpert 增加标准 MCP tools 的推荐方式,也是交付可在 ChatKit 中内联渲染的 MCP Apps 的推荐方式。 本文只讲插件侧如何打包和实现。ChatKit 宿主运行时、iframe bridge、消息渲染和安全模型请参考 ChatKit MCP Apps 如果你想从一个完整业务案例入手,可以继续阅读 开发一个销售经营分析 MCP App 插件。该教程以销售经营分析为例,展示如何把 model-visible tool、app-only tool、ui:// resource 和 Vanilla TypeScript App 组合成一个可交互插件。

适用场景

当你需要以下能力时,使用 plugin-managed MCP server:
  • 通过 MCP 协议暴露工具,而不是实现 Xpert 原生 ToolsetStrategy
  • 让工具实现尽量保持 MCP host 可移植性
  • 在工具结果中返回可交互的 MCP App
  • 提供 iframe 可调用、但模型不可见的 app-only 工具
  • 把 MCP server 的安装、启用、停用和版本管理纳入插件生命周期
如果要开发长期存在的 Workbench 页面或集成配置页面,应使用 Remote Component 插件 或其他 View Extension 能力。不要让 Agent middleware 承担 MCP App resource host 的职责;middleware 可以触发工作流,但 MCP Apps 应通过 MCP resource 和 MCP Apps Host 交付。

包结构

一个最小的 plugin-managed MCP App 包通常是。面向正式发布的工具插件通常放在 tools/;教程和参考实现放在 examples/
如果插件只贡献 plugin-managed MCP server,src/index.ts 可以很轻量,但仍应导出标准插件元数据,便于宿主展示、安装和初始化资源。

package.json

MCP server 入口必须被构建到 dist,并包含在发布包中。
@xpert-ai/plugin-sdk 放在 peerDependencies,由宿主提供 SDK 实例。stdio server 直接 import @modelcontextprotocol/sdk 时,应把它放在 dependencies。MCP Apps 插件推荐同时依赖 @modelcontextprotocol/ext-apps,使用官方 registerAppToolregisterAppResourceRESOURCE_MIME_TYPE helper。ext-apps 是 ESM 包,建议插件 server 使用 "type": "module" 并运行在 Node.js 20 或更高版本。

插件 Manifest

.xpertai-plugin/plugin.json 中声明 MCP server:
使用 ${PLUGIN_ROOT},不要把安装后的绝对 runtime 路径写入 manifest。Xpert 会把占位符保存在生成的 Toolset schema 中,并在运行时解析到当前加载的插件目录。这样插件重装或重新加载后,不会因为旧的 @runtime__... 路径失效。 如果 server 需要插件自己的可写数据目录,可以使用 ${PLUGIN_DATA}

Runtime 与企业级安全

plugin-managed MCP stdio server 不会在插件安装时启动。它会在 Xpert 初始化 MCP Toolset 以发现工具时启动,通常发生在 agent graph 编译或调用阶段。v1 仍保持 on-toolset-init 时机;on-first-tool-call 需要缓存工具元数据,后续再优化。 在企业部署中,stdio MCP server 会经过 Xpert 受控 stdio runtime:
  1. Xpert 为当前插件 runtime 副本解析 ${PLUGIN_ROOT}${PLUGIN_DATA}
  2. Xpert 校验 tenant、organization、workspace、Toolset、serverName 以及是否为 plugin-managed。
  3. Xpert 把 MCP server command 改写为平台 runner,而不是把原始命令直接交给 MCP adapter。
  4. runner 使用 shell: false 启动真实子进程,并限制 env、cwd、stderr 采集、启动超时、空闲超时、最大生命周期和进程组清理。
  5. MCPToolset.close() 以及 graph 正常完成、异常、abort 清理都会关闭登记的 runtime。
生产环境默认 fail-closed。只有平台策略配置完成后才启用 stdio runtime:
plugin-managed stdio server 应通过 Node.js 启动 ${PLUGIN_ROOT} 内的入口文件。Xpert 会拒绝旧的绝对 @runtime__... 路径、路径穿越和 symlink 逃逸。插件代码目录只作为只读来源,可写状态应放到 ${PLUGIN_DATA}。生产环境中的 custom stdio 命令必须命中管理员配置的 command allowlist。 生产环境默认禁用 initScripts。如果管理员显式启用,也必须走同一套 runtime 和策略限制。 插件可以在 policy.runtime 中请求 runtime 限制,但最终以平台策略为准并被 clamp:

Runtime 运维与审计

Super Admin 可以通过 MCP Runtime 运维页或 /api/xpert-toolset/operations/mcp-runtimes 查看 runtime 记录并停止仍在运行的实例。该接口默认只返回当前租户和当前组织范围内的数据。 runtime 管理分为两层:
  • 内存中的 runtime manager 只表示当前进程内仍可控制的 live runtime。
  • mcp_runtime_instance 审计表保存 runtime 生命周期与关联关系,默认按最近 180 天查询。
审计记录包含 tenant、organization、workspace、Toolset、plugin、server、execution、conversation、app instance、resource installation、status、PID、启动时间、关闭时间、关闭原因、空闲过期时间、最长生命周期时间和截断后的 stderr 摘要。记录只保存脱敏后的 command label/hash 和 policy snapshot,不保存 env、token、runner spec 或原始 HTML。 运维页支持按 workspace、Toolset、plugin、execution、app instance、status 和时间范围查询。筛选项来自审计记录和当前 live runtime 的合并结果;workspace 选项和表格范围列会优先显示工作区名称,无法解析名称时回退到短 ID。Toolset 选项优先显示 Toolset 名称。 状态语义:
  • live 表示该 runtime 仍在当前 API 进程的 runtime manager 中,且可以被平台停止。
  • running / starting 只表示记录状态;历史记录如果不是 live,不能再被 stop。
  • runtime manager 在列表查询时会校验 runner PID 是否仍存在;如果进程已经退出,会把记录收敛为关闭/失败状态并移出 live 列表。
  • origin 用于区分 runtime 来源:Agent toolset 表示 Agent 初始化 Toolset 时启动,MCP App Host 表示 MCP App revive 或 iframe host 为资源/RPC 重新创建 client 时启动。
运维页的 Actions 列提供两类操作:
  • 每行的 停止:只停止该行对应的 live runtime。
  • 表头的 停止当前筛选:停止当前筛选条件下所有 live 且处于 starting/running 的 runtime;历史记录只读,不会被停止。
所有停止操作仍通过受控 runtime manager 执行,关闭原因会写入审计记录,例如 admin-stopadmin-killidle-timeoutmax-lifetime-timeouttransport-closerunner-process-exited

MCP Server 入口

stdio 入口负责创建 MCP server、注册 resources/tools,并连接 StdioServerTransport
stdio MCP server 不要把普通日志写入 stdout,因为 stdout 是 MCP 协议通道。诊断日志应写入 stderr

注册 MCP App Resource

MCP App resource 必须使用 ui:// URI,并返回 text/html;profile=mcp-app
resource 展示 metadata,例如 titledescriptionicon,应放在 resource 的 _meta.ui 上。titledescription 可以是普通字符串,也可以是 Xpert 风格的 I18nObjecticon 使用共享的 IconDefinition 结构(svgimagefontemojilottie)。ChatKit 会根据当前 host 语言解析 titledescription,并在 MCP App 消息头部渲染 icon。 资源安全与呈现 metadata 应放在 resource 的 _meta.ui 上。resources/read 返回的 content item metadata 优先;registerAppResource 配置中的 _meta.ui 会出现在 resources/list 中,可作为 host fallback。不要把 CSP 或权限放在 tool _meta.ui 中,旧 host 兼容除外。tool _meta.ui 可以少量镜像展示 metadata 作为短生命周期兜底,但 resource 仍然是权威来源。 小型 demo 可以用 TypeScript 函数返回 HTML 字符串。生产应用更推荐把 App 按普通前端工程维护,并在构建阶段产出静态 HTML 或 JS asset;MCP resource handler 再从 dist 读取构建产物。这样比手写和维护大段模板字符串更可靠。 推荐的集中开发方式是把 MCP App 前端源码放在插件包内的 src/app
其中 index.html 只维护页面外壳,main.ts 维护标准 MCP Apps bridge 和交互逻辑,styles.css 维护样式。构建时使用 Vite 或 esbuild 把它们打成一个可由 MCP resource 返回的 dist/app/index.htmlsrc/lib/app-html.ts 这类 server 侧 helper 应只负责读取构建产物,不再手写大段 HTML 字符串。 esbuild 方式的最小构建脚本可以读取 src/app/index.html,打包 src/app/main.ts,内联 src/app/styles.css,并输出 dist/app/index.html。将该脚本接入插件构建,例如:
如果使用 Nx,确保 nx build <plugin> 会在编译 server 代码前后执行 App 构建,并确认发布包的 files 包含 dist.xpertai-plugin 和必要的构建脚本或源码目录。

注册 Tools

模型可见工具通过返回 _meta.ui.resourceUri 打开 MCP App。工具结果也可以包含 structuredContent 作为 App 初始状态,并返回文本作为不支持 UI 的客户端降级内容。
registerAppTool 会规范化 _meta.ui.resourceUri,并保留兼容用的 resource URI metadata。_meta['openai/outputTemplate'] 可作为 ChatGPT 兼容 alias,但 Xpert + ChatKit 不实现 ChatGPT 专属 window.openai API。 app-only 工具可以通过 MCP Apps bridge 调用,但不会暴露给 LLM。
Xpert 会同时校验两侧可见性:
  • 没有 model visibility 的工具不会暴露给 LLM
  • iframe 调用目标必须具备 app visibility,并且在 Toolset policy 中已启用

App HTML

App 运行在 sandbox iframe 中,通过 MCP Apps JSON-RPC bridge 与 ChatKit 通信。插件文档只保留编写和打包要点;完整宿主协议请看 ChatKit MCP Apps HTML App 至少应该:
  • 通过 ui/initialize 初始化
  • 处理 ui/notifications/tool-input,读取原始工具输入
  • 处理 ui/notifications/tool-result,读取原始工具结果
  • 通过 tools/call 调用 app-visible 工具
  • 通过 ui/notifications/size-changed 通知高度变化
  • 避免直接调用 Xpert 后端 API
bridge 消息结构、历史回放和初始工具结果的大小限制请参考 ChatKit MCP Apps。新的 App 应读取标准 CallToolResult 形状的 ui/notifications/tool-result.params.contentparams.structuredContent;如果需要兼容旧版本,可以临时 fallback 到 params.result。App 也应能处理没有初始 tool-result 的情况:旧历史消息或超过宿主内联阈值的大结果可能只收到 tool-input,此时应展示摘要、提示重新运行,或通过 app-visible 工具按需重新加载/分页读取数据。当 App 超过小型 demo 的复杂度后,建议像普通前端项目一样维护源码,并把 MCP resource 作为构建产物的交付机制。

国际化

插件 App 应根据 ui/initialize 返回值在 iframe 内部完成 UI 本地化。ChatKit 会提供 hostContext.localehostContext.languagehostContext.direction,并在 App 运行前设置 HTML 文档的 lang / dir 属性。MCP 工具结果尽量保持结构化和语言中立,再由 src/app/main.ts 或前端框架负责格式化标签、摘要、数字、日期、图表标题和校验消息。

主题和样式

ChatKit 会在 MCP App iframe 内注入公共 CSS 变量,完整变量表和示例见 ChatKit MCP Apps 的主题变量。插件 App 应使用通用 --mcp-app-* 变量来定义背景、文字、边框、按钮、圆角和字体,不要依赖 ChatKit 内部 class 或私有 token;其他 MCP Apps host 也可以复用这一契约。业务图表的数据系列色可以由 App 自己定义语义色板,避免把宿主的弱化 UI 色或灰阶 token 直接用于柱、线、点。
如果使用 ECharts、Mapbox、Monaco 等需要 JavaScript 主题对象的库,可以在 App 初始化后通过 getComputedStyle(document.documentElement) 读取这些变量,或从 ui/initialize 返回的 hostContext.themeCssVariables 读取同一份值。 如果 App 加载 CDN 资源,需要在 resource _meta.ui.csp.resourceDomains 中声明域名。如果 App 需要浏览器侧网络请求,需要在 resource _meta.ui.csp.connectDomains 中声明目标域;更推荐通过 resources/readtools/call 走宿主代理访问。

构建和安装

构建插件,并确认发布包中包含 dist/mcp-server.js.xpertai-plugin/plugin.json
在 Xpert 中安装或重新加载插件,然后初始化插件资源,让 Xpert 创建 plugin-managed MCP Toolset。把该 Toolset 挂到 agent 上,再在 ChatKit 中请求交互式结果。 本地开发时要注意:源码包重新构建后,Xpert 插件运行目录中的已安装副本不一定会自动更新。修改后需要重新安装或重新加载插件,确保运行副本拿到最新的 dist 文件和 manifest。

测试清单

发布前建议覆盖:
  • stdio server 能启动,并且普通日志不写入 stdout
  • App 构建产物 dist/app/index.html 已生成,并包含 MCP Apps bridge 脚本
  • tools/list 返回模型可见工具,并排除 app-only 工具
  • 模型可见工具结果包含 _meta.ui.resourceUri
  • MCP App resource 返回 text/html;profile=mcp-app
  • MCP App resource metadata 包含需要的 _meta.ui.csp_meta.ui.permissionsprefersBorder
  • structuredContent 结构符合 iframe 预期
  • app-only 工具包含 _meta.ui.visibility = ['app']
  • Toolset policy 启用了 App 需要的所有工具
  • iframe 能初始化、接收原始工具输入和工具结果、调用 app-only 工具并调整高度
  • 外部脚本和连接域名已写入 resource CSP metadata
  • 插件重装后不会持久化旧的 @runtime__... 绝对路径

故障排查

相关文档