# MCP API：bridge、relay 与工具

浏览器 bridge、relay 的 Node exports，以及 WU 发布的 18 个 MCP 工具参考。

<span id="tres-interfaces-con-responsabilidades-distintas"></span>

<span class="section-anchor" id="section-1"></span>

## 三种职责不同的接口

WU 集成浏览器 bridge 和 Node MCP 服务器。工具客户端通过 stdio 使用 JSON-RPC 与服务器通信，服务器通过已认证 WebSocket 请求连接页面执行操作。

| 接口 | 环境 | 访问 |
| --- | --- | --- |
| `wu.mcp` | 浏览器 | `connect`, `disconnect`, `isConnected` |
| `wu-framework/mcp-server` | Node | 配置、监听器、服务器和协议 helpers |
| `wu_*` 工具 | MCP 客户端 | 通过 `tools/list` 发布的目录 |

导入 `wu-framework/mcp-server` 不启动监听器，也不接管 stdin/stdout。运行 `wu-mcp-server` 或调用 `main(env)` 才会启动服务。[可运行 AI/MCP 示例](/zh-cn/docs/framework/ai-recipes/)用真实浏览器和无需 LLM provider 的本地 action 连接完整流程。

<span id="bridge-del-navegador"></span>

<span class="section-anchor" id="section-2"></span>

## 浏览器 bridge

```js
import wu from 'wu-framework';

await wu.aiReady();
await wu.mcp.connect(relayUrl, { token: sessionToken });

// Consulta de estado; connect no espera el resultado de autenticación.
const connected = wu.mcp.isConnected();

// Al retirar la integración:
wu.mcp.disconnect();
```

`relayUrl` 和 `sessionToken` 来自可信宿主集成。Token 不应放在 URL、公开 bundle 或 store 持久状态中。

| 方法 | 参数 | 返回值与生命周期 |
| --- | --- | --- |
| `connect(url?, options?)` | WebSocket URL；默认 `ws://localhost:19100`；必须提供 token，除非在 loopback 使用 `{ insecure: true }` | 加载/启动的 `Promise<void>`；socket 打开后开始握手 |
| `disconnect()` | 无 | `void`；关闭 socket、停止重连，并释放 bridge 订阅和拦截器 |
| `isConnected()` | 无 | `boolean`；检查 socket 打开及客户端已知的认证状态 |

4.0 bridge 通过双向 HMAC v2 握手验证 relay，且不发送 token。请同时升级 bridge 和 relay：4.0 之前的 relay 不支持 v2；新版 relay 保留 v1 以兼容旧版 bridges。缺少 token 会产生 `WU_MCP_TOKEN_REQUIRED`。本地模式要求 `{ insecure: true }`，且 relay 设置 `WU_MCP_INSECURE=1`。非 loopback 必须使用 `wss://`；不安全 URL 会产生 `WU_MCP_INSECURE_URL`。

bridge 正在连接或已连接时，不会打开另一连接。意外断开可能启动重连，`disconnect()` 取消该意图。关闭 MCP 不会自动卸载全部应用，也不取消已启动 handlers 的任意 Promise。

<span id="configuración-del-servidor"></span>

<span class="section-anchor" id="section-3"></span>

## 服务器配置

```js
import { loadConfig } from 'wu-framework/mcp-server';

const config = loadConfig(process.env);
```

`loadConfig` 验证变量并返回不可变对象，不输出或向客户端暴露 token。无效值抛出错误，不静默回退默认值。

| 变量 | 默认值 | 行为约定 |
| --- | --- | --- |
| `WU_MCP_TOKEN` | 必填 | 16–4096 UTF-8 字节字符串 |
| `WU_MCP_HOST` | `127.0.0.1` | 监听器 host，不含空格或 `/` |
| `WU_MCP_PORT` | `19100` | 1–65535 整数 |
| `WU_MCP_TIMEOUT` | `30000` | 每个 bridge 命令超时，50–300000 ms |
| `WU_MCP_AUTH_TIMEOUT` | `5000` | 身份验证等待时间，50–60000 ms |
| `WU_MCP_MAX_PAYLOAD` | `1048576` | WebSocket 上限，1024–16777216 字节 |
| `WU_MCP_ORIGINS` | HTTP(S) 回环地址 | 逗号分隔的精确 origin 列表，不含路径 |
| `WU_MCP_INSECURE` | 禁用 | 仅 `1` 允许省略 token；若设置 token，仍必须提供 |

结果包含 `host`、`port`、`timeout`、`authTimeout`、`maxPayload`、`token`、`insecure` 和 `allowedOrigins`。最后一项是 `Set` 或 `null`，不是要发送给浏览器的数组。

