MCP API: bridge, relay, and tools
Reference for the browser bridge, relay Node exports, and the 18 MCP tools published by WU.
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 connects every endpoint with a real browser and a local action without an LLM provider.
Browser bridge
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.
Server configuration
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.
Puente listener
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.
Constructing or starting the MCP server
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.
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 acceptslevel: 'all' | 'log' | 'warn' | 'error', defaultall; network acceptsstatusfrom 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.clearandsubmit:falseby default. Typing may emit control events;submit: truemay trigger form submission.event: string of 1–200 characters. Thewu:,system:, andapp:prefixes are rejected in the relay and bridge.- An empty read
pathselects 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.
JSON-RPC and negative results
After initializing the MCP session, an action call has this form:
{
"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.
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.
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.
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 or review runnable AI actions and MCP.