# MCP API: bridge, relay, and tools

Reference for the browser bridge, relay Node exports, and the 18 MCP tools published by WU.

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

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

## Three interfaces with different responsibilities

WU integrates a browser bridge and a Node MCP server. The tool client speaks JSON-RPC over stdio to the server; the server uses an authenticated WebSocket connection to request operations from the connected page.

| Interface | Environment | Access |
| --- | --- | --- |
| `wu.mcp` | Browser | `connect`, `disconnect`, `isConnected` |
| `wu-framework/mcp-server` | Node | Configuration, listener, server, and protocol helpers |
| `wu_*` tools | MCP client | Catalog published through `tools/list` |

Importing `wu-framework/mcp-server` does not start a listener or take over stdin/stdout. Running `wu-mcp-server`, or calling `main(env)`, does start the service. The [runnable AI/MCP recipe](/docs/framework/ai-recipes/) connects every endpoint with a real browser and a local action without an LLM provider.

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

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

## Browser 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` and `sessionToken` come from a trusted host integration. The token must not appear in URLs, public bundles, or persisted store state.

| Method | Parameters | Return and lifecycle |
| --- | --- | --- |
| `connect(url?, options?)` | WebSocket URL; default `ws://localhost:19100`; token required except with `{ insecure: true }` on loopback | Loading/startup `Promise<void>`. Begins the handshake after opening the socket |
| `disconnect()` | None | `void`. Closes the socket, stops reconnecting, and releases the bridge subscription and interceptors |
| `isConnected()` | None | `boolean`. Checks an open socket and authentication state known to the client |

The 4.0 bridge authenticates the relay with a mutual HMAC v2 handshake without sending the token. Upgrade bridge and relay together: relays before 4.0 do not understand v2; the new relay retains v1 for older bridges. Missing tokens fail with `WU_MCP_TOKEN_REQUIRED`. Local mode requires `{ insecure: true }` and `WU_MCP_INSECURE=1` on the relay. Outside loopback, `wss://` is required; an insecure URL fails with `WU_MCP_INSECURE_URL`.

The bridge avoids opening another connection while connecting or connected. Accidental disconnection may trigger reconnection; `disconnect()` cancels that intent. Closing MCP does not automatically unmount every app or cancel arbitrary promises from handlers already started.

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

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

## Server configuration

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

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

`loadConfig` validates variables and returns an immutable object. It neither prints nor exposes the token to a client. Invalid values throw instead of silently falling back to defaults.

| Variable | Default | Contract |
| --- | --- | --- |
| `WU_MCP_TOKEN` | Required | UTF-8 string of 16–4096 bytes |
| `WU_MCP_HOST` | `127.0.0.1` | Listener host; no spaces or `/` |
| `WU_MCP_PORT` | `19100` | Integer 1–65535 |
| `WU_MCP_TIMEOUT` | `30000` | Per-command bridge timeout, 50–300000 ms |
| `WU_MCP_AUTH_TIMEOUT` | `5000` | Authentication time, 50–60000 ms |
| `WU_MCP_MAX_PAYLOAD` | `1048576` | WebSocket limit, 1024–16777216 bytes |
| `WU_MCP_ORIGINS` | HTTP(S) loopback | Exact comma-separated origins, without a path |
| `WU_MCP_INSECURE` | Disabled | Only `1` permits omitting the token. If a token exists, it remains required |

The result contains `host`, `port`, `timeout`, `authTimeout`, `maxPayload`, `token`, `insecure`, and `allowedOrigins`. The last property is a `Set` or `null`, not an array sent to the browser.

Origin is checked during the WebSocket HTTP upgrade. A missing or disallowed Origin receives 403. The authentication handshake then completes. The relay permits one active authenticated bridge at a time; knowing the token does not allow silently replacing an existing session.

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

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

## Puente listener

```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();
```

Construction starts the listener. `ready()` resolves when it can listen and rejects bind failures; it does not wait for a page. You may inject `{ logger }` as the constructor's second argument; it must expose `error(...args)`. Service logs must go to stderr to avoid corrupting the stdout protocol.

| Method | Return | Failures or limits |
| --- | --- | --- |
| `ready()` | `Promise<Puente>` | Rejects startup failure |
| `address()` | Listener address | Does not indicate whether a page is connected |
| `conectado()` | `boolean` | Instantaneous read; the session may close afterward |
| `enviar(command, params = {})` | `Promise<unknown>` | Rejects without a bridge, on disconnection, timeout, or response error |
| `close()` | `Promise<void>` | Rejects pending requests, closes sockets and listener; does not restart the service |

`enviar` receives the bridge command name, for example `list_actions`. Its corresponding external MCP name is `wu_list_actions`. Do not interchange these levels.

Each request keeps an identifier, timeout, and the socket that must respond. The relay removes the pending entry on completion or failure. `close()` releases pending requests and forcibly terminates sockets that have not closed after the shutdown grace period.

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

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

## Constructing or starting the MCP server

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

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

`main(env)` creates the listener, awaits startup, registers tools, and connects stdio transport. It installs SIGINT/SIGTERM handlers to close the server and bridge. It resolves startup and does not return a session-management object. From an external client, the equivalent executable is `wu-mcp-server`.

For a host explicitly managing its lifecycle:

