WebMCP lets a web page expose its own functions as tools to the AI agent in the shopper's browser. Where an MCP server serves remote assistants with no browser session, WebMCP tools run in the shopper's tab with their cookies, market and cart, and the shopper watches the page update as the agent works. WebMCP is in early preview in Chromium-based browsers.
WebMCP runs in Chromium-based browsers from version 146.0.7672 with the #enable-webmcp-testing flag, under a draft Community Group specification. The declarative attributes are ignored by browsers without WebMCP, so forms keep working everywhere. Imperative tools must check that document.modelContext exists.
MCP server | WebMCP | |
|---|---|---|
Agent | Remote assistant (Claude, ChatGPT) | The agent in the shopper's browser |
Session | None: | The shopper's own: cookies, market, cart |
Runs | On your server | In the page, as client-side JavaScript |
Calls | Your Discovery and Shop API functions | The same functions, through your server actions and routes |
Use the same tool names and arguments on both (search_products, add_to_cart, view_cart), so an agent that knows one surface knows the other.
Self-contained forms become tools with three attributes. The browser builds the input schema from the form's controls and labels:
<search>
<form action="/en/search" method="get"
toolname="search_products"
tooldescription="Search the catalogue by words, brand or spec, and list matching products."
toolautosubmit>
<label for="q">Search</label>
<input id="q" name="q" type="search" toolparamdescription="For example: 18V impact driver over 200 Nm">
<button type="submit">Search</button>
</form>
</search><search>
<form action="/en/search" method="get"
toolname="search_products"
tooldescription="Search the catalogue by words, brand or spec, and list matching products."
toolautosubmit>
<label for="q">Search</label>
<input id="q" name="q" type="search" toolparamdescription="For example: 18V impact driver over 200 Nm">
<button type="submit">Search</button>
</form>
</search>toolautosubmit off forms that place an order or pay. The browser then stops for the shopper to review and submit.<fieldset> to describe a group of radio buttons, such as the variant choice.fieldset with radio buttons and a labelled <input type="number" name="quantity" min="1">. A quantity shown as text between two buttons can't become a parameter (see Accessibility).When the agent submits, the submit event carries agentInvoked and respondWith(). Return the result as data instead of navigating, and return validation errors as text the agent can act on:
form.addEventListener('submit', (event) => {
if (!event.agentInvoked) return;
event.preventDefault();
const data = new FormData(event.target);
event.respondWith(addToCart(data.get('sku'), Number(data.get('quantity')), lang).then((count) => `Added. The basket now has ${count} items.`));
});form.addEventListener('submit', (event) => {
if (!event.agentInvoked) return;
event.preventDefault();
const data = new FormData(event.target);
event.respondWith(addToCart(data.get('sku'), Number(data.get('quantity')), lang).then((count) => `Added. The basket now has ${count} items.`));
});Style forms while an agent uses them with the :tool-form-active and :tool-submit-active pseudo-classes, so the shopper can see what is happening.
Use the imperative API for what isn't a single form: filters made of links, the cart drawer, the product on the page. In React, use useWebMCP from the use-webmcp-tool package rather than calling registerTool from components. Without a framework, register with an AbortSignal and abort it when the page changes:
if ('modelContext' in document) {
const controller = new AbortController();
await document.modelContext.registerTool(
{
name: 'view_cart',
description: "Show the shopper's basket: lines, quantities and totals.",
inputSchema: { type: 'object', properties: {} },
async execute() {
openCartDrawer();
return await cartView(lang);
},
annotations: { readOnlyHint: true },
},
{ signal: controller.signal },
);
// On navigation or unmount: controller.abort();
}if ('modelContext' in document) {
const controller = new AbortController();
await document.modelContext.registerTool(
{
name: 'view_cart',
description: "Show the shopper's basket: lines, quantities and totals.",
inputSchema: { type: 'object', properties: {} },
async execute() {
openCartDrawer();
return await cartView(lang);
},
annotations: { readOnlyHint: true },
},
{ signal: controller.signal },
);
// On navigation or unmount: controller.abort();
}A tool set that fits a storefront:
Tool | Page | Hint |
|---|---|---|
| Category, brand |
|
| Product |
|
| Every page |
|
| Every page | Reversible |
| Rental product |
|
{ error: "…" }) the agent can retry from.document.modelContext.getTools() and executeTool(), and by hand with the Chrome flag on.Chrome's Modern Web Guidance has detailed guides for agentic forms and JavaScript tools, which this page follows.