Quickstart: Browser-Side AI Tools
Author your first WebMCP tool — typed input (Zod or JSON Schema 7), per-user RBAC, rate limits, and admin-catalog wiring — the full setup in 6 steps, in under 20 minutes.
Understand the Difference Between MCP and WebMCP
| Layer | Where it runs | Auth model | When to use |
|---|---|---|---|
| **MCP** | Server-side | Service account (per-tenant) | Background AI workflows, scheduled jobs, ETL |
| **WebMCP** | Browser-side | User's JWT / session | Interactive AI tools, in-context UI actions |
Use the React Hook (`useAeionTool`)
For tools tied to a specific React component / page, use the hook:
```tsx import { useAeionTool } from "@aeion/webmcp"; import { z } from "zod";
export function CustomerListPage() { // ... your component ...
useAeionTool({
name: "customer_bulk_archive",
description:
"Archive multiple customer records by ID. Returns a count of archived records.",
inputSchema: {
customerIds: z
.array(z.string().uuid())
.min(1)
.max(1000)
.describe("Customer record IDs to archive"),
reason: z
.string()
.min(5)
.max(500)
.describe("Reason for archival (audit log)"),
},
permissions: ["customers:archive"],
rateLimit: { maxCalls: 10, windowSeconds: 60 },
handler: async ({ customerIds, reason }, ctx) => {
const results = await Promise.allSettled(
customerIds.map((id) =>
ctx.apiClient(/api/v1/customers/${id}/archive, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ reason }),
}),
),
);
return {
success: true,
data: {
attempted: customerIds.length,
succeeded: results.filter((r) => r.status === "fulfilled").length,
failed: results.filter((r) => r.status === "rejected").length,
},
};
},
});
return <div>...</div>; } ```
The tool is automatically registered with the browser's navigator.modelContext when the component mounts and unregistered when it unmounts. AI agents can call it as long as the user is on the page. The handler's second argument (ctx) carries the current user, tenant, permissions, and a pre-authenticated apiClient — use ctx.apiClient rather than importing your own fetch wrapper, since it's the one the registry's rate-limit and audit pipeline is wired around.
Define Strict Inputs
| Pattern | Example |
|---|---|
| **Required field** | `required: ["customerIds"]` |
| **Length bounds** | `{ type: "string", minLength: 5, maxLength: 500 }` |
| **Number range** | `{ type: "number", minimum: 0, maximum: 100 }` |
| **Enum** | `{ type: "string", enum: ["draft", "active", "archived"] }` |
| **Array constraints** | `{ type: "array", minItems: 1, maxItems: 1000, uniqueItems: true }` |
| **Format validators** | `{ type: "string", format: "uuid" / "email" / "uri" / "date-time" }` |
| **Pattern** | `{ type: "string", pattern: "^[A-Z][A-Z0-9_]*$" }` |
| **Conditional** | `if: { properties: { mode: { const: "X" } } }, then: { required: ["xField"] }` |
Gate by Permission + Rate Limit
Per-tool permission requirements:
typescript
permissions: ["customers:archive", "customers:read"]; // ALL required
For useAeionTool (Step 2), the hook's own registry enforces this: the handler only runs if the current user has every listed permission, otherwise the call short-circuits with { success: false, error: "Insufficient permissions" } before your code ever runs. Module-factory tools (Step 5) are registered imperatively and only get a coarse module-level read-permission check at registration time — if you need the same per-call, per-permission enforcement for a factory tool, check ctx.entitlements / ctx.permissions yourself at the top of execute.
Rate limits:
typescript
rateLimit: {
maxCalls: 10, // 10 invocations
windowSeconds: 60, // per minute
}
The tool definition's rateLimit field accepts maxCalls and windowSeconds (per UseAeionToolOptions in @aeion/webmcp). The registry tracks counters per registered tool to surface back-pressure to the AI agent when the limit is hit.
Audit logging — every invocation fires a fire-and-forget audit record (tool name, result, duration, user, tenant, scope — no input payload) that's visible in Admin → Settings → AI Tools → Audit Log, filterable by success/error/denied. Because the automatic path doesn't capture your tool's input arguments, there's nothing to redact by default; if you need a record of exactly what was archived, log that from inside your own handler/execute (e.g., by relying on the target collection's own audit trail on the underlying API call).
Create a Module-Wide Factory
For tools available globally (not tied to a specific page), step outside React and create a module factory.
In your module's helper layer, define a factory matching ModuleActionConfig from @aeion/webmcp:
```typescript import type { ModuleActionConfig } from "@aeion/webmcp";
const makeResult = (data: unknown) => ({ content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }], });
export function createMyModuleActions(): ModuleActionConfig[] {
return [
{
name: "mymodule_bulk_archive",
description: "Bulk-archive records of your module's type",
category: "custom",
permissions: ["mymodule:archive"],
inputSchema: {
type: "object",
properties: {
recordIds: { type: "array", items: { type: "string" }, minItems: 1 },
},
required: ["recordIds"],
},
execute: async ({ recordIds }, ctx) => {
const ids = recordIds as string[];
const results = await Promise.allSettled(
ids.map((id) =>
ctx.apiClient(/api/v1/mymodule/${id}/archive, { method: "POST" }),
),
);
return makeResult({
attempted: ids.length,
succeeded: results.filter((r) => r.status === "fulfilled").length,
failed: results.filter((r) => r.status === "rejected").length,
});
},
},
// ... more tools ...
];
}
```
Note the factory itself takes no arguments — the API client comes through as ctx.apiClient on every execute call, not as a constructor parameter. execute also returns the { content: [...] } shape expected by navigator.modelContext.registerTool(), not a bare object.
Factory pattern benefits:
- Tools available everywhere in the admin (not just on specific pages)
- Single registration point per module (easier to audit)
- Testable in isolation
- Can compose multiple low-level API calls into a higher-level tool
Wire the Factory + Verify in Admin
Export your factory and register it in the admin's module-tools registry:
```typescript // In the admin's module-tools wire-up import { createMyModuleActions } from "@/modules/mymodule";
export const moduleTools = { mymodule: createMyModuleActions, // ... other modules ... }; ```
Open the admin → Settings → AI Tools. It has three tabs:
- Tools — every tool currently registered for this page/session, with name, category, scope, and an expandable row showing the raw input schema. It also shows two counters: how many tools are Registered in Aeion's internal registry vs. how many the connected agent can actually see via
navigator.modelContext.listTools()(Agent sees) — useful for spotting a tool that registered but never made it into the live MCP context. - Audit Log — every invocation (success / error / denied), filterable by status, with tool name, scope, role, permission required, and duration. Refreshes every 30 seconds.
- About — a reference on tool layers (base tools, collection CRUD, module workflow tools, page-contextual tools) and how to connect an agent.
There's no per-tool disable switch or a "dry-run with synthetic inputs" panel today — to pull a tool temporarily, stop registering it (unmount the component, or remove it from the module factory) and it disappears from navigator.modelContext on its next re-render.
End-to-end verification:
- Log in as a user with the required permissions
- Open AeionClaw chat or any AI surface
- Ask the AI to perform the action ("archive these 5 customers because of reason X")
- Verify execution succeeds + an entry appears in the Audit Log tab
Integration Recipes
AeionClaw skill calls WebMCP tools. A Claw skill can be configured to use specific WebMCP tools as part of its capability set. The user's RBAC carries through — Claw can't escalate privileges via WebMCP.
Browser-side data fetching for AI context. Use a query-only WebMCP tool to give the AI live data: "fetch all open tickets for this customer". Combined with a chat UI, the AI has real-time context without server-side polling.
Multi-step workflows. Combine multiple WebMCP tools — "find customers matching X criteria", then "email them this template", then "tag them with this segment". AI chains them automatically once each tool is well-defined.
Admin overrides. Build a "magic" admin-only tool that bypasses normal flow for ops scenarios. Gate behind a super-admin permission; log heavily; surface in incident-response dashboards.
Field-level autofill. WebMCP tools registered on a specific form can auto-fill fields from natural-language prompts ("generate a marketing campaign description for spring 2026 based on this product").
Bulk action wizards. Long-running bulk actions can use a WebMCP tool that returns immediately with a progress-tracking ID; the UI polls progress and shows live feedback. Avoids tying up the AI conversation while the work runs.
Troubleshooting
Tool not appearing in `navigator.modelContext`. Check that the component is mounted + the hook is firing. Common cause: conditional rendering wraps the tool registration. Move useAeionTool out of any conditional path.
AI never calls the tool. Two failure modes: (1) tool description is too vague — AI doesn't know when to use it; (2) tool name conflicts with another tool with similar semantics. Open the admin's Settings → AI Tools → Tools tab to confirm the tool actually registered (and that the connected agent can see it via navigator.modelContext.listTools()), then expand its row to review the description the AI reasons over.
Permission-denied errors despite user having permission. Permissions are checked at invocation time against the user's CURRENT roles. If their role was recently changed, ask them to log out / log in. Also check role-permission mappings under Admin → Identity → Roles.
Rate limit triggering unexpectedly. Limits are enforced client-side via sessionStorage, always keyed per user and per tool (never shared platform-wide) — double-check the maxCalls/windowSeconds you set on the tool definition. Because the counter lives in sessionStorage, it resets when the tab closes or in a fresh/incognito tab, which can make limits look inconsistent across test sessions.
Tool succeeds but returns no data. Make sure your execute function returns a JSON-serializable result. Returning undefined or null is fine but provides no AI context for next steps; return an object even on success ({ success: true, count: 0 }).
Slow tool execution causes UI lag. WebMCP tools run on the browser's main thread. Long operations (5+ seconds) should either: (1) split into smaller WebMCP tool calls, or (2) call a server-side endpoint that runs async and return a tracking ID.
Frequently Asked Questions
WebMCP for tools the user could do themselves in the UI (with the AI as an accelerator). Server-side MCP for autonomous background workflows where there's no user session. Rule of thumb: if the tool needs user RBAC context, use WebMCP; if it runs without a user (cron, ETL, scheduled job), use server-side MCP.
Four layers: (1) input validation (Zod for hook-registered tools, JSON Schema 7 for module-factory tools) before your code runs, (2) permission check against the user's current permissions — enforced per-call for hook-registered tools, so double-check factory-registered tools yourself if you need the same guarantee, (3) client-side rate limiting via `sessionStorage`, (4) audit logging of every hook-registered invocation. The user's JWT scopes every API call the tool makes — privilege escalation through the API itself is impossible.
Yes. Inside `execute`, use `fetch` to call any external endpoint. The user's JWT doesn't apply (it's an external API), but your tool can pass tenant-scoped credentials. Watch CORS — external APIs need to allow your domain.
The tool's `execute` keeps running until completion or browser tab close. Long-running tools should chunk work + emit progress events. AI gracefully handles the "page changed" case via tool unregistration events.
Two strategies: (1) add a version suffix to the tool name (`mymodule_bulk_archive_v2`); (2) add an explicit `version` field in the schema. AI uses the latest registered version automatically. Keep old versions registered for one or two release cycles for backward compatibility.
No persistent state — each invocation is independent. For state, use React state (page-scoped) or your DB (cross-session). The audit log gives you a history if you need it.
Yes. The module-factory pattern (Step 5) is framework-agnostic. The hook (Step 2) is React-specific, but the underlying registration API works from any JS context. Vue, Svelte, vanilla JS — all supported.
Two places: (1) browser DevTools console — schema validation errors log there; (2) Admin → Settings → AI Tools → Audit Log tab — filterable history of every invocation (success/error/denied) with duration and the permission that was checked. The Tools tab also lets you expand a tool's row to inspect its exact input schema.
Yes — the tool's `execute` runs in your component's React context, so you can use refs, setState, scrollTo, focus management, etc. Useful for "scroll the AI's response into view" or "highlight the matched fields" UX patterns.
Yes — WebMCP is built on the same Model Context Protocol primitives as Anthropic's MCP. Tools are exposed via `navigator.modelContext`, which any compliant AI client (Claude, GPT-via-MCP, custom agents) can consume. The browser-side execution + per-user RBAC are Aeion-specific extensions.