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());Discover and execute tools
Section titled “Discover and execute tools”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.
Registration cleanup
Section titled “Registration cleanup”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.
How it works
Section titled “How it works”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 legacynavigator.modelContext), registration and subscriptions are no-ops, discovery resolves to[], andisAvailable()returnsfalse. Execution rejects withWebMCPUnavailableErrorso missing support is distinct from anullnavigation 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 priorabort(). 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.
registerToolreturns 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.
Cross-document exposure
Section titled “Cross-document exposure”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
Section titled “Annotations”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.
Tool design
Section titled “Tool design”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— notdoAction,submit, orhandleClick. - 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. Markrequiredfields, 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
readOnlyfor inspection tools anduntrustedContentHintfor untrusted results. Compatibility fields such asdestructivemay 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.
Errors and unavailability
Section titled “Errors and unavailability”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().
Origin trial
Section titled “Origin trial”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),});Debugging in DevTools
Section titled “Debugging in DevTools”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
Testing and evals
Section titled “Testing and evals”Test WebMCP tools at two levels:
- Deterministic tests for
executeitself — plain unit tests, nothing WebMCP-specific. - 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.