# Runnable AI actions and MCP

Register and execute WU actions without a provider; connect a real MCP relay and check schema, permissions, authentication, cancellation, and cleanup.

<span id="una-acción-no-necesita-un-proveedor-llm"></span>

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

## An action does not need an LLM provider

`wu.ai.execute` invokes a registered handler. You can use it from your own interface, an external agent, or a test without configuring `provider`, writing prompts, or supplying an API key. Providers are needed for operations that converse with models; action registration and direct execution work separately.

The `examples/ai/local-actions.mjs` recipe performs a real sum and checks rejections before the handler calculates it.

[Download the recipes project](/examples/wu-server-recipes.zip), extract the ZIP, and run `npm install` for dependencies. It includes the same actions used by the Node and MCP examples, along with their tests.

```sh
node examples/ai/local-actions.mjs
node scripts/verify-server-examples.mjs
```

<span id="registrar-habilitar-y-ejecutar"></span>

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

## Register, enable, and execute

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

const ai = await wu.aiReady();
ai.init({ permissions: {
  executeActions: true,
  readStore: false,
  writeStore: false,
  emitEvents: false,
  readDOM: false,
  modifyDOM: false,
  readDiagnostics: false,
  navigate: false,
  network: false,
} });

ai.action('sum_local', {
  description: 'Suma dos números sin efectos externos',
  parameters: {
    type: 'object',
    properties: { a: { type: 'number' }, b: { type: 'number' } },
    required: ['a', 'b'],
  },
  handler({ a, b }) {
    if (!Number.isFinite(a) || !Number.isFinite(b) || !Number.isFinite(a + b)) {
      throw new Error('Se requieren números y resultado finitos');
    }
    return { sum: a + b };
  },
});

const outcome = await ai.execute('sum_local', { a: 2, b: 3 });
// { success: true, result: { sum: 5 } }
```

Await `aiReady()` before operations returning data. `action` returns the AI object for chained registrations. `execute` returns a promise containing `{ success, result? , reason? }`; a permission or validation failure is normally represented by `success: false`, rather than throwing an exception to the caller.

`ai.tools()` exposes catalog schemas for external clients. A tool appearing in that list does not grant permission to execute it.

<span id="schema-permiso-y-reglas-de-dominio"></span>

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

## Schema, permissions, and domain rules

WU parameter validation is deliberately small: it checks required fields, top-level property types—including `integer`—and `enum`. It is not a complete JSON Schema validator. Rules such as numeric bounds, `additionalProperties`, formats, and recursive validation require product validation before effects occur.

The recipe tests a missing field, a string where a number was expected, and a non-finite value. WU rejects the first two cases; the handler rejects the third using an explicit rule. A counter demonstrates that only the allowed case reached the calculation.

```js
ai.permissions.configure({ executeActions: false });
const result = await ai.execute('sum_local', { a: 2, b: 3 });
// { success: false, reason: 'Permission denied: executeActions' }
```

An action can declare `permissions: ['readStore']` or `permissionsWhen(params)`. Required flags are checked before validation and the handler. The API supplied as the second argument offers only declared and enabled operations, such as `getState`, `setState`, `emit`, or `fetch`.

Keep these controls separate:

| Control | Decides |
| --- | --- |
| WU app capabilities | Technical access to store, events, and RPC through its facade |
| AI flags | Whether the subsystem may execute actions, read DOM, write state, or use the network |
| Domain authorization | Which business operation the server permits for the authenticated actor |
| Executor capability | Whether the backend or runner implements cancellation, transactions, or another operation |

Handlers are trusted host code. A closure can access global objects outside the supplied API; registration is not a sandbox for hostile JavaScript. For actions with backend effects, use a service that revalidates identity, schema, scope, and authorization. These recipes keep calculations pure and do not access privileged drivers.

<span id="confirmación-y-cleanup"></span>

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

## Confirmation and cleanup

```js
const stop = wu.on('ai:tool:confirm', event => {
  // La receta de rechazo no concede ninguna aprobación.
  ai.rejectTool(event.data.callId);
});

