Right now, when an AI agent books a table on your site, it does it the hard way. It reads the page, guesses which input is "party size", types into it, finds something that looks like a submit button, clicks, and hopes.
WebMCP is a proposal to skip the guessing. Your page tells the browser: "here are the actions I support, here's the JSON Schema for each one, call this function." The agent calls the function. No DOM archaeology.
Chrome's team announced an early preview in February 2026, the spec is being developed at the W3C with Microsoft involved, and Chrome's docs were last updated in May 2026 (Chrome for Developers). It's still experimental. That makes now a good time to prototype it and a bad time to bet a launch on it.
⚠️ Version note (October 2026): early demos used
navigator.modelContext. Chrome's current docs usedocument.modelContext. Method names and options have shifted between Chrome versions. Check the current docs before copying anything, including from this post.
The mental model
If you've used MCP (Model Context Protocol) on the server side, WebMCP is the same idea moved into the page:
| Server MCP | WebMCP | |
|---|---|---|
| Where tools live | A separate MCP server process | JavaScript in your page |
| Who calls them | An AI app connected to your server | An agent running in the user's browser |
| Auth | You build it (OAuth, API keys) | The user's existing session and cookies |
| Best for | Back-end integrations | Actions a logged-in user takes on your site |
The second-to-last row is the important one. A WebMCP tool runs as the user, in their tab. You don't need a new auth system. You also inherit every security problem that comes with that, which we'll get to.
The API surface
From Chrome's imperative API docs, document.modelContext gives you:
-
registerTool(tool, options?): add a tool. Options include anAbortSignal(to unregister) andexposedTo(origins allowed to see it). -
getTools()andexecuteTool(): used by the agent side, and handy for testing. - A
toolchangeevent when the available tools change.
A tool definition has:
-
name: a stable identifier likesearch_products -
description: plain-language text the model reads to decide when to call it -
inputSchema: JSON Schema for the arguments -
annotations(optional): hints such asreadOnlyHintandconsequentialHint -
execute(input, { signal }): your function
There's also a declarative flavour that annotates existing <form> elements so you don't write JS at all. This post sticks to the imperative API because it's easier to reason about.
Example: a store search and add-to-cart
Here's a realistic pair of tools for an e-commerce page: one read-only, one that changes state.
// webmcp-tools.js
// Progressive enhancement: do nothing if the browser doesn't support WebMCP.
const mc = document.modelContext ?? navigator.modelContext; // older previews used navigator
if (mc?.registerTool) {
const controller = new AbortController();
mc.registerTool(
{
name: "search_products",
description:
"Search this store's catalogue. Returns up to 10 products with id, name, price in USD and stock status.",
inputSchema: {
type: "object",
properties: {
query: { type: "string", description: "Free-text search, e.g. 'trail running shoes'" },
maxPrice: { type: "number", description: "Optional upper price limit in USD" },
},
required: ["query"],
},
annotations: { readOnlyHint: true },
async execute({ query, maxPrice }, { signal }) {
const params = new URLSearchParams({ q: query });
if (maxPrice) params.set("max_price", String(maxPrice));
const res = await fetch(`/api/search?${params}`, { signal });
if (!res.ok) return `Search failed with status ${res.status}.`;
const items = await res.json();
return JSON.stringify(
items.slice(0, 10).map(({ id, name, price, inStock }) => ({ id, name, price, inStock }))
);
},
},
{ signal: controller.signal }
);
mc.registerTool(
{
name: "add_to_cart",
description:
"Add a product to the signed-in user's cart. Use a product id returned by search_products.",
inputSchema: {
type: "object",
properties: {
productId: { type: "string" },
quantity: { type: "integer", minimum: 1, maximum: 10 },
},
required: ["productId"],
},
annotations: { consequentialHint: true },
async execute({ productId, quantity = 1 }, { signal }) {
const res = await fetch("/api/cart", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ productId, quantity }),
signal,
});
if (!res.ok) return `Could not add item (status ${res.status}).`;
// Keep the visible UI in sync so the human sees what the agent did.
document.dispatchEvent(new CustomEvent("cart:updated"));
return `Added ${quantity} × ${productId} to the cart.`;
},
},
{ signal: controller.signal }
);
// Unregister when the SPA route changes, e.g. controller.abort();
}
A few choices worth copying:
- Feature-detect and exit quietly. Most browsers won't have this API for a while. The page must work without it.
- Reuse your existing API routes. The tool is a thin wrapper. Your server still validates everything, because the server can't tell whether a human or an agent clicked.
- Return small, structured results. Agents pay for every token you return. Don't send the whole product object.
- Update the UI. If the agent changes state, the human watching the tab should see it.
Testing it locally
Chrome exposes WebMCP behind an origin trial and/or flags, depending on version. Check the Chrome Status entry linked from the docs for your channel. Once it's on, you can poke your tools from DevTools:
const tools = await document.modelContext.getTools();
console.table(tools.map(t => ({ name: t.name, description: t.description })));
const result = await document.modelContext.executeTool(
tools.find(t => t.name === "search_products"),
{ query: "trail shoes", maxPrice: 120 }
);
console.log(result);
Security: treat every tool like a public API endpoint
WebMCP doesn't create new permissions. It makes existing ones easier for an automated caller to use. So:
-
Server-side authorization stays the source of truth. Never put "is this user allowed?" logic only in
execute. -
Mark consequential tools. Anything that spends money, sends messages, or deletes data should carry
consequentialHintso the agent and browser can ask the user to confirm. Don't count on that, though. Add your own confirmation step for irreversible actions. - Don't trust strings that come back in. The agent's arguments are model output, which may have been steered by a prompt injection on another page. Validate against your schema on the server.
-
Limit cross-origin exposure. Only use
exposedTofor origins you control. -
Rate-limit. An agent can call
add_to_cart500 times faster than a human can click.
The W3C repo has open issues about tool-registration security (for example, an early provideContext() call that could clear and replace another script's tools). Third-party scripts on your page can register tools too. Audit what your tag manager loads.
So is this SEO?
Not directly. WebMCP doesn't affect crawling, indexing or ranking, and Google hasn't said anything to suggest it will. Discovery still happens the old way: an agent has to find your page through search or a link before it can call your tools. Any blog post claiming "WebMCP is a ranking factor" is guessing.
Where it does matter is the step after discovery. If an agent is choosing between two stores to complete a task, the one with a clean, callable search_products is the one that finishes the job, and probably the one that gets used again. That's conversion optimization for a new kind of user.
What I'd do this quarter
- Fix the basics first: semantic forms and buttons (next post in this series). They help every agent today, with or without WebMCP.
- Pick one or two read-only tools (search, availability, store finder) and prototype them behind a feature flag.
- Log tool calls separately from normal API calls so you can see whether any agent actually uses them.
- Wait on write tools until the spec and the browser confirmation UX settle down.
Next in the series: Your next visitor is an agent: semantic HTML is SEO again.

Top comments (0)