> ## Documentation Index
> Fetch the complete documentation index at: https://docs.xpertai.cn/llms.txt
> Use this file to discover all available pages before exploring further.

# MCP Tools 和 MCP Apps

插件可以把 MCP server 作为可安装资源交付。这是为 Xpert 增加标准 MCP tools 的推荐方式，也是交付可在 ChatKit 中内联渲染的 MCP Apps 的推荐方式。

本文只讲插件侧如何打包和实现。ChatKit 宿主运行时、iframe bridge、消息渲染和安全模型请参考 [ChatKit MCP Apps](../chatkit/chatkit-mcp-apps)。

如果你想从一个完整业务案例入手，可以继续阅读 [开发一个销售经营分析 MCP App 插件](../tutorial/echarts-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 插件](./remote-component) 或其他 View Extension 能力。不要让 Agent middleware 承担 MCP App resource host 的职责；middleware 可以触发工作流，但 MCP Apps 应通过 MCP resource 和 MCP Apps Host 交付。

## 包结构

一个最小的 plugin-managed MCP App 包通常是。面向正式发布的工具插件通常放在 `tools/`；教程和参考实现放在 `examples/`。

```text theme={null}
tools/my-mcp-app/
├── .xpertai-plugin/
│   └── plugin.json
├── package.json
├── src/
│   ├── index.ts
│   ├── mcp-server.ts
│   └── lib/
│       ├── mcp-tools.ts
│       ├── app-html.ts
│       └── data.ts
└── tsconfig.lib.json
```

如果插件只贡献 plugin-managed MCP server，`src/index.ts` 可以很轻量，但仍应导出标准插件元数据，便于宿主展示、安装和初始化资源。

```ts theme={null}
import { z } from 'zod';
import type { XpertPlugin } from '@xpert-ai/plugin-sdk';
import { MyMcpAppPluginModule } from './lib/plugin.js';

const plugin: XpertPlugin = {
  meta: {
    name: '@acme/plugin-sales-mcp-app',
    version: '0.1.0',
    category: 'tools',
    targetApps: ['xpert'],
    displayName: 'Sales MCP App',
    description: 'Interactive sales analysis tools delivered through MCP Apps.',
  },
  config: {
    schema: z.object({}),
  },
  register() {
    return { module: MyMcpAppPluginModule, global: true };
  },
};

export default plugin;
```

## package.json

MCP server 入口必须被构建到 `dist`，并包含在发布包中。

```json theme={null}
{
  "name": "@acme/plugin-sales-mcp-app",
  "version": "0.1.0",
  "type": "module",
  "main": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "bin": {
    "acme-sales-mcp-app": "./dist/mcp-server.js"
  },
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.js"
    },
    "./mcp-server": {
      "types": "./dist/mcp-server.d.ts",
      "import": "./dist/mcp-server.js"
    }
  },
  "files": [
    "dist",
    ".xpertai-plugin",
    "README.md"
  ],
  "dependencies": {
    "@modelcontextprotocol/ext-apps": "^1.7.4",
    "@modelcontextprotocol/sdk": "^1.29.0"
  },
  "peerDependencies": {
    "@xpert-ai/plugin-sdk": "^3.9.1",
    "zod": "^3.25.0"
  }
}
```

