Skip to content

WebMCP

This package wraps the W3C WebMCP API at document.modelContext. It can register, discover, and execute tools. It also reads the earlier navigator.modelContext API for compatibility. For React, see useWebMCP.

WebMCP lets a page declare structured tools that agents can discover and call.

import { registerTool } from "@web-ai-sdk/webmcp";
import { z } from "zod"; // Zod 4; other Standard Schema libraries work too
const AddToCartInput = z.object({
sku: z.string().min(1),
quantity: z.number().int().positive().default(1),
});
const cleanup = registerTool({
name: "add_to_cart",
description: "Add a SKU to the user's cart",
input: AddToCartInput,
inputSchema: z.toJSONSchema(AddToCartInput, {
io: "input",
target: "draft-2020-12",
}),
execute: async ({ sku, quantity }) => addToCart(sku, quantity),
});
// 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());

getTools() returns metadata for tools exposed to the current document. executeTool() accepts one returned tool and serializes its input for the native call.

import {
executeTool,
getTools,
subscribeToToolChanges,
} from "@web-ai-sdk/webmcp";
const tools = await getTools();
const echo = tools.find((tool) => tool.name === "echo_message");
if (echo) {
const result = await executeTool(echo, { message: "hello" });
console.log(result);
}
const unsubscribe = subscribeToToolChanges(() => {
void getTools().then(renderToolList);
});

getTools({ fromOrigins }) also requests tools from descendant documents at listed secure origins. The registering document must expose each tool to the caller’s origin.

The browser returns inputSchema as serialized JSON. It also includes the registering window and origin.

executeTool() returns the native serialized string unchanged; null means the tool triggered navigation. Chrome publicly documents it, but it is not yet in the published WebMCP community-draft IDL, so the SDK marks it experimental.

interface Tool<TInput = unknown, TOutput = unknown> {
name: string;
title?: string;
description: string;
inputSchema?: object;
readOnly?: boolean;
destructive?: boolean;
annotations?: ToolAnnotations;
execute: (input: TInput) => Promise<TOutput> | TOutput;
}

Schema-aware definitions add optional Standard Schema fields. input is validated before application code runs, and execute receives its parsed or transformed output. output validates the resolved result and returns its parsed or transformed output.

interface ToolDefinition<
InputSchema extends StandardSchemaV1 | undefined = undefined,
TOutput = unknown,
OutputSchema extends StandardSchemaV1 | undefined = undefined,
> extends Omit<Tool, "execute"> {
input?: InputSchema; // Standard Schema; SDK-only
output?: OutputSchema; // Standard Schema; SDK-only
execute: (
input: InputSchema extends StandardSchemaV1
? StandardSchemaV1.InferOutput<InputSchema>
: unknown,
) =>
| Promise<
OutputSchema extends StandardSchemaV1
? StandardSchemaV1.InferInput<OutputSchema>
: TOutput
>
| (OutputSchema extends StandardSchemaV1
? StandardSchemaV1.InferInput<OutputSchema>
: TOutput);
}

Standard Schema is library-neutral and adds no runtime dependency to the SDK. Standard Schema validation and Standard JSON Schema conversion are orthogonal capabilities. The browser-facing inputSchema remains explicit so consumers choose the converter and JSON Schema dialect. When a library supports conversion, derive it from the same schema rather than maintaining a second handwritten definition.

Name Type Description
name required string Tool name. Must be unique within the model context.
title string Human-readable title for host user interfaces.
description required string What the tool does. Routed to the agent’s tool selector.
execute required (input) => Promise<T> | T The function the agent calls.
input StandardSchemaV1 SDK-only input validator. Its parsed or transformed value is passed to execute.
output StandardSchemaV1 SDK-only resolved-output validator. Its parsed or transformed value is returned.
inputSchema object JSON Schema describing the input shape. Some hosts validate against it before dispatch.
readOnly boolean Shorthand for annotations.readOnlyHint = true.
destructive boolean Compatibility shorthand for annotations.destructiveHint = true.
annotations ToolAnnotations Raw passthrough. The current draft defines readOnlyHint and untrustedContentHint; compatibility fields are described below.

