Skip to content

WebMCP

Building block for the W3C WebMCP API exposed at document.modelContext. (For backward compatibility with the previous shape of the API, this package also reads from navigator.modelContext.) Register agent-callable tools from any framework; the package is vanilla TypeScript / DOM with an optional React adapter shipped as a subpath export. See useWebMCP for React.

Where the Built-in AI APIs make the browser an AI runtime, WebMCP makes your page an agent surface: instead of a visiting agent guessing what your interface elements do, the page declares structured tools the agent can discover and call.

import { registerTool } from "@web-ai-sdk/webmcp";
const cleanup = registerTool({
name: "add_to_cart",
description: "Add a SKU to the user's cart",
execute: async (input) => ({ ok: true, ...input }),
});
// Later, when the tool should no longer be exposed:
cleanup();

To register many at once, map over the array and combine the cleanups:

const cleanups = tools.map(registerTool);
const cleanup = () => cleanups.forEach((c) => c());
interface Tool<TInput = unknown, TOutput = unknown> {
name: string;
description: string;
inputSchema?: object;
readOnly?: boolean;
destructive?: boolean;
annotations?: ToolAnnotations;
execute: (input: TInput) => Promise<TOutput> | TOutput;
}
NameTypeDescription
name requiredstringTool name. Must be unique within the model context.
description requiredstringWhat the tool does. Routed to the agent’s tool selector.
execute required(input) => Promise<T> | TThe function the agent calls.
inputSchemaobjectJSON Schema describing the input shape. Some hosts validate against it before dispatch.
readOnlybooleanShorthand for annotations.readOnlyHint = true.
destructivebooleanShorthand for annotations.destructiveHint = true.
annotationsToolAnnotationsRaw passthrough (readOnlyHint, destructiveHint, idempotentHint, openWorldHint). Merged on top of the shorthand flags.

registerTool(tool) returns a cleanup function: () => void. Calling it unregisters the tool. Calling it twice is a no-op. On browsers without document.modelContext (or the legacy navigator.modelContext), the cleanup is itself a no-op.

document.modelContext.registerTool({...}, { signal }) is the spec entry point. (For backward compatibility with the previous shape of the API, this package also reads from navigator.modelContext.) The wrapper sits on top with three quality-of-life additions:

  • Feature detection. On browsers without document.modelContext (or the legacy navigator.modelContext), every entry point is a no-op so consumer code ships unchanged. isAvailable() returns false.
  • Last-writer-wins on duplicate names. React effect cleanup is asynchronous; a fast re-render can attempt to register "foo" before Chrome processed the prior abort(). The wrapper detects the resulting duplicate-name error, queues a microtask retry, and tracks ownership so a stale registration can’t squat the name.
  • One AbortController per call. registerTool returns a cleanup function that aborts the underlying controller and unregisters the tool.

Annotations communicate intent to the agent. WebMCP exposes shorthand flags (readOnly, destructive) plus a raw annotations passthrough that merges on top:

registerTool({
name: "delete_account",
description: "Permanently delete the user account",
destructive: true, // → annotations.destructiveHint
annotations: {
idempotentHint: false,
openWorldHint: true,
},
execute: () => { /* ... */ },
});

Shorthand flags map to the spec hints (readOnlyHint, destructiveHint); pass annotations directly when you need idempotentHint or openWorldHint.

A tool is a semantic interface, not just a function: the agent selects tools by reading names, descriptions, schemas, and annotations. A few rules of thumb:

  • Action-oriented names. add_to_cart, select_shipping_address, run_diagnostics — not doAction, submit, or handleClick.
  • Descriptions that say when to use the tool, not just what it does. Besides the schema, the description is the agent’s only routing signal.
  • Precise inputSchema. Mark required fields, describe every property, and use enums for closed sets so hosts can validate before dispatch.
  • Small, structured outputs. Return compact objects the agent can act on, and surface failures as structured error objects rather than thrown strings.
  • Honest annotations. readOnly for inspection tools, destructive for anything irreversible or user-impacting — and still confirm destructive actions with the user; an annotation is not authorization, and neither is a tool. Never expose a tool that bypasses your normal authorization checks.
  • Lifecycle-scoped registration. Register a tool only while the UI state it acts on exists, and call the cleanup when that state goes away.

The wrapper never throws on missing API; it’s a no-op. The vanilla functions return a no-op cleanup, so consumer code stays declarative:

import { registerTool, isAvailable } from "@web-ai-sdk/webmcp";
if (!isAvailable()) {
console.log("WebMCP not available; tools will not be exposed to agents.");
return;
}
registerTool({ ... });

Chrome opened a public origin trial for WebMCP (Chrome 149), so you can test agent-callable tools with real traffic before the API stabilizes. Sign up on the Chrome origin trials page; for local development a flag works without a token — see Browser support for the current flag names.

Keep WebMCP optional either way. The wrapper already no-ops when the API is missing, so the same registration code ships to every browser — with or without the trial:

import { registerTool } from "@web-ai-sdk/webmcp";
registerTool({
name: "add_to_cart",
description: "Add a product SKU to the user's cart",
inputSchema: {
type: "object",
properties: {
sku: { type: "string", description: "The product SKU to add" },
},
required: ["sku"],
},
execute: async ({ sku }) => addToCart(sku),
});

Chromium DevTools includes experimental WebMCP debugging in the Application panel (introduced in Chrome’s DevTools 149; Edge ships it too): inspect registered tools and their schemas, run a tool manually with custom parameters, track active and pending invocations, and inspect execution status and return payloads. Tools registered through registerTool show up there like any other document.modelContext registration.

The panel is off by default; enable both flags (under chrome://flags / edge://flags) and restart the browser:

  • #devtools-webmcp-support
  • #enable-webmcp-testing

Test WebMCP tools at two levels:

  1. Deterministic tests for execute itself — plain unit tests, nothing WebMCP-specific.
  2. Agent evals for tool selection — does an agent pick the right tool, with the right arguments, in the right order, from a natural-language prompt?

Chrome’s WebMCP evals tooling expresses the second kind as the call expected from a user prompt:

{
"messages": [{ "role": "user", "content": "Add this product to my cart." }],
"expectedCall": [
{ "functionName": "add_to_cart", "arguments": { "sku": "abc-123" } }
]
}

Multi-step journeys wrap call chains in ordered (must run sequentially) or unordered (any order). When an eval fails, the fix is usually in the tool’s name, description, or inputSchema — that is the interface the agent actually reads; see Tool design.