}` 引用。页面通过服务执行文件操作,不直接访问主应用源码或 Node API。
## 页面 API 速查
以当前宿主实现和锁定的 SDK 类型为准。先注册事件,再 `connect()`;连接后读取 `getHostCapabilities()`,按返回的能力启用功能。官方 API 文档描述 SDK 的完整表面,宿主支持范围还要看下表。
### 官方 MCP Apps API
| API / 事件 | 用途 | ZCode 行为与注意事项 |
| --------------------------------------------------------------- | ------------------------------------ | --------------------------------------------------------------------------------------------- |
| `new App(info, capabilities, options)`、`connect()` | 创建页面连接并完成握手 | 每个活页面只建一个 App;不要同时访问会触发另一连接的 `window.zcode` |
| `getHostVersion()`、`getHostCapabilities()`、`getHostContext()` | 读取宿主、能力、主题、语言和展示模式 | 在连接完成后使用;不要只按宿主版本号猜能力 |
| `ontoolinput`、`ontoolresult`、`ontoolcancelled` | 接收完整输入、结果和取消 | 大 UI 延迟加载时,保留初始结果再交给 UI,避免丢通知 |
| `onhostcontextchanged` | 响应主题、语言、尺寸或模式变化 | 更新显示;不要因此重复执行业务操作 |
| `callServerTool({ name, arguments }, options)` | 调用当前插件服务的工具 | 沿用 Agent 的权限和审批;`options.signal` 可取消;返回真实 `CallToolResult` |
| `readServerResource({ uri })` | 读取当前服务的资源 | 返回 MCP `contents`;二进制是 base64;不能用它访问其他插件或任意本地文件 |
| `listServerResources({ cursor })` | 分页列出服务资源 | 资源模板列表用后表中的 `resources/templates/list` 请求 |
| `sendMessage({ role: "user", content })` | 发出主对话后续消息 | 需要 `message` 能力;有用户手势时发送,无手势时进入确认流程 |
| `updateModelContext({ content, structuredContent })` | 给下一轮对话附加选区等上下文 | 需要 `updateModelContext` 能力;在输入区可见、可删,不立即发消息,不是持久存储 |
| `createSamplingMessage(params, { signal })` | App 内调用当前任务模型 | 需要 `sampling` 能力;支持文本与符合限制的图片输入,返回文本;对话历史由 App 提供 |
| `registerTool(name, config, handler)` | 页面提供可被模型调用的工具 | 握手前登记;声明 App 的 `tools` 能力;随活页面登记/撤销并遵守既有审批 |
| `onlisttools`、`oncalltool`、`sendToolListChanged()` | 手动管理页面工具目录 | 是另一种页面工具实现方式;取消信号在回调 `extra.mcpReq?.signal` 中;Showcase 提供完整例子 |
| `requestDisplayMode({ mode })` | 请求切换展示位置 | `inline` 为内联,`fullscreen` 为侧栏;以返回模式为准;`pip` 当前保持原模式 |
| `sendSizeChanged({ height })` | 报告内容高度 | 可用 SDK 自动测量;手动测量内容容器,避免把视口高度反馈给宿主 |
| `openLink({ url })` | 通过宿主打开外链 | 仅允许 `http:` / `https:` |
| `downloadFile({ contents })` | 调用原生保存对话框 | 需要 `downloadFile` 能力;支持嵌入资源或当前服务的资源链接;取消/写入失败返回 `isError: true` |
| `sendLog({ level, data })` | 发送可观察的页面诊断 | 不向 stdio 服务的 stdout 写调试日志;不要记录密钥或用户内容 |
| `onteardown` | 释放页面监听器等资源 | 宿主做有时限的 teardown,不应把唯一保存操作留到此时 |
| `requestTeardown()` | 请求宿主释放页面 | 当前 ZCode 只记录请求;实际生命周期跟随卡片/面板,不能把它当关闭按钮 |
Sampling 使用接纳请求时的任务模型,不读取主聊天历史;只有 App 明确传入的历史进入请求。当前不接受 `temperature`、`tools`、原生 audio 等未实现参数,`includeContext` 只支持 `none`。回答和取消不应被自动追加为主对话回合。
### ZCode 扩展与低层 MCP 请求
下面的 `app.request()` 是官方 App 的低层接口。请求结果应使用 `@modelcontextprotocol/core` 的对应 schema 校验。
| 接口 / 字段 | 调用或声明方式 | 适用范围 |
| ------------ | ----------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| 会话视图状态 | `ui/set-widget-state`,参数 `{ widgetState }`,结果 `EmptyResultSchema` | 先检查 `experimental["zcode/widgetState"]`;初值来自 hostContext 同名键;仅宿主内存,会话内重建可恢复,应用重启不保证 |
| 资源模板列表 | `resources/templates/list`,结果 `ListResourceTemplatesResultSchema` | 当前服务的 MCP 模板目录;不要假定 App 存在 `listServerResourceTemplates()` 便捷方法 |
| 资源订阅 | `resources/subscribe` / `resources/unsubscribe`,参数 `{ uri }`,结果 `EmptyResultSchema` | 先检查 `experimental["zcode/resourceSubscribe"]`;服务端也须支持订阅 |
| 资源变更通知 | `notifications/resources/updated` / `notifications/resources/list_changed` | 先用 `setNotificationHandler()` 注册处理器;收到 URI 后自行重新读取资源 |
| 当前侧栏面板 | `app.getHostCapabilities()?.experimental?.["zcode/surface"]` 返回 `{ id }` | 清单 `ui.surfaces[].id` 与工具 `_meta.ui.surface` 保持一致 |
| CSP 放宽 | 资源 `_meta["zcode/csp"]` 中 `unsafeEval`、`wasmUnsafeEval` | 检查 `experimental["zcode/csp"]` 返回的支持标记;不等于获得文件或网络权限 |
### 现有 `window.zcode` 页面
已有兼容页面可以继续使用以下别名。新页面优先使用官方 App;这两种连接方式选一种即可。
| 类别 | 可用成员 |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| 连接与观察 | `ready()`、`subscribe(listener)`,后者返回取消订阅函数 |
| 输入与宿主信息 | `toolInput`、`toolOutput`、`toolResponseMetadata`、`toolCancelled`、`hostInfo`、`hostCapabilities`、`hostContext`、`protocolVersion` |
| 展示信息 | `theme`、`locale`、`displayMode`、`maxHeight`、`safeArea`、`userAgent` |
| 工具与资源 | `callTool(name, args)`、`readResource(uri)`、`listResources()`、`listResourceTemplates()` |
| 资源订阅 | `subscribeResource(uri)`、`unsubscribeResource(uri)`、`onResourceUpdated(listener)`、`onResourceListChanged(listener)` |
| 会话视图状态 | `widgetState`、`setWidgetState(state)` |
| 对话协作 | `sendFollowUpMessage({ prompt, structuredContent })`、`updateModelContext({ content, structuredContent })` |
| 展示与文件 | `requestDisplayMode({ mode })`、`notifyIntrinsicHeight(height)`、`openExternal({ href })`、`downloadFile(contents)` |
别名没有 sampling 或页面工具的便捷接口;需要这些能力时使用官方 App。`openExternal` 的参数叫 `href`,官方 `openLink` 的参数叫 `url`,不要混用。
### 最小页面连接示例
这是需要打包到插件 HTML 中的页面代码,不是可直接在普通浏览器标签页中连接宿主的脚本。
```ts
import { App } from "@modelcontextprotocol/ext-apps";
import { EmptyResultSchema } from "@modelcontextprotocol/core";
const app = new App(
{ name: "my-panel", version: "0.1.0" },
{ availableDisplayModes: ["inline", "fullscreen"] },
);
let latestResult: unknown;
app.ontoolresult = (result) => {
latestResult = result.structuredContent;
document.querySelector("pre")!.textContent = JSON.stringify(latestResult);
};
function applyTheme() {
document.documentElement.dataset.theme = app.getHostContext()?.theme ?? "light";
}
app.onhostcontextchanged = applyTheme;
await app.connect();
applyTheme();
async function saveViewState(state: unknown) {
if (!app.getHostCapabilities()?.experimental?.["zcode/widgetState"]) return;
await app.request(
{ method: "ui/set-widget-state", params: { widgetState: state } },
EmptyResultSchema,
);
}
```
页面 HTML 需提供示例中的 ``;实际组件应在挂载后消费已经收到的 `latestResult`。这段代码只处理通信,文档保存仍通过插件服务执行。
## 平台、数据与能力边界
| 数据 / 能力 | 应放在哪里或如何使用 |
| -------------------------------- | ------------------------------------------------------------------------------------------ |
| 业务文档、清理计划、场景版本 | 插件服务持有;文件或数据库写入插件数据目录,按业务规则确认和恢复 |
| 当前活页面输入、滚动和运行中请求 | 同一活页面切内联/侧栏时保留;不重复连接或重发请求 |
| `widgetState` | 临时界面快照;重建、手动重试与进程重启的语义不同,不能用来承诺永久保存 |
| localStorage / IndexedDB | 稳定来源下的浏览器存储,可跨进程;插件身份/工作区/账号隔离,清浏览器数据不等于清插件文档 |
| 网络、字体、WASM、脚本 | 优先随包提供;外部访问受资源 CSP 限制,页面没有 Node 或任意文件系统访问权 |
| 原生权限 | camera / microphone / geolocation / clipboardWrite 需资源声明及宿主/系统授权;不是默认可用 |
当前 HTML 资源上限 **16 MiB**,页面资源读取上限 **8 MiB**;大页面可拆分资源。初始工具结果存在大小限制:`structuredContent` 64 KiB、页面元数据 16 KiB、`content` 32 KiB;超限字段会被省略并标记,页面应从服务重新读取完整数据。
**尚未实现的范围:**纯图片/资源链接的完整消息内容扩展、App 工具双向进度(Showcase SC51/SC52)。已有的文本附图、资源读取、模型工具行进度不能视为这些能力已经齐备。
官方 SDK 的概念与接口入口见 [MCP Apps API](https://apps.extensions.modelcontextprotocol.io/api/) 和 [Quickstart](https://apps.extensions.modelcontextprotocol.io/api/documents/quickstart.html)。宿主行为以本文对应的当前源码和实际能力协商为准,不据此承诺某个已发布版本的兼容范围。
## 宿主源码导航
| 排查目标 | 当前源码入口 |
| --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 能力声明 | [buildPluginUiHostCapabilities.ts](packages/ui/src/plugin-ui/domain/buildPluginUiHostCapabilities.ts) |
| 会话消息、上下文和 widgetState | [pluginUiInteractionPorts.ts](packages/ui/src/plugin-ui/app/pluginUiInteractionPorts.ts) |
| 工具、外链和展示模式 | [pluginUiHostAppHandlers.ts](packages/ui/src/plugin-ui/app/pluginUiHostAppHandlers.ts) |
| 资源订阅 | [pluginUiResourceSubscriptions.ts](packages/ui/src/plugin-ui/app/pluginUiResourceSubscriptions.ts) |
| 页面保留、回收与恢复规则 | [plugin-ui/CONTRACT.md](packages/ui/src/plugin-ui/CONTRACT.md) |
| HTML、资源与 Host 边界 | [plugin-ui-bridge/CONTRACT.md](packages/services/src/plugin-ui-bridge/CONTRACT.md) |
| Electron 沙箱与浏览器存储 | [pluginSandbox/CONTRACT.md](packages/desktop/src/main/pluginSandbox/CONTRACT.md) |
| API 限额、sampling 参数与别名类型 | [MCP Apps 契约](packages/shared/src/mcp-apps/contract.ts)、[sampling.ts](packages/shared/src/mcp-apps/sampling.ts)、[aliasApi.ts](packages/desktop/src/renderer/src/plugin-sandbox/aliasApi.ts) |
| 实际桌面集成测试 | [mcp-apps-host-e2e.mjs](packages/desktop/scripts/mcp-apps-host-e2e.mjs) |
五个插件的业务源码位于独立 `zcode-plugins` 仓库,其根目录也提供 `UI_PLUGIN.md`。本仓库负责宿主,不复制插件成品;宿主 API 变化时同步维护两边文档中英文版的能力与 API 表。