registerTool(tool, options?) returns a cleanup function. It unregisters the tool and is safe to call more than once. Unsupported browsers return a no-op cleanup.

document.modelContext.registerTool({...}, { signal, exposedTo }) is the standard entry point. The wrapper adds this lifecycle behavior:

  • Feature detection. On browsers without document.modelContext (or the legacy navigator.modelContext), registration and subscriptions are no-ops, discovery resolves to [], and isAvailable() returns false. Execution rejects with WebMCPUnavailableError so missing support is distinct from a null navigation result.
  • Last-writer-wins on duplicate names. React effect cleanup is asynchronous; a fast re-render can attempt to register "foo" before the browser processes 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.
  • Library-neutral validation. Direct definitions may carry Standard Schema input and output validators. The SDK validates and transforms around execute, then strips those SDK-only fields before native registration.

By default, the owning document can discover its tool. Pass exposedTo when descendant documents at other origins also need access:

const cleanup = registerTool(tool, {
exposedTo: ["https://agent.example"],
});

The SDK forwards the array with its own AbortSignal. The browser validates each origin. The wrapper logs validation failures without creating an unhandled rejection.

List only origins that need access. exposedTo controls exposure; it does not grant authorization.

An in-page agent or inspector requests those cross-origin tools with the complementary discovery option:

const tools = await getTools({
fromOrigins: ["https://tool-provider.example"],
});

Annotations communicate intent to the agent. The current WebMCP draft defines readOnlyHint and untrustedContentHint. Use the latter when a tool returns external, user-generated, or otherwise untrusted content:

registerTool({
name: "search_community_posts",
title: "Search community posts",
description: "Search user-authored community posts.",
readOnly: true,
annotations: {
untrustedContentHint: true,
},
execute: async () => ({ results: await searchCommunityPosts() }),
});

untrustedContentHint is a trust-boundary signal; it does not validate result shape, truth, freshness, or safety. readOnly remains shorthand for the draft’s readOnlyHint.

For source compatibility, the SDK retains destructiveHint, idempotentHint, openWorldHint, and the destructive shorthand as passthroughs for MCP-shaped and earlier WebMCP hosts. They are not defined by the current WebMCP draft, so current-draft browsers may ignore them. Raw annotations values merge on top of shorthand values.

Agents select tools by reading their names, descriptions, schemas, and annotations. Follow these rules:

  • 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. Use readOnly for inspection tools and untrustedContentHint for untrusted results. Compatibility fields such as destructive may help older hosts, but no annotation is authorization. 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.

Registration and event subscription return no-op cleanups when the API is missing. Discovery returns an empty list.

Execution uses null for navigation. It therefore reports missing support with WebMCPUnavailableError:

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

Native getTools() errors remain observable, including invalid or untrustworthy fromOrigins, denied permissions policy, and detached-document failures. executeTool() also preserves native execution and cancellation failures.

When a direct definition includes input, invalid input rejects with ToolValidationError before execute runs. When it includes output, an invalid resolved result rejects with ToolOutputValidationError. Both errors preserve the tool name and Standard Schema issues. Synchronous and asynchronous validators and schema transformations are supported.

defineTool() remains as a deprecated compatibility wrapper. It preserves its historical validate: true input-validation opt-in, but new code should pass definitions directly to registerTool() or useWebMCP().

Chrome opened a public origin trial for WebMCP from Chrome 149, and Microsoft lists WebMCP among the Edge 150 origin trials. For local Chrome development, the vendor documents a flag that works without a token. See Browser support for the current setup and source links.

Keep WebMCP optional. Registration becomes a no-op when the API is missing, so the same code can run in other browsers:

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),
});

Chrome DevTools 149 added experimental WebMCP debugging to the Application panel. You can inspect tools, run them with custom input, and inspect results. Tools registered through this SDK appear with other document.modelContext tools.

The panel is off by default; enable both Chrome 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.