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 compatible agents can discover and call. It does not guarantee discovery or invocation by an agent product.

Browser API support and agent-consumer support are separate compatibility dimensions. A browser provider exposes document.modelContext. An agent consumer discovers and invokes page tools in a product surface.

Chrome and Edge document browser API trials. They do not document ChatGPT product behavior. ChatGPT Desktop Site tools are a documented consumer in the desktop app’s built-in browser only. They are not Chrome browser support.

ChatGPT documents these Site-tool constraints:

  • Availability depends on the account, selected model, current page, and tools the page provides.
  • ChatGPT asks for website permission. Users can turn Site tools off in the desktop app.
  • Tools are page-scoped. They do not carry after navigation. Closing the page makes them unavailable.

Do not infer product behavior from Chromium engine lineage. The SDK can detect document.modelContext in the current document. It cannot prove that an external agent will discover or invoke a registered tool.

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 }, options) =>
addToCart(sku, quantity, { signal: options?.signal }),
});
// 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 an object or array.

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 an object or a legacy serialized string. It also includes the registering window and origin.

executeTool() returns the native serialized string unchanged; null means the tool triggered navigation. Pass { signal } as the third argument for native cancellation.

Omitted or undefined input defaults to {}. null and primitives reject with TypeError. Circular data, BigInt values, and failing toJSON() methods reject during serialization. Objects that serialize to undefined also reject.

Chrome accepts object input starting at 155.0.8052.0. The SDK uses verified native function arity to select object or legacy string input before invocation. Serialization runs once, and execution is never retried. Unrecognized arities reject with WebMCPUnavailableError.

Migration: pass { message: "hello" } instead of a primitive string when that matches the tool schema. Remove caller-side JSON.stringify(). The input parameter is now input?: object.

See Browser support for verified builds and the supported trial range.

interface ToolExecuteCallbackOptions {
signal: AbortSignal;
}
interface Tool<TInput = unknown, TOutput = unknown> {
name: string;
title?: string;
description: string;
inputSchema?: object;
readOnly?: boolean;
destructive?: boolean;
annotations?: ToolAnnotations;
execute: (
input: TInput,
options?: ToolExecuteCallbackOptions,
) => 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,
options?: ToolExecuteCallbackOptions,
) =>
| 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, options?) => Promise<T> | T The function the agent calls. options.signal reports browser cancellation.
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, untrustedContentHint, consequentialHint, and debugging; 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.

Register tools only while the page state they act on exists. Call the cleanup when that state changes or the page tears down. This aligns with the React hook’s mount, unmount, and enabled lifecycle.

The browser passes an execution AbortSignal through the callback’s optional second argument. Forward it to cancellable work:

execute: async (input, options) =>
fetch("/api/tool", {
method: "POST",
body: JSON.stringify(input),
signal: options?.signal,
});

Older trial browsers can omit the options. The SDK does not create a replacement signal.

Unregistering a tool does not cancel an execution that already started. Application code decides how to observe the execution signal.

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. It reports the native API only. It cannot report external-agent discovery or invocation. 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"],
});

getTools() preserves each native tool, its schema value, browser ordering, and cross-document metadata. An omitted inputSchema stays omitted.

The current WebMCP draft defines object-valued discovery schemas. Chrome switched in 155.0.8051.0. Earlier supported trial builds may return serialized strings, so RegisteredTool.inputSchema accepts object | string.

Check the value’s type when supporting mixed trial generations:

const tools = await getTools();
for (const tool of tools) {
const schema = tool.inputSchema;
if (typeof schema === "string") {
console.log("Legacy schema string", schema);
} else if (schema !== undefined) {
console.log("Schema object", schema);
}
}

The SDK does not parse strings or stringify, normalize, or clone objects. Malformed legacy strings remain observable. Registration still accepts an object-valued inputSchema.

The React hook exposes the same metadata type. See Browser support for the compatibility range.

Annotations communicate intent to the agent. The current WebMCP draft defines readOnlyHint, untrustedContentHint, consequentialHint, and debugging. Use untrustedContentHint 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.

Set annotations.consequentialHint: true for tools that can cause significant real-world or difficult-to-reverse effects. A compatible host can use this hint when deciding whether to request confirmation.

The hint is not authorization and does not guarantee a confirmation prompt. Applications must enforce authentication, authorization, validation, origin controls, and rate limits. The SDK does not add confirmation behavior.

The SDK forwards explicit true and false values and leaves the property absent when omitted. Discovery preserves the metadata returned by the host. There is no shorthand, and the SDK does not infer this value from destructiveHint.

registerTool({
name: "submit_order",
description: "Submit the reviewed order.",
annotations: { consequentialHint: true },
execute: async () => submitReviewedOrder(),
});

Set annotations.debugging: true for tools intended for debugging and developer tooling rather than end-user interactions. The field categorizes a tool. It does not grant permission or bypass application checks. Apply the same authentication, authorization, and validation as other tools.

The SDK forwards explicit true and false values and leaves the property absent when omitted. Discovery preserves the metadata returned by the host. There is no shorthand. The SDK does not filter tools by this field or infer other annotations from it.

registerTool({
name: "inspect_cart_state",
description: "Return the current cart state for debugging.",
readOnly: true,
annotations: { debugging: true },
execute: async () => ({ cart: getCartSnapshot() }),
});

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, untrustedContentHint for untrusted results, and debugging for developer-tooling tools. 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.

The WebMCP draft gates its APIs behind the tools policy-controlled feature. Its default allowlist is self. Chrome documents that this default allows top-level and same-origin documents.

To disable WebMCP in a document that must not expose tools, send this response header with the document:

Permissions-Policy: tools=()

The empty allowlist disables tools for the document and all descendant frames, including same-origin and cross-origin frames. The browser enforces the policy before page scripts run.

Under the draft, native registration, discovery, and execution reject with NotAllowedError in a disabled document. registerTool() logs the error. getTools() and executeTool() reject with it. isAvailable() checks only for the API and does not read the policy.

The draft’s security section presents this header as defense in depth. It blocks WebMCP use by injected scripts or compromised dependencies in that document. It does not:

  • authorize a tool call or user;
  • request user confirmation;
  • make tool content trusted;
  • prevent prompt injection in documents that keep WebMCP enabled.

The SDK does not set response headers. Keep the controls in Annotations and Tool design.

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, a 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. Microsoft Learn Edge 155 release notes list a WebMCP origin trial. The Edge 153 and Edge 154 notes list the same trial. The listing does not establish that Edge matches each Chrome build detail in this guide. Register through the Edge origin-trial listing. 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.