# API MCP: bridge, relay y herramientas

Referencia del bridge del navegador, exports Node del relay y las 18 herramientas MCP publicadas por WU.

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

## Tres interfaces con responsabilidades distintas

WU integra un bridge en el navegador y un servidor MCP en Node. El cliente de herramientas habla JSON-RPC por stdio con el servidor; éste utiliza un socket WebSocket autenticado para pedir operaciones a la página conectada.

| Interfaz | Entorno | Acceso |
| --- | --- | --- |
| `wu.mcp` | Navegador | `connect`, `disconnect`, `isConnected` |
| `wu-framework/mcp-server` | Node | Configuración, listener, servidor y helpers de protocolo |
| Herramientas `wu_*` | Cliente MCP | Catálogo publicado mediante `tools/list` |

Importar `wu-framework/mcp-server` no inicia un listener ni toma stdin/stdout. Ejecutar `wu-mcp-server`, o llamar `main(env)`, sí inicia el servicio. La [receta ejecutable de IA/MCP](/es/docs/framework/ai-recipes/) conecta todos los extremos con un navegador real y una action local sin proveedor LLM.

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

## Bridge del navegador

```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` y `sessionToken` provienen de una integración confiable del host. El token no debe formar parte de URLs, bundles públicos ni estado persistido en el store.

| Método | Parámetros | Retorno y ciclo de vida |
| --- | --- | --- |
| `connect(url?, options?)` | URL WebSocket; default `ws://localhost:19100`; token obligatorio salvo `{ insecure: true }` en loopback | `Promise<void>` de carga/inicio. Inicia el handshake después de abrir el socket |
| `disconnect()` | Ninguno | `void`. Cierra el socket, detiene reconexiones y libera suscripción e interceptores del bridge |
| `isConnected()` | Ninguno | `boolean`. Comprueba socket abierto y estado de autenticación conocido por el cliente |

El bridge 4.0 autentica al relay mediante un handshake HMAC mutuo v2 sin enviar el token. Actualiza bridge y relay juntos: un relay anterior a 4.0 no entiende v2; el relay nuevo conserva v1 para bridges antiguos. Sin token falla con `WU_MCP_TOKEN_REQUIRED`. El modo local exige `{ insecure: true }` y `WU_MCP_INSECURE=1` en el relay. Fuera de loopback exige `wss://`; una URL insegura falla con `WU_MCP_INSECURE_URL`.

El bridge evita abrir otra conexión si ya está conectando o conectado. Una desconexión accidental puede iniciar reconexiones; `disconnect()` cancela esa intención. Cerrar MCP no desmonta automáticamente todas las apps ni cancela promesas arbitrarias de handlers ya iniciados.

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

## Configuración del servidor

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

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

`loadConfig` valida las variables y devuelve un objeto inmutable. No imprime ni expone el token a un cliente. Los valores inválidos lanzan un error en lugar de caer silenciosamente al default.

| Variable | Default | Contrato |
| --- | --- | --- |
| `WU_MCP_TOKEN` | Obligatorio | String de 16–4096 bytes UTF-8 |
| `WU_MCP_HOST` | `127.0.0.1` | Host del listener; sin espacios ni `/` |
| `WU_MCP_PORT` | `19100` | Entero 1–65535 |
| `WU_MCP_TIMEOUT` | `30000` | Timeout por comando al bridge, 50–300000 ms |
| `WU_MCP_AUTH_TIMEOUT` | `5000` | Tiempo para autenticarse, 50–60000 ms |
| `WU_MCP_MAX_PAYLOAD` | `1048576` | Límite WebSocket, 1024–16777216 bytes |
| `WU_MCP_ORIGINS` | HTTP(S) loopback | Lista de origins exactos separados por comas, sin ruta |
| `WU_MCP_INSECURE` | Desactivado | Sólo `1` permite omitir token. Si hay un token, éste sigue siendo requerido |

El resultado contiene `host`, `port`, `timeout`, `authTimeout`, `maxPayload`, `token`, `insecure` y `allowedOrigins`. Esta última propiedad es un `Set` o `null`, no un array que se envíe al navegador.

