WebMCP
Registering WebMCP tools with JavaScript
The imperative API is the stable path into the standard. What a good tool looks like, how the abort signal keeps a single-page app honest, and the mistakes that make an agent call the wrong function.
Last reviewed 27 August 2026
One call registers one tool. Everything else — how an agent chooses it, whether it gets the arguments right, whether it dares call it without asking — comes down to how you fill in the fields.
await document.modelContext.registerTool({
name: "search_products",
description:
"Search the product catalog by free text, with optional price and " +
"dimension filters. Returns up to 20 matches with price, key specs " +
"and stock status. Use this before get_product_details when the " +
"user has not named a specific product.",
inputSchema: {
type: "object",
properties: {
query: { type: "string", description: "Free-text search term" },
maxPrice: { type: "number", description: "Upper price bound in EUR" },
maxWidthCm: { type: "number", description: "Maximum device width in centimetres" },
},
required: ["query"],
},
annotations: { readOnlyHint: true },
execute: async ({ query, maxPrice, maxWidthCm }, { signal }) => {
const params = new URLSearchParams({ q: query });
if (maxPrice) params.set("max_price", String(maxPrice));
if (maxWidthCm) params.set("max_width_cm", String(maxWidthCm));
const res = await fetch(`/api/search?${params}`, {
credentials: "same-origin",
signal,
});
if (!res.ok) {
return { content: [{ type: "text", text: `Search failed: ${res.status}` }] };
}
return { content: [{ type: "text", text: JSON.stringify(await res.json()) }] };
},
});The description is the interface
An agent picks between your tools by reading their descriptions, and nothing else. This is the part engineers reliably under-invest in, because in every other API the docstring is commentary rather than dispatch logic. Here it is dispatch logic.
A description that works tends to answer three questions in order:
- What comes back. Not searches products but returns up to 20 matches with price, key specs and stock status. An agent that knows the shape of the return can plan two steps ahead.
- When to prefer this one. If you have
search_productsandget_product_details, say which comes first. Without it you get detail lookups on identifiers the agent invented. - What it will not do. Does not place orders is a sentence that saves a support ticket.
Hand the tool list — names and descriptions only, no code — to a colleague who has not seen the site, and read them a user request. If they cannot pick the right tool and guess the arguments, neither will an agent.
Schemas: typed, described, required
Without an inputSchema the agent sends whatever it invented. With one, the browser validates before your code runs. Three habits matter more than the rest:
- Give every property a
description.maxWidthCmwith maximum device width in centimetres gets metric input; without it you will receive inches. - Use
enumwherever the set is closed. It converts a free-text guess into a choice. - Keep
requiredgenuinely minimal. Every required field is another chance for the call to be abandoned rather than attempted.
Lifecycle: the abort signal is not optional
registerTool accepts an AbortSignal in its options, and aborting it unregisters the tool. In a single-page application this is what stops your tool list from describing a page the user left four navigations ago — an agent calling add_to_cart from a checkout flow that no longer exists is a genuinely bad outcome.
function useProductTools(product) {
useEffect(() => {
const controller = new AbortController();
document.modelContext.registerTool(
{
name: "add_to_cart",
description: `Add "${product.name}" to the cart. Confirm quantity with the user first.`,
inputSchema: {
type: "object",
properties: { quantity: { type: "integer", minimum: 1, maximum: 10 } },
required: ["quantity"],
},
execute: async ({ quantity }) => addToCart(product.id, quantity),
},
{ signal: controller.signal },
);
return () => controller.abort(); // leaves the view, drops the tool
}, [product.id]);
}The second signal — the one handed to execute — is a different thing: it aborts the invocation, not the registration. Thread it into every fetch you make so a cancelled agent turn does not leave requests running.
Keep the handler away from your UI state
A tool handler cannot reach into React state or a Redux store the way a click handler can. In practice this is the single biggest source of retrofit effort on existing applications, and it is a design problem rather than a WebMCP problem: business logic that only exists inside a component was always going to be hard to call from anywhere else.
The move is to extract the operation into a plain function that takes arguments and returns data, then let both the click handler and the tool call it. If that refactor looks large, it is worth scoping honestly before promising a delivery date.
How many tools
The practical ceiling is well under fifty per page. Past that, tool choice degrades — the agent has more plausible options than it can distinguish, and starts picking by name similarity. Most sites need three to eight on any given page: find, inspect, compare, act.
document.modelContext is undefined in every browser that has not enabled the trial, and the specification requires a secure context. Guard registration behind a check, or load the polyfill so the same code path works everywhere.
if (window.isSecureContext && "modelContext" in document) {
await registerAllTools();
}Sources
Primary documents, checked on 27 August 2026
- webmachinelearning.github.io/webmcp — W3C Web Machine Learning Community Group draft — WebIDL, annotations, permissions policy
- github.com/webmachinelearning/webmcp — Explainer repository, imperative and declarative API
- developer.chrome.com/docs/ai/webmcp — Origin trial, flags, permissions policy directive
- html.spec.whatwg.org — AbortSignal — Cancellation semantics used by registerTool and execute
- json-schema.org — The schema dialect used for inputSchema
Keep reading
Check your own site against this
The Agent Readiness Score measures exactly what this article describes, and shows the evidence behind every finding.
Run the check →