ui:// resource 和 Vanilla TypeScript App 组合成一个可交互插件。
适用场景
当你需要以下能力时,使用 plugin-managed MCP server:- 通过 MCP 协议暴露工具,而不是实现 Xpert 原生
ToolsetStrategy - 让工具实现尽量保持 MCP host 可移植性
- 在工具结果中返回可交互的 MCP App
- 提供 iframe 可调用、但模型不可见的 app-only 工具
- 把 MCP server 的安装、启用、停用和版本管理纳入插件生命周期
包结构
一个最小的 plugin-managed MCP App 包通常是。面向正式发布的工具插件通常放在tools/;教程和参考实现放在 examples/。
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,使用官方 registerAppTool、registerAppResource 和 RESOURCE_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:
- Xpert 为当前插件 runtime 副本解析
${PLUGIN_ROOT}和${PLUGIN_DATA}。 - Xpert 校验 tenant、organization、workspace、Toolset、serverName 以及是否为 plugin-managed。
- Xpert 把 MCP server command 改写为平台 runner,而不是把原始命令直接交给 MCP adapter。
- runner 使用
shell: false启动真实子进程,并限制 env、cwd、stderr 采集、启动超时、空闲超时、最大生命周期和进程组清理。 MCPToolset.close()以及 graph 正常完成、异常、abort 清理都会关闭登记的 runtime。
${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 天查询。
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 时启动。
- 每行的 停止:只停止该行对应的 live runtime。
- 表头的 停止当前筛选:停止当前筛选条件下所有 live 且处于
starting/running的 runtime;历史记录只读,不会被停止。
admin-stop、admin-kill、idle-timeout、max-lifetime-timeout、transport-close 或 runner-process-exited。
MCP Server 入口
stdio 入口负责创建 MCP server、注册 resources/tools,并连接StdioServerTransport。
stdout,因为 stdout 是 MCP 协议通道。诊断日志应写入 stderr。
注册 MCP App Resource
MCP App resource 必须使用ui:// URI,并返回 text/html;profile=mcp-app。
title、description 和 icon,应放在 resource 的 _meta.ui 上。title 和 description 可以是普通字符串,也可以是 Xpert 风格的 I18nObject;icon 使用共享的 IconDefinition 结构(svg、image、font、emoji 或 lottie)。ChatKit 会根据当前 host 语言解析 title 和 description,并在 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.html。src/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 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。
- 没有
modelvisibility 的工具不会暴露给 LLM - iframe 调用目标必须具备
appvisibility,并且在 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
CallToolResult 形状的 ui/notifications/tool-result.params.content 和 params.structuredContent;如果需要兼容旧版本,可以临时 fallback 到 params.result。App 也应能处理没有初始 tool-result 的情况:旧历史消息或超过宿主内联阈值的大结果可能只收到 tool-input,此时应展示摘要、提示重新运行,或通过 app-visible 工具按需重新加载/分页读取数据。当 App 超过小型 demo 的复杂度后,建议像普通前端项目一样维护源码,并把 MCP resource 作为构建产物的交付机制。
国际化
插件 App 应根据ui/initialize 返回值在 iframe 内部完成 UI 本地化。ChatKit 会提供 hostContext.locale、hostContext.language 和 hostContext.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 直接用于柱、线、点。
getComputedStyle(document.documentElement) 读取这些变量,或从 ui/initialize 返回的 hostContext.themeCssVariables 读取同一份值。
如果 App 加载 CDN 资源,需要在 resource _meta.ui.csp.resourceDomains 中声明域名。如果 App 需要浏览器侧网络请求,需要在 resource _meta.ui.csp.connectDomains 中声明目标域;更推荐通过 resources/read 或 tools/call 走宿主代理访问。
构建和安装
构建插件,并确认发布包中包含dist/mcp-server.js 和 .xpertai-plugin/plugin.json。
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.permissions、prefersBorder structuredContent结构符合 iframe 预期- app-only 工具包含
_meta.ui.visibility = ['app'] - Toolset policy 启用了 App 需要的所有工具
- iframe 能初始化、接收原始工具输入和工具结果、调用 app-only 工具并调整高度
- 外部脚本和连接域名已写入 resource CSP metadata
- 插件重装后不会持久化旧的
@runtime__...绝对路径
故障排查
| 现象 | 可能原因 | 修复方式 |
|---|---|---|
Cannot find module ... @runtime__... | 持久化的 MCP schema 中记录了旧安装运行目录 | 在 plugin.json 中使用 ${PLUGIN_ROOT},重新构建插件,并重新安装或初始化旧插件资源。 |
| MCP server 立即退出 | 缺少构建后的 dist/mcp-server.js、ESM/CJS 配置错误,或 server setup 阶段异常 | 本地执行 node dist/mcp-server.js 并查看 stderr;确认 package.json 中的 type、exports、files。 |
| app-only 工具仍出现在模型工具列表 | 缺少 _meta.ui.visibility = ['app'],或宿主没有保留 _meta | 为工具设置 visibility,并检查生成的 Toolset metadata。 |
App 能加载但 tools/call 失败 | 目标工具不是 app-visible,或被 Toolset policy 禁用 | 添加 app visibility,并在 mcpServers.*.policy.enabledTools 中启用该工具。 |
| CDN 脚本被阻止 | Resource CSP metadata 没有包含 CDN 域名 | 把域名加入 resource _meta.ui.csp.resourceDomains。 |