try {
  const result = await ai.execute('sum_with_confirmation', { a: 2, b: 3 });
  // success: false; reason: 'User denied action'
} finally {
  stop();
  ai.destroy();
  await wu.destroy();
}
```

The `sum_with_confirmation` action is registered in `examples/ai/register-actions.mjs` with `confirm: true`. A UI obtaining an affirmative decision uses `confirmTool(callId)`; `rejectTool` resolves denial. Without a decision, confirmation expires after 30 seconds and denies.

Resolve or reject pending confirmations before destroying the subsystem, and remove subscriptions installed by your integration. `destroy` cleans AI resources; it does not arbitrarily cancel promises or effects captured by your own handlers. It also does not turn in-memory history into a durable audit.

<span id="mcp-del-cliente-al-navegador-vivo"></span>

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

## MCP: from the client to the live browser

The package's MCP server is a relay. The client speaks MCP/JSON-RPC over stdio; the relay sends commands over WebSocket to the bridge on a page with WU loaded. The action executes on that page under its AI permissions.

```text
Cliente MCP → stdio → wu-mcp-server → WebSocket → wu.mcp → wu.ai.execute
```

Run the complete recipe:

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

The recipe starts the packaged server through the `wu-framework/mcp-server` export, serves the installed UMD bundle from a local listener, and opens headless Chromium. It uses Edge on Windows and Playwright Chromium on other systems. If the Playwright browser is missing, install its binaries with `npx playwright install chromium`. You can choose an installed channel through `WU_BROWSER_CHANNEL`.

It does not require an installed external MCP client: `examples/mcp/stdio-client.mjs` implements only the JSON-RPC operations needed to initialize, list tools, invoke, and cancel a wait. For general integration, use an MCP client handling the full protocol.

<span id="conectar-tu-propio-cliente"></span>

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

## Connecting your own client

The package installs the `wu-mcp-server` executable. Configure its environment from the host launching that process. Do not place the token in a URL or publish it in frontend code.

| Variable | Behavior |
| --- | --- |
| `WU_MCP_TOKEN` | Required unless insecure mode is explicitly enabled; 16–4096 bytes |
| `WU_MCP_HOST` | `127.0.0.1` by default |
| `WU_MCP_PORT` | `19100`; accepts 1–65535 |
| `WU_MCP_ORIGINS` | Exact comma-separated HTTP(S) origins; no paths |
| `WU_MCP_TIMEOUT` | Bridge response wait: 30000 ms default, range 50–300000 |
| `WU_MCP_AUTH_TIMEOUT` | Authentication wait: 5000 ms default, range 50–60000 |
| `WU_MCP_MAX_PAYLOAD` | 1048576 bytes default, range 1024–16777216 |
| `WU_MCP_INSECURE` | Only `1` allows omitting the token; the recipe keeps authentication mandatory |

Without `WU_MCP_ORIGINS`, HTTP(S) loopback pages are accepted; a WebSocket client without Origin fails that check. The recipe restricts the relay to the exact origin of the server it just created.

On the page, a trusted integration supplies the ephemeral token to the bridge:

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

The promise loads the module and starts connecting; it does not guarantee the handshake has finished. Check `wu.mcp.isConnected()` and query `wu_conexion` from the MCP client to observe both ends. The relay supports one active authenticated bridge at a time.

<span id="herramientas-y-lectura-de-resultados"></span>

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

## Tools and reading results

| Tool | Main arguments |
| --- | --- |
| `wu_conexion` | `{}`; relay state even without a connected page |
| `wu_list_actions` | `{}`; page action catalog |
| `wu_execute_action` | `{ action, params? }` |
| `wu_get_state` | `{ path? }`; requires `readStore` |
| `wu_set_state` | `{ path, value }`; requires `writeStore` |
| `wu_emit_event` | `{ event, data? }`; rejects `wu:`, `system:`, and `app:` namespaces |

The relay also registers tools for WU state, apps, events, navigation, DOM, diagnostics, and mounting. `tools/list` returns the exact catalog for the installed version. Its permissions remain enforced inside the page.

There are two result levels: the MCP envelope and the action result. A denied action may travel in an `isError: false` envelope while containing `success: false` in the JSON of `content[0].text`. Check both:

```js
const response = await client.callTool('wu_execute_action', {
  action: 'sum_local', params: { a: 2, b: 3 },
});
if (response.isError) throw new Error(response.content[0].text);
const outcome = JSON.parse(response.content[0].text);
if (!outcome.success) throw new Error(outcome.reason);
console.log(outcome.result);
```

This snippet uses the recipe's minimal client. Another SDK's `callTool` signature may differ.

<span id="cancelación-que-puedes-observar"></span>

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

## Observable cancellation

The recipe sends `notifications/cancelled` when canceling the wait for an MCP request. It then checks that the `bounded_sum` handler, already started in the browser, completes its fixed delay and calculation. The relay does not pass an `AbortSignal` to `wu.ai.execute`.

Canceling a wait therefore does not prove work stopped. For long-running operations with effects, design a service with an operation identifier, `status`/`cancel` authorization, and cooperative executor cancellation. WASM/WASI can host portable logic, but the host must still implement those contracts and network transport. The [WU-connected service](/docs/guides/browser-backend/) shows a different case: it connects the RPC signal to effective Worker termination.

When finished, call `wu.mcp.disconnect()` to stop reconnecting and release its interceptors. The test checks that `fetch` and `console.log` recover their previous references, the relay no longer sees a connected app, and created processes close. See [extensions and AI](/docs/framework/advanced/) and [security](/docs/framework/security/) for runtime limits.