WebMCP: Make Your Site Callable by AI Agents (With Working Code)
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 a

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 use document.modelContext. Method names and options have shifted between Chrome versions. Check the current docs before copying anything, including from this post.
If you've used MCP (Model Context Protocol) on the server side, WebMCP is the same idea moved into the page:
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.
From Chrome's imperative API docs, document.modelContext gives you:
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.
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(); } Enter fullscreen mode Exit fullscreen mode A few choices worth copying:
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 ); Enter fullscreen mode Exit fullscreen mode 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:
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.
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.
Next in the series: Your next visitor is an agent: semantic HTML is SEO again.