`@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：

```json theme={null}
{
  "name": "@acme/plugin-sales-mcp-app",
  "version": "0.1.0",
  "targetApps": ["xpert"],
  "targetAppMeta": {
    "xpert": {
      "types": ["mcp-server", "tool"],
      "capabilities": ["mcp-apps", "sales-analysis"]
    }
  },
  "mcpServers": {
    "sales-drilldown": {
      "type": "stdio",
      "command": "node",
      "args": ["${PLUGIN_ROOT}/dist/mcp-server.js"],
      "policy": {
        "enabled": true,
        "defaultToolsApprovalMode": "approve",
        "enabledTools": [
          "sales_overview",
          "sales_drilldown"
        ],
        "tools": {
          "sales_overview": {
            "approvalMode": "approve"
          },
          "sales_drilldown": {
            "approvalMode": "approve"
          }
        }
      }
    }
  }
}
```

使用 `${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：

```bash theme={null}
XPERT_MCP_STDIO_RUNTIME_ENABLED=true
XPERT_MCP_STDIO_ALLOWED_COMMANDS=node
XPERT_MCP_STDIO_MAX_CONCURRENT_PER_TENANT=10
XPERT_MCP_STDIO_STARTUP_TIMEOUT_MS=15000
XPERT_MCP_STDIO_IDLE_TIMEOUT_MS=1800000
XPERT_MCP_STDIO_MAX_LIFETIME_MS=7200000
```

plugin-managed stdio server 应通过 Node.js 启动 `${PLUGIN_ROOT}` 内的入口文件。Xpert 会拒绝旧的绝对 `@runtime__...` 路径、路径穿越和 symlink 逃逸。插件代码目录只作为只读来源，可写状态应放到 `${PLUGIN_DATA}`。生产环境中的 custom stdio 命令必须命中管理员配置的 command allowlist。

生产环境默认禁用 `initScripts`。如果管理员显式启用，也必须走同一套 runtime 和策略限制。

插件可以在 `policy.runtime` 中请求 runtime 限制，但最终以平台策略为准并被 clamp：

```json theme={null}
{
  "policy": {
    "runtime": {
      "provider": "local-process",
      "startupTimeoutMs": 15000,
      "idleTimeoutMs": 1800000,
      "maxLifetimeMs": 7200000,
      "allowedCommands": ["node"]
    }
  }
}
```

### 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-stop`、`admin-kill`、`idle-timeout`、`max-lifetime-timeout`、`transport-close` 或 `runner-process-exited`。

## MCP Server 入口

stdio 入口负责创建 MCP server、注册 resources/tools，并连接 `StdioServerTransport`。

```ts theme={null}
#!/usr/bin/env node
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { pathToFileURL } from 'node:url';
import { registerSalesMcpApp } from './lib/mcp-tools.js';

export function createSalesMcpServer() {
  const server = new McpServer({
    name: 'acme-sales-mcp-app',
    version: '0.1.0',
  });

  registerSalesMcpApp(server);
  return server;
}

export async function main() {
  const server = createSalesMcpServer();
  await server.connect(new StdioServerTransport());
}

if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
  main().catch((error) => {
    const message = error instanceof Error ? error.stack || error.message : String(error);
    process.stderr.write(`${message}\n`);
    process.exit(1);
  });
}
```

stdio MCP server 不要把普通日志写入 `stdout`，因为 stdout 是 MCP 协议通道。诊断日志应写入 `stderr`。

## 注册 MCP App Resource

MCP App resource 必须使用 `ui://` URI，并返回 `text/html;profile=mcp-app`。

```ts theme={null}
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import type { ReadResourceResult } from '@modelcontextprotocol/sdk/types.js';
import {
  RESOURCE_MIME_TYPE,
  registerAppResource,
} from '@modelcontextprotocol/ext-apps/server';
import { getDashboardHtml } from './app-html.js';

const SALES_APP_URI = 'ui://sales-dashboard';
const SALES_APP_CSP = {
  resourceDomains: ['https://cdn.jsdelivr.net'],
  connectDomains: [],
  frameDomains: [],
  baseUriDomains: [],
};
const SALES_APP_DISPLAY = {
  title: {
    en_US: 'Sales Performance',
    zh_Hans: '销售经营分析',
  },
  description: {
    en_US: 'Interactive sales dashboard with drilldown analysis.',
    zh_Hans: '支持下钻分析的交互式销售看板。',
  },
  icon: {
    type: 'svg',
    value: '<svg viewBox="0 0 24 24"><path fill="currentColor" d="M4 19h16v2H4z"/></svg>',
    alt: 'Sales dashboard',
  },
};

function buildDashboardResource(): ReadResourceResult {
  return {
    contents: [
      {
        uri: SALES_APP_URI,
        mimeType: RESOURCE_MIME_TYPE,
        text: getDashboardHtml(),
        _meta: {
          ui: {
            ...SALES_APP_DISPLAY,
            csp: SALES_APP_CSP,
            prefersBorder: true,
          },
        },
      },
    ],
  };
}

export function registerSalesMcpApp(server: McpServer) {
  registerAppResource(
    server,
    'sales-dashboard',
    SALES_APP_URI,
    {
      title: 'Sales Dashboard',
      description: 'Interactive sales dashboard rendered in ChatKit.',
      _meta: {
        ui: {
          ...SALES_APP_DISPLAY,
          csp: SALES_APP_CSP,
          prefersBorder: true,
        },
      },
    },
    () => buildDashboardResource(),
  );
}
```

resource 展示 metadata，例如 `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`：

```text theme={null}
src/app/
├── index.html
├── main.ts
└── styles.css
scripts/
└── build-app.mjs
dist/
└── app/
    └── index.html
```

其中 `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`。将该脚本接入插件构建，例如：

```json theme={null}
{
  "scripts": {
    "build:app": "node scripts/build-app.mjs"
  }
}
```

如果使用 Nx，确保 `nx build <plugin>` 会在编译 server 代码前后执行 App 构建，并确认发布包的 `files` 包含 `dist`、`.xpertai-plugin` 和必要的构建脚本或源码目录。

## 注册 Tools

模型可见工具通过返回 `_meta.ui.resourceUri` 打开 MCP App。工具结果也可以包含 `structuredContent` 作为 App 初始状态，并返回文本作为不支持 UI 的客户端降级内容。

```ts theme={null}
import { registerAppTool } from '@modelcontextprotocol/ext-apps/server';

const overviewMeta = {
  ui: {
    resourceUri: SALES_APP_URI,
    visibility: ['model', 'app'],
  },
  'openai/outputTemplate': SALES_APP_URI,
};

registerAppTool(
  server,
  'sales_overview',
  {
    title: 'Sales Overview',
    description: 'Show an interactive sales dashboard with drilldown analysis.',
    inputSchema: {
      metric: z.enum(['revenue', 'margin', 'orders']).default('revenue'),
      groupBy: z.enum(['region', 'product', 'month']).default('region'),
      year: z.number().int().default(2026),
    },
    annotations: {
      readOnlyHint: true,
      destructiveHint: false,
      idempotentHint: true,
      openWorldHint: false,
    },
    _meta: overviewMeta,
  },
  (args) => ({
    content: [{ type: 'text', text: 'Revenue by region for 2026.' }],
    structuredContent: {
      chart: {
        labels: ['West', 'East', 'South', 'North'],
        values: [7600000, 6500000, 5600000, 4800000],
      },
    },
    _meta: overviewMeta,
  }),
);
```

`registerAppTool` 会规范化 `_meta.ui.resourceUri`，并保留兼容用的 resource URI metadata。`_meta['openai/outputTemplate']` 可作为 ChatGPT 兼容 alias，但 Xpert + ChatKit 不实现 ChatGPT 专属 `window.openai` API。

app-only 工具可以通过 MCP Apps bridge 调用，但不会暴露给 LLM。

```ts theme={null}
registerAppTool(
  server,
  'sales_drilldown',
  {
    title: 'Sales Drilldown',
    description: 'App-only tool used by the dashboard after a chart click.',
    inputSchema: {
      metric: z.enum(['revenue', 'margin', 'orders']).default('revenue'),
      groupBy: z.enum(['region', 'product', 'month']).default('product'),
      year: z.number().int().default(2026),
      filters: z.object({
        region: z.string().optional(),
        product: z.string().optional(),
        month: z.string().optional(),
      }).default({}),
    },
    _meta: {
      ui: {
        visibility: ['app'],
      },
    },
  },
  (args) => ({
    content: [{ type: 'text', text: 'Drilldown data loaded.' }],
    structuredContent: {
      chart: {
        labels: ['Hardware', 'Software', 'Services'],
        values: [4100000, 2600000, 900000],
      },
      filters: args.filters,
    },
  }),
);
```

Xpert 会同时校验两侧可见性：

* 没有 `model` visibility 的工具不会暴露给 LLM
* iframe 调用目标必须具备 `app` visibility，并且在 Toolset policy 中已启用

## App HTML

App 运行在 sandbox iframe 中，通过 MCP Apps JSON-RPC bridge 与 ChatKit 通信。插件文档只保留编写和打包要点；完整宿主协议请看 [ChatKit MCP Apps](../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](../chatkit/chatkit-mcp-apps)。新的 App 应读取标准 `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 的主题变量](../chatkit/chatkit-mcp-apps#主题变量)。插件 App 应使用通用 `--mcp-app-*` 变量来定义背景、文字、边框、按钮、圆角和字体，不要依赖 ChatKit 内部 class 或私有 token；其他 MCP Apps host 也可以复用这一契约。业务图表的数据系列色可以由 App 自己定义语义色板，避免把宿主的弱化 UI 色或灰阶 token 直接用于柱、线、点。

```css theme={null}
body {
  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);
}

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

如果使用 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/read` 或 `tools/call` 走宿主代理访问。

## 构建和安装

构建插件，并确认发布包中包含 `dist/mcp-server.js` 和 `.xpertai-plugin/plugin.json`。

```bash theme={null}
pnpm nx build @acme/plugin-sales-mcp-app
```

在 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.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`。                                           |

## 相关文档

* [ChatKit MCP Apps](../chatkit/chatkit-mcp-apps)
* [MCP 工具](../agent/toolset/mcp-tools/mcp-tools)
* [Remote Component 插件](./remote-component)
* [插件的开发步骤](./development-steps)
* [发布与使用](./publish-and-use)