La comprobación de Origin sucede en el upgrade HTTP de WebSocket. Un Origin ausente o no permitido recibe 403. Después se completa el handshake de autenticación. El relay admite un bridge autenticado activo a la vez; conocer el token no permite reemplazar silenciosamente una sesión que ya está activa.

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

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

La construcción inicia el listener. `ready()` resuelve cuando puede escuchar y rechaza ante un fallo de bind; no espera una página. Puedes inyectar `{ logger }` como segundo argumento del constructor; debe exponer `error(...args)`. Los logs del servicio deben ir a stderr para no corromper el protocolo de stdout.

| Método | Retorno | Fallos o límites |
| --- | --- | --- |
| `ready()` | `Promise<Puente>` | Rechaza si falla el arranque |
| `address()` | Dirección del listener | No indica si hay una página conectada |
| `conectado()` | `boolean` | Lectura instantánea; la sesión puede cerrarse después |
| `enviar(command, params = {})` | `Promise<unknown>` | Rechaza sin bridge, por desconexión, timeout o error de respuesta |
| `close()` | `Promise<void>` | Rechaza pendientes, cierra sockets y listener; no vuelve a iniciar el servicio |

`enviar` recibe el nombre del comando del bridge, por ejemplo `list_actions`. El nombre MCP externo correspondiente es `wu_list_actions`. No intercambies ambos niveles.

Cada request mantiene un identificador, un timeout y el socket que debe responder. El relay retira la entrada pendiente al completarse o fallar. `close()` libera pendientes y fuerza la terminación de sockets que no hayan cerrado tras el margen de cierre.

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

## Construir o iniciar el servidor 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)` crea el listener, espera su arranque, registra herramientas y conecta el transporte stdio. Instala manejo de SIGINT/SIGTERM para cerrar servidor y puente. Resuelve el arranque y no devuelve un objeto para gestionar la sesión. Desde un cliente externo, el ejecutable equivalente es `wu-mcp-server`.

Para un host que administra explícitamente su lifecycle:

| Export | Contrato |
| --- | --- |
| `createMcpServer(puente, config)` | Devuelve un servidor con `connect(transport)` y `close()`; registra las 18 herramientas |
| `createCommandRunner(puente)` | Devuelve una función `(commandName, params?) => Promise<WuMcpToolResult>` que adapta la respuesta del bridge |

El host suministra un transporte MCP compatible a `connect`. Al cerrar una integración embebida, cierra tanto el servidor MCP como `Puente`; uno no reemplaza el lifecycle del otro. La receta descargable utiliza el ejecutable empaquetado y un cliente stdio mínimo, sin necesitar importar clases privadas del SDK incluido en el bundle.

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

## Catálogo de herramientas

Este es el catálogo registrado por el servidor. Los parámetros de herramientas pasan por los schemas del relay; los permisos de ejecución se comprueban de nuevo en el bridge o en `wu.ai.execute`.

| Herramienta | Argumentos | Resultado principal | Permiso de página |
| --- | --- | --- | --- |
| `wu_conexion` | `{}` | `{ puerto, host, requiere_token, app_conectada }` | No necesita página conectada |
| `wu_list_actions` | `{}` | `{ actions, count }` | Bridge autenticado; listar no concede ejecución |
| `wu_execute_action` | `{ action, params? }` | `{ action, success, result?, reason? }` | `executeActions` y permisos de la action |
| `wu_status` | `{}` | Framework, apps, claves raíz, cantidad de acciones/eventos | Bridge autenticado; las claves raíz requieren `readStore` |
| `wu_list_apps` | `{}` | Descriptores y estado de apps | Bridge autenticado |
| `wu_list_events` | `{ limit? }` | Eventos recientes redactados | `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`; namespaces internos prohibidos |
| `wu_navigate` | `{ route }` | `{ navigated: route }` | `emitEvents`; `writeStore` para actualizar `currentPath` |
| `wu_snapshot` | `{ appName? }` | App, árbol accesible y timestamp | `readDOM` |
| `wu_screenshot` | `{ selector?, quality? }` | Captura DOM, metadatos o error | `readDOM` |
| `wu_click` | `{ selector?, text? }` | Elemento activado o error | `modifyDOM` |
| `wu_type` | `{ selector, text, clear?, submit? }` | Resultado de escritura o error | `modifyDOM` |
| `wu_console` | `{ level?, limit? }` | Entradas de consola redactadas | `readDiagnostics` |
| `wu_network` | `{ method?, status?, limit? }` | Metadatos de red filtrados | `readDiagnostics` |
| `wu_mount_app` | `{ appName, container? }` | App montada | `modifyDOM` y el contrato normal de montaje |
| `wu_unmount_app` | `{ appName }` | App desmontada | `modifyDOM` |