在 WebSocket HTTP upgrade 时检查 Origin。缺失或不允许的 Origin 收到 403。随后完成认证握手。relay 同时只允许一个已认证 bridge 处于活动状态；知道 token 也不能静默替换现有活动会话。

<span id="listener-puente"></span>

<span class="section-anchor" id="section-4"></span>

## Puente 监听器

```js
import { Puente, loadConfig } from 'wu-framework/mcp-server';

const puente = new Puente(loadConfig(process.env));
await puente.ready();

// Estas consultas no requieren una página autenticada.
const address = puente.address();
const connected = puente.conectado();

// Cuando el host termine su sesión:
await puente.close();
```

构造时启动监听器。`ready()` 在能够监听时解析，bind 失败时拒绝，不等待页面。构造函数第二参数可注入 `{ logger }`，必须提供 `error(...args)`。服务日志应写入 stderr，避免破坏 stdout 协议。

| 方法 | 返回值 | 失败或限制 |
| --- | --- | --- |
| `ready()` | `Promise<Puente>` | 启动失败时拒绝 |
| `address()` | 监听器地址 | 不说明是否有页面连接 |
| `conectado()` | `boolean` | 即时读取；之后会话可能关闭 |
| `enviar(command, params = {})` | `Promise<unknown>` | 无 bridge、断连、超时或响应错误时拒绝 |
| `close()` | `Promise<void>` | 拒绝待处理项，关闭 sockets 和监听器，不重新启动服务 |

`enviar` 接收 bridge 命令名称，例如 `list_actions`；对应的外部 MCP 名称是 `wu_list_actions`。不要混用两个层级。

各 request 保存标识符、超时和预期响应 socket，完成或失败时 relay 移除等待条目。`close()` 释放待处理项，并强制终止超过关闭宽限期仍未关闭的 sockets。

<span id="construir-o-iniciar-el-servidor-mcp"></span>

<span class="section-anchor" id="section-5"></span>

## 创建或启动 MCP 服务器

```js
import { main } from 'wu-framework/mcp-server';

// El entorno debe contener la configuración de autenticación.
await main(process.env);
```

`main(env)` 创建监听器、等待启动、注册工具并连接 stdio 传输，安装 SIGINT/SIGTERM 处理以关闭服务器和 bridge。它完成启动后解析，不返回可管理会话的对象。外部客户端使用的对应可执行文件是 `wu-mcp-server`。

宿主需要显式管理生命周期时：

| Export | 行为约定 |
| --- | --- |
| `createMcpServer(puente, config)` | 返回带 `connect(transport)` 和 `close()` 的服务器，并注册 18 个工具 |
| `createCommandRunner(puente)` | 返回 `(commandName, params?) => Promise<WuMcpToolResult>` 函数，适配 bridge 响应 |

宿主向 `connect` 提供兼容 MCP 传输。关闭嵌入式集成时，MCP 服务器和 `Puente` 都需关闭，一方不能替代另一方生命周期。下载示例使用打包可执行文件和最小 stdio 客户端，无需导入 bundle 内 SDK 私有类。

<span id="catálogo-de-herramientas"></span>

<span class="section-anchor" id="section-6"></span>

## 工具目录

以下为服务器注册的目录。工具参数经过 relay schemas，执行权限在 bridge 或 `wu.ai.execute` 再次检查。

| 工具 | 参数 | 主要结果 | 页面权限 |
| --- | --- | --- | --- |
| `wu_conexion` | `{}` | `{ puerto, host, requiere_token, app_conectada }` | 无需连接页面 |
| `wu_list_actions` | `{}` | `{ actions, count }` | 已认证 bridge；列出不授予执行权 |
| `wu_execute_action` | `{ action, params? }` | `{ action, success, result?, reason? }` | `executeActions` 和 action 权限 |
| `wu_status` | `{}` | Framework、应用、根键、actions/events 数量 | 已认证 bridge；根键需要 `readStore` |
| `wu_list_apps` | `{}` | 应用描述与状态 | 已认证 bridge |
| `wu_list_events` | `{ limit? }` | 已脱敏的最近事件 | `readDiagnostics` |
| `wu_get_state` | `{ path? }` | `{ path, value }` | `readStore` |
| `wu_set_state` | `{ path, value }` | `{ path, value, updated: true }` | `writeStore` |
| `wu_emit_event` | `{ event, data? }` | `{ emitted, data }` | `emitEvents`；禁止内部命名空间 |
| `wu_navigate` | `{ route }` | `{ navigated: route }` | `emitEvents`；更新 `currentPath` 还需 `writeStore` |
| `wu_snapshot` | `{ appName? }` | 应用、可访问树与 timestamp | `readDOM` |
| `wu_screenshot` | `{ selector?, quality? }` | DOM 捕获、元数据或错误 | `readDOM` |
| `wu_click` | `{ selector?, text? }` | 已激活元素或错误 | `modifyDOM` |
| `wu_type` | `{ selector, text, clear?, submit? }` | 写入结果或错误 | `modifyDOM` |
| `wu_console` | `{ level?, limit? }` | 已脱敏控制台条目 | `readDiagnostics` |
| `wu_network` | `{ method?, status?, limit? }` | 已过滤网络元数据 | `readDiagnostics` |
| `wu_mount_app` | `{ appName, container? }` | 已挂载应用 | `modifyDOM` 和正常挂载契约 |
| `wu_unmount_app` | `{ appName }` | 已卸载应用 | `modifyDOM` |

