Teaching your website to answer agents
On May 19, 2026, at Google I/O, Chrome announced WebMCP, and as of Chrome 149 it ships as an origin trial. WebMCP lets a web page expose structured tools to a browser-based AI agent, so the agent calls a function you defined instead of guessing its way through your DOM

Introduction
On May 19, 2026, at Google I/O, Chrome announced WebMCP, and as of Chrome 149 it ships as an origin trial. WebMCP lets a web page expose structured tools to a browser-based AI agent, so the agent calls a function you defined instead of guessing its way through your DOM. If you have ever watched an agent try to "click the blue button," this is the fix. This tutorial builds a working storefront page that an agent can search and add to a cart through real WebMCP tools, using both the imperative JavaScript API and the declarative HTML form approach.
What WebMCP actually is
WebMCP is the Model Context Protocol, brought to the browser. MCP already lets servers expose tools to models like Anthropic's Claude or OpenAI's GPT. WebMCP moves that contract to the front end: your page registers tools on navigator.modelContext, and any agent running in the browser (a Chrome extension, an Agentic Mode, a future navigator.ai caller) can discover and invoke them.
The point is reliability. Screen-scraping a page is a guess that breaks the moment you ship a redesign. A registered tool is a promise. The agent gets a name, a description, and a typed input schema, and it calls the function. Your CSS class names stop being load-bearing.
- Imperative API: register tools with standard JavaScript for anything (search, navigation, state changes).
- Declarative API: annotate an existing HTML <form> and Chrome turns it into a tool for free.
Getting it running in Chrome 149
WebMCP is behind a flag and not in stable Chrome yet. You need Chrome Canary 146.0.7672.0 or higher (the origin trial opens it up more broadly in Chrome 149)
# 1. Install Chrome Canary (macOS example)
brew install --cask google-chrome-canary
# 2. Launch it, then in the address bar go to:
# chrome://flags/#enable-webmcp-testing
# Set "WebMCP for testing" to Enabled, click Relaunch.
# 3. Serve any local page over http (WebMCP needs a real origin, not file://)
python3 -m http.server 8000Open http://localhost:8000 in Canary with the flag on, and navigator.modelContext exists. That object is your whole API surface
Registering your first tool
Here is the imperative API in full. The descriptor takes a name, a description the agent reads to decide when to call it, an inputSchema in JSON Schema form, and an execute handler that returns content the agent can read back.
// store.js - runs on your product page
const cart = [];
navigator.modelContext.registerTool({
name: "add_to_cart",
description: "Add a product to the shopping cart by its SKU and quantity.",
inputSchema: {
type: "object",
properties: {
sku: { type: "string", description: "The product SKU, e.g. 'TS-114'" },
quantity: { type: "integer", description: "How many to add", minimum: 1 }
},
required: ["sku"]
},
execute: ({ sku, quantity = 1 }) => {
cart.push({ sku, quantity });
renderCart();
return {
content: [{
type: "text",
text: `Added ${quantity} x ${sku}. Cart now has ${cart.length} line(s).`
}]
};
}
});The execute return shape matters. WebMCP expects { content: [{ type: "text", text }] }, the same content envelope MCP servers return. The agent does not see your renderCart() side effect. It sees the text you hand back, so write that text as the honest result of the action, not a hopeful one.
Giving the agent something to search
A cart tool is useless if the agent cannot find products first. Register a second tool that queries your real catalog. This is where WebMCP earns its keep: the agent runs your search logic, with your ranking, instead of parsing a results page.
const CATALOG = [
{ sku: "TS-114", name: "Merino wool sweater", price: 89, tags: ["wool", "winter"] },
{ sku: "TS-220", name: "Linen summer shirt", price: 54, tags: ["linen", "summer"] },
{ sku: "TS-305", name: "Wool blend scarf", price: 32, tags: ["wool", "winter"] }
];
navigator.modelContext.registerTool({
name: "search_products",
description: "Search the store catalog by keyword. Returns SKU, name, and price.",
inputSchema: {
type: "object",
properties: {
query: { type: "string", description: "Search keyword, e.g. 'wool'" }
},
required: ["query"]
},
execute: ({ query }) => {
const q = query.toLowerCase();
const hits = CATALOG.filter(
p => p.name.toLowerCase().includes(q) || p.tags.some(t => t.includes(q))
);
const lines = hits.map(p => `${p.sku}: ${p.name} ($${p.price})`).join("\n");
return {
content: [{ type: "text", text: hits.length ? lines : "No products found." }]
};
}
});Now an agent can run "search for wool, add the cheapest one to the cart" as two tool calls. It never touches the DOM. When the catalog changes, the tool keeps working, because the tool is the interface, not the markup.
Tools that talk to your backend
Real tools are not synchronous array filters over a hardcoded list. They call an API. The execute handler can return a promise, and WebMCP awaits it, so an async function that hits your backend behaves exactly like the synchronous ones above. This is where most production tools will live
navigator.modelContext.registerTool({
name: "check_stock",
description: "Check live inventory for a SKU. Returns the quantity in stock.",
inputSchema: {
type: "object",
properties: { sku: { type: "string", description: "Product SKU" } },
required: ["sku"]
},
execute: async ({ sku }) => {
try {
const res = await fetch(`/api/inventory/${encodeURIComponent(sku)}`);
if (!res.ok) {
return { content: [{ type: "text", text: `Stock lookup failed (${res.status}).` }] };
}
const { quantity } = await res.json();
return { content: [{ type: "text", text: `${sku}: ${quantity} in stock.` }] };
} catch (err) {
return { content: [{ type: "text", text: "Inventory service unreachable. Try again." }] };
}
}
});Two things matter here. The handler is async, and WebMCP waits for the promise to resolve before handing the result back, so calling your real /api/inventory endpoint needs no special treatment. And every failure path still returns the { content: [...] } envelope, never a thrown exception. An agent cannot read a JavaScript stack trace. If your tool throws, the agent sees nothing, falls back to guessing, and you are back to the scraping behavior WebMCP exists to remove. Catch the error and hand the agent a sentence it can reason about
Testing a tool before an agent ever sees it
You do not need a live agent to develop against WebMCP. The registration runs in the page, so you can confirm the API is present from the DevTools console and exercise the underlying logic while you build the UI
// In the Chrome DevTools console, on your page with the flag enabled:
"modelContext" in navigator; // -> true when the WebMCP flag is on
// Your tools register on page load. Drive the same functions your
// execute handlers call (renderCart, the catalog filter, the fetch)
// and confirm the side effects before wiring up an agent to call themBuild the tool, confirm the API is present, exercise the handler's real work by hand, and only then let an agent drive it. The agent is the last step, not the debugger. (If your tool only works when an agent calls it, you do not have a tool, you have a coincidence.)
The declarative shortcut
Most teams already have a checkout form. WebMCP can lift an existing <form> into a tool with annotations, no JavaScript handler required. Chrome reads the form fields and the agent submits through them.
<form id="newsletter"
data-tool-name="subscribe_newsletter"
data-tool-description="Subscribe an email address to the store newsletter.">
<label>
Email
<input name="email" type="email" required
data-tool-param-description="The subscriber's email address" />
</label>
<button type="submit">Subscribe</button>
</form>The data-tool-name and data-tool-description attributes register the form as a WebMCP tool. Each input's data-tool-param-description tells the agent what the field wants. Submit still works for human users exactly as before. You added an agent interface and changed nothing about the human one
Cleaning up
Tools registered on a single-page app should be removed when the view unmounts, or the agent will see stale tools that no longer have a backing UI.
// When leaving the product page
navigator.modelContext.unregisterTool("add_to_cart");
navigator.modelContext.unregisterTool("search_products");When to wire this up, and when to wait
Reach for WebMCP when your site has real actions an agent should take: search, filter, add to cart, book, configure. Anything where today an agent would scrape and misclick is a candidate, and exposing the tool is strictly more reliable than hoping the layout holds.
Hold off when your page is static content with nothing to do, or when your audience is not running agentic browsers yet. This is an origin trial, not a stable API, and the surface can still change before it lands for everyone. (Shipping a flag-gated feature to production users who do not have the flag is a great way to debug nothing for a week.) Treat it as a progressive enhancement: register the tools when navigator.modelContext exists, and lose nothing when it does not
if ("modelContext" in navigator) {
registerStoreTools();
}The web spent twenty years optimizing pages for humans who read and click. The next interface is a function call from something that does neither. WebMCP is the first standard that lets you answer it on purpose instead of by accident
You might also like
Keep reading from the journal.
June 6, 2026AI
The map that ran out of memory
Somewhere between the demo and the third customer, the product started dying.
June 2, 2026Agents
The agent that delegated
On May 28, 2026, Anthropic shipped the Agent SDK alongside Claude Opus 4.8. The SDK lets a single orchestrator spawn parallel subagents, each with its own tools and structured output schema. The agents run concurrently, return typed results, and the orchestrator synthesizes them.
May 18, 2026AI
Don't pre-build the AI layer
The retail industry has a name for the developer who builds the mall before the anchor tenant signs. Optimistic. The same developer five years later, when the mall is half empty and the anchor never showed, is called bankrupt.