Detalles de parámetros:

- `limit` de eventos: entero de 1–200, default 20. El ring contiene como máximo 200 entradas.
- `limit` de consola y red: entero de 1–500, default 50. Consola acepta `level: 'all' | 'log' | 'warn' | 'error'`, default `all`; red acepta `status` entre 100 y 599.
- `quality`: 0–1, default 0.8 del bridge. El helper usa captura DOM/foreignObject y PNG por defecto; esa calidad no modifica la compresión PNG ni garantiza fidelidad de captura privilegiada.
- `clear` y `submit`: `false` por defecto. Escribir puede emitir eventos del control; `submit: true` puede activar el envío de un formulario.
- `event`: string de 1–200 caracteres. Los prefijos `wu:`, `system:` y `app:` son rechazados en el relay y el bridge.
- `path` vacío en lectura selecciona la raíz; escritura exige un path no vacío. Los datos diagnósticos pasan por redacción antes de salir del bridge.

`wu_navigate` emite `shell:navigate`: el shell debe manejar ese evento. El resultado no demuestra por sí solo que se haya cambiado la página y el gate utilizado es `emitEvents`, no el flag IA `navigate`. `wu_status` devuelve `storeKeys: []` cuando `readStore` está desactivado.

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

## JSON-RPC y resultados negativos

Después de inicializar la sesión MCP, una llamada a una action tiene esta forma:

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

El servidor entrega contenido textual en `content[0].text`. La función `createCommandRunner` marca `isError` cuando el bridge devuelve un campo `error`, o cuando falla el transporte. Un resultado de acción con `success: false` puede permanecer dentro de un sobre `isError: false`.

Comprueba la capa MCP y la capa de la action por separado. No interpretes un HTTP correcto, un socket abierto o un sobre sin `isError` como prueba de que el handler obtuvo autorización y terminó con éxito.

`tools/list` enumera herramientas de la versión instalada. `wu_list_actions` enumera otro catálogo: los handlers que la página registró en el subsistema IA. Para ejecutar una action hacen falta ambos registros y los permisos adecuados.

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

## Helpers de autenticación y nombres

| Export | Firma | Comportamiento |
| --- | --- | --- |
| `isOriginAllowed` | `(origin, allowedOrigins = null) => boolean` | Con `null`, permite únicamente origins HTTP(S) exactos de loopback |
| `tokenMatches` | `(candidate, expectedDigest) => boolean` | Compara el SHA-256 del candidato con un digest esperado de 32 bytes usando `timingSafeEqual` |
| `isReservedEvent` | `(eventName) => boolean` | Detecta los tres namespaces internos reservados |

`tokenMatches` espera un digest, no el token en texto como segundo argumento. Un digest de tamaño incorrecto puede provocar un error de comparación. Estos helpers no abren sockets ni conceden permisos de dominio.

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

## Cancelación, seguridad y pruebas

El protocolo de esta implementación no propaga una cancelación MCP hasta el handler de `wu.ai.execute`. La receta cancela una espera y observa que el cálculo que ya había comenzado continúa. `disconnect` y `close` liberan transporte y pendientes, pero no implementan compensación de efectos.

Usa acciones propias acotadas y un servicio autenticado para operaciones privilegiadas. Los flags IA del browser son controles técnicos de esa página; no sustituyen autorización de negocio, idempotencia ni un registro durable de operaciones.

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

La prueba incluye handshake, token incorrecto, herramientas/acciones reales, suma, schema inválido, permisos denegados, evento reservado, cancelación observable y cleanup. El token se genera en memoria, todos los listeners son locales y no se requiere proveedor LLM. Puedes [descargar las recetas completas](/examples/wu-server-recipes.zip) o revisar [acciones IA y MCP ejecutables](/es/docs/framework/ai-recipes/).