参数详情：

- 事件 `limit` 为 1–200 整数，默认 20；ring 最多保留 200 条。
- 控制台和网络 `limit` 为 1–500 整数，默认 50。控制台接受 `level: 'all' | 'log' | 'warn' | 'error'`，默认 `all`；网络接受 100 至 599 的 `status`。
- `quality` 为 0–1，bridge 默认 0.8。helper 默认使用 DOM/foreignObject 捕获和 PNG；quality 不改变 PNG 压缩，也不保证特权截图级别的还原度。
- `clear` 和 `submit` 默认 `false`。写入可能触发控件事件，`submit: true` 可能提交表单。
- `event` 为 1–200 字符字符串。relay 和 bridge 都拒绝 `wu:`、`system:` 和 `app:` 前缀。
- 读取时空 `path` 选择根，写入要求非空路径。诊断数据离开 bridge 前会脱敏。

`wu_navigate` 发出 `shell:navigate`，shell 必须处理该事件。仅凭结果无法证明页面已改变，使用的 gate 是 `emitEvents`，而非 AI 的 `navigate` flag。禁用 `readStore` 时，`wu_status` 返回 `storeKeys: []`。

<span id="json-rpc-y-resultados-negativos"></span>

<span class="section-anchor" id="section-7"></span>

## JSON-RPC 与失败结果

初始化 MCP 会话后，action 调用形式如下：

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "wu_execute_action",
    "arguments": { "action": "sum_local", "params": { "a": 2, "b": 3 } }
  }
}
```

服务器在 `content[0].text` 中返回文本。bridge 返回 `error` 字段或传输失败时，`createCommandRunner` 标记 `isError`。`success: false` 的 action 结果可能仍处于 `isError: false` 的 envelope 内。

分别检查 MCP 层和 action 层。HTTP 正常、socket 打开或 envelope 没有 `isError`，都不能证明 handler 获得授权并成功完成。

`tools/list` 列出安装版本的工具；`wu_list_actions` 列出另一目录，即页面在 AI 子系统注册的 handlers。执行 action 需要两层注册和对应权限。

<span id="helpers-de-autenticación-y-nombres"></span>

<span class="section-anchor" id="section-8"></span>

## 身份验证与名称 helpers

| Export | 签名 | 行为 |
| --- | --- | --- |
| `isOriginAllowed` | `(origin, allowedOrigins = null) => boolean` | 为 `null` 时，仅允许精确的 HTTP(S) 回环 origin |
| `tokenMatches` | `(candidate, expectedDigest) => boolean` | 使用 `timingSafeEqual` 比较候选值 SHA-256 与预期的 32 字节 digest |
| `isReservedEvent` | `(eventName) => boolean` | 检测三个保留内部命名空间 |

`tokenMatches` 的第二参数要求 digest，不是明文 token。digest 大小不正确可能导致比较错误。这些 helpers 不打开 socket，也不授予领域权限。

<span id="cancelación-seguridad-y-pruebas"></span>

<span class="section-anchor" id="section-9"></span>

## 取消、安全与测试

此实现协议不会将 MCP 取消传播到 `wu.ai.execute` handler。示例取消等待，并观察已开始的计算继续完成。`disconnect` 和 `close` 释放传输及待处理项，但不实现副作用补偿。

特权操作请使用范围受限的自定义 actions 和已认证服务。浏览器 AI 标志是页面技术控制，不能替代业务授权、幂等性或持久操作记录。

```sh
node examples/mcp/local-relay.mjs
node scripts/verify-server-examples.mjs --mcp
```

测试涵盖握手、错误 token、真实工具/actions、求和、无效 schema、权限拒绝、保留事件、可观察取消和 cleanup。token 在内存生成，所有 listeners 都是本地，无需 LLM provider。可[下载完整示例](/examples/wu-server-recipes.zip)，或查看[可运行 AI actions 与 MCP](/zh-cn/docs/framework/ai-recipes/)。