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.
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, 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.
node examples/ai/local-actions.mjs
node scripts/verify-server-examples.mjs
Register, enable, and execute
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.
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.
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.
Confirmation and cleanup
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.
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.
Cliente MCP → stdio → wu-mcp-server → WebSocket → wu.mcp → wu.ai.execute
Run the complete recipe:
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.
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:
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.
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:
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.
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 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 and security for runtime limits.