| Export | Contract |
| --- | --- |
| `createMcpServer(puente, config)` | Returns a server with `connect(transport)` and `close()`; registers the 18 tools |
| `createCommandRunner(puente)` | Returns a `(commandName, params?) => Promise<WuMcpToolResult>` function adapting the bridge response |

The host supplies compatible MCP transport to `connect`. When closing an embedded integration, close both the MCP server and `Puente`; neither replaces the other's lifecycle. The downloadable recipe uses the packaged executable and a minimal stdio client without importing private classes from the bundled SDK.

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

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

## Tool catalog

This is the server's registered catalog. Tool parameters pass through relay schemas; execution permissions are checked again in the bridge or `wu.ai.execute`.

| Tool | Arguments | Main result | Page permission |
| --- | --- | --- | --- |
| `wu_conexion` | `{}` | `{ puerto, host, requiere_token, app_conectada }` | Does not need a connected page |
| `wu_list_actions` | `{}` | `{ actions, count }` | Authenticated bridge; listing grants no execution permission |
| `wu_execute_action` | `{ action, params? }` | `{ action, success, result?, reason? }` | `executeActions` and action permissions |
| `wu_status` | `{}` | Framework, apps, root keys, action/event count | Authenticated bridge; root keys require `readStore` |
| `wu_list_apps` | `{}` | App descriptors and state | Authenticated bridge |
| `wu_list_events` | `{ limit? }` | Recent redacted events | `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`; internal namespaces prohibited |
| `wu_navigate` | `{ route }` | `{ navigated: route }` | `emitEvents`; `writeStore` to update `currentPath` |
| `wu_snapshot` | `{ appName? }` | App, accessible tree, and timestamp | `readDOM` |
| `wu_screenshot` | `{ selector?, quality? }` | DOM capture, metadata, or error | `readDOM` |
| `wu_click` | `{ selector?, text? }` | Activated element or error | `modifyDOM` |
| `wu_type` | `{ selector, text, clear?, submit? }` | Write result or error | `modifyDOM` |
| `wu_console` | `{ level?, limit? }` | Redacted console entries | `readDiagnostics` |
| `wu_network` | `{ method?, status?, limit? }` | Filtered network metadata | `readDiagnostics` |
| `wu_mount_app` | `{ appName, container? }` | App mounted | `modifyDOM` and the normal mounting contract |
| `wu_unmount_app` | `{ appName }` | App unmounted | `modifyDOM` |

Parameter details:

- Event `limit`: integer 1–200, default 20. The ring holds at most 200 entries.
- Console and network `limit`: integer 1–500, default 50. Console accepts `level: 'all' | 'log' | 'warn' | 'error'`, default `all`; network accepts `status` from 100 to 599.
- `quality`: 0–1, bridge default 0.8. The helper uses DOM/foreignObject capture and PNG by default; this quality does not change PNG compression or guarantee privileged-capture fidelity.
- `clear` and `submit`: `false` by default. Typing may emit control events; `submit: true` may trigger form submission.
- `event`: string of 1–200 characters. The `wu:`, `system:`, and `app:` prefixes are rejected in the relay and bridge.
- An empty read `path` selects the root; writing requires a nonempty path. Diagnostic data is redacted before leaving the bridge.

`wu_navigate` emits `shell:navigate`: the shell must handle that event. The result alone does not prove the page changed, and the gate used is `emitEvents`, not the AI `navigate` flag. `wu_status` returns `storeKeys: []` when `readStore` is disabled.

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

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

## JSON-RPC and negative results

After initializing the MCP session, an action call has this form:

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

The server delivers text in `content[0].text`. `createCommandRunner` sets `isError` when the bridge returns an `error` field or transport fails. An action result with `success: false` may remain inside an `isError: false` envelope.

Check the MCP and action layers separately. Do not interpret successful HTTP, an open socket, or an envelope without `isError` as proof that the handler was authorized and completed successfully.

`tools/list` lists tools in the installed version. `wu_list_actions` lists a different catalog: handlers the page registered in the AI subsystem. Executing an action requires both registrations and appropriate permissions.

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

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

## Authentication and naming helpers

| Export | Signature | Behavior |
| --- | --- | --- |
| `isOriginAllowed` | `(origin, allowedOrigins = null) => boolean` | With `null`, allows only exact HTTP(S) loopback origins |
| `tokenMatches` | `(candidate, expectedDigest) => boolean` | Compares the candidate's SHA-256 with an expected 32-byte digest using `timingSafeEqual` |
| `isReservedEvent` | `(eventName) => boolean` | Detects the three reserved internal namespaces |

`tokenMatches` expects a digest, not a plaintext token, as its second argument. An incorrectly sized digest may cause a comparison error. These helpers open no sockets and grant no domain permissions.

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

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

## Cancellation, security, and tests

This implementation's protocol does not propagate MCP cancellation to the `wu.ai.execute` handler. The recipe cancels a wait and observes that the calculation already started continues. `disconnect` and `close` release transport and pending requests, but do not compensate effects.

Use bounded custom actions and an authenticated service for privileged operations. Browser AI flags are technical controls for that page; they do not replace business authorization, idempotency, or durable operation records.

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

The test includes handshake, wrong token, real tools/actions, summation, invalid schema, denied permissions, reserved events, observable cancellation, and cleanup. The token is generated in memory, all listeners are local, and no LLM provider is required. You can [download the complete recipes](/examples/wu-server-recipes.zip) or review [runnable AI actions and MCP](/docs/framework/ai-recipes/).