How to Implement WebMCP on Your Site in 2026
WebMCP turns actions on your page into named, typed tools that an AI agent in the same tab can call directly. This page gives you the declarative form markup, the imperative registerTool() code and the polyfill route, all checked against the current W3C Community Group draft.
WebMCP (Web Model Context Protocol) is a browser API that lets a page register named tools, each with a JSON Schema for its inputs and a callback you wrote, so an AI agent in the same tab can invoke them instead of guessing at your UI. Tools run inside the user's existing session, so there is no separate server to stand up and no credentials to copy anywhere. The API lives at document.modelContext, and a parallel declarative form covers the common case with no JavaScript at all.
Why WebMCP matters for AI visibility
A browser agent working a page without WebMCP reads the accessibility tree, guesses which control is the submit button, and retries when a click lands somewhere unexpected. Every re-render, hashed class name and A/B variant breaks that guess, and the failure is silent: the agent reports success on an action that never happened. A registered tool replaces the guess with a contract. The agent receives a name, a typed input schema and a return value, and your code decides what actually runs.
That makes WebMCP the one agent-facing surface where you set the vocabulary rather than a crawler. Adoption is still thin, so the cost of being early is small and the worst case is renaming a method later. For the background on where the protocol came from and how it sits next to Anthropic's MCP, the conceptual explanation lives here. The rest of this page is implementation.
Where the spec lives
WebMCP has changed shape several times since the first preview, and secondary write-ups go stale within weeks. Check these before you ship anything.
- WebMCP Draft Community Group Report - the normative text and WebIDL for
ModelContext,ModelContextTooland theDocumentpartial interface. The version current at the time of writing is dated 26 August 2026. - webmachinelearning/webmcp explainer and issues - the explainer with the canonical
registerTool()example, the separate declarative API explainer, and the open issues where field names get renamed. - Chrome for Developers: imperative API -
registerTool(),getTools(),executeTool(), thetoolchangeevent and AbortController-based unregistration. - Chrome for Developers: declarative API - the form attribute names and the
SubmitEventadditions that let a form return a value to the agent. - Chrome for Developers: tool security - the annotation hints, the
exposedTooption for cross-origin sharing, and the character budgets agents apply to names, descriptions and output. - WebMCP-org/npm-packages - the
@mcp-b/webmcp-polyfillruntime and@mcp-b/webmcp-typesdeclarations, tested against the upstream web platform tests.
Three ways to implement WebMCP
The declarative route annotates forms you already have and needs no JavaScript, which suits content sites and server-rendered apps. The imperative route registers tools from script, which is what you need when the set of available actions depends on application state. The polyfill route runs that same imperative code in browsers with no native implementation, which is how you ship to real traffic while the origin trials run.
Annotate an existing form (no JavaScript)
Four attributes turn a working HTML form into a tool. Use this first: if the form already has labelled, named controls it is most of the way there, and the markup degrades to an ordinary form everywhere else.
<form toolname="search_catalogue"
tooldescription="Search the product catalogue by keyword and category."
toolautosubmit
action="/search" method="get">
<label for="q">Keywords</label>
<input type="text" id="q" name="q" required
toolparamdescription="Words to match against product titles and descriptions.">
<label for="category">Category</label>
<select id="category" name="category"
toolparamdescription="Restrict results to one category.">
<option value="">All categories</option>
<option value="tents">Tents</option>
<option value="sleeping-bags">Sleeping bags</option>
</select>
<button type="submit">Search</button>
</form>
<script>
document.querySelector('form[toolname="search_catalogue"]')
.addEventListener('submit', (event) => {
if (!event.agentInvoked) return; // a person clicked Search: let it navigate
event.preventDefault();
event.respondWith(runSearch(new FormData(event.target)));
});
</script>
What this does: the browser derives the input schema from the named controls, so the agent sees a search_catalogue tool with a q string and a category enum. Without toolautosubmit the agent fills the form and waits for the user to press Search; with it, the form submits and the optional respondWith() handler returns a result to the agent instead of navigating.
Register a stateful tool with the imperative API
Reach for this when the tool is not a form: cart mutations, filters, canvas operations, anything where the available actions change as the user moves through the app. Registration is scoped to an AbortSignal, which is how you take a tool away again.
// ES module, loaded on product pages only.
if ('modelContext' in document) {
const controller = new AbortController();
await document.modelContext.registerTool(
{
name: 'add_to_cart',
title: 'Add to cart',
description: 'Add a product to the shopping cart. Returns the new cart total.',
inputSchema: {
type: 'object',
properties: {
sku: { type: 'string', description: 'Product SKU shown on the product page.' },
quantity: { type: 'integer', minimum: 1, description: 'How many units to add.' }
},
required: ['sku'],
additionalProperties: false
},
annotations: { readOnlyHint: false, untrustedContentHint: false },
async execute({ sku, quantity = 1 }, { signal }) {
const line = await cart.add(sku, quantity, { signal });
return `Added ${quantity} x ${sku}. Cart holds ${line.count} items, total ${line.total}.`;
}
},
{ signal: controller.signal }
);
// Leaving the product view removes the tool without killing work in flight.
router.on('leave', () => controller.abort());
}
What this does: the agent gets one unambiguous action with typed arguments, and the confirmation string tells it what changed so it does not have to re-read the page to verify. Aborting the controller fires a toolchange notification, so the agent stops offering an action that no longer exists.
Ship to every browser with the polyfill
Native WebMCP is Chromium only and still gated behind a flag or an origin trial, and no other engine implements it. The polyfill installs document.modelContext only when the browser has none, so one code path covers native and non-native visitors.
// npm install @mcp-b/webmcp-polyfill
// npm install -D @mcp-b/webmcp-types
import { initializeWebMCPPolyfill } from '@mcp-b/webmcp-polyfill';
// Idempotent: it never replaces a native document.modelContext.
initializeWebMCPPolyfill();
export async function registerArticleTools(article) {
if (!('modelContext' in document)) return () => {};
const controller = new AbortController();
await document.modelContext.registerTool(
{
name: 'get_article_summary',
description: 'Return the title, canonical URL and summary of the article on screen.',
inputSchema: { type: 'object', properties: {} },
annotations: { readOnlyHint: true, untrustedContentHint: true },
execute: async () => ({
title: article.title,
canonical: document.querySelector('link[rel="canonical"]')?.href,
summary: article.summary
})
},
{ signal: controller.signal }
);
return () => controller.abort();
}
What this does: visitors on a Chromium build with WebMCP enabled use the native implementation; everyone else gets the polyfill, which also picks up the declarative form attributes from example 01. Cross-document features such as non-empty exposedTo and fromOrigins reject under the polyfill, so keep those paths behind a native check.
Implementation guidelines
These are the things that break WebMCP in production rather than in a demo.
- Tie every registration to an AbortController. Register a tool when the action becomes possible and abort the signal when it stops being possible. A listed tool that fails on call is worse than no tool, because the agent has already told the user it will work.
- Stay inside the character budgets. Chrome's guidance is roughly 500 characters per tool description, 150 per parameter description, 30 for a name, and 1.5K for tool output. Longer strings get truncated or trip agent guardrails, and the agent silently drops the tool.
- Do not opt out of origin isolation. WebMCP is only exposed in origin-isolated documents. Chromium has been origin-keyed by default since Chrome 106, so the usual cause of a missing
document.modelContextis a legacyOrigin-Agent-Cluster: ?0header kept around to makedocument.domainwritable. - Remember the tools Permissions Policy in iframes. The
toolspolicy defaults toself, so a widget you embed cross-origin registers nothing until the parent page addsallow="tools"to the iframe element. - Validate strictly in code, loosely in schema. Accept raw user input, declare enums and real types rather than opaque numeric IDs, and return a descriptive error string the model can correct against instead of throwing an opaque exception.
- Annotate honestly. Set
readOnlyHintonly on tools that change nothing, and setuntrustedContentHinton any tool returning user-generated or third-party content so the agent can treat the payload as data rather than instructions. - Keep the human path intact. Safari and Firefox have no implementation and Chromium support is still trial-gated, so every tool must map to something a person can still click. WebMCP is an addition to your UI, never a replacement for it.
Do this, not that
Do
- Name tools with a verb that says what happens on call, such as
create_booking, and keep it to 30 characters or fewer. - Feature-detect with
'modelContext' in documentbefore touching the API, so older browsers do not throw on load. - Put
toolparamdescriptionon the named form control itself, not the wrapping element, so it lands on the right schema property. - Keep the tool set small and non-overlapping; two tools that could both do the job make the agent pick wrong more often than a missing tool does.
Do not
- Do not write
navigator.modelContext. It is a deprecated compatibility alias fordocument.modelContextand Chromium logs a warning on first access. - Do not call
provideContext()orclearContext(). Both were dropped from the draft; the current surface isregisterTool()plus anAbortSignal. - Do not publish
/.well-known/webmcp.jsonand expect discovery. There is no manifest in the spec: tools are registered at runtime and the browser aggregates them. - Do not mark a destructive tool
readOnlyHint: trueto get agents to call it without confirming. That is the annotation agents use to decide when to skip asking the user.
How Glippy checks this
Glippy scores WebMCP under Agent Interactivity (category 10). It looks for forms carrying toolname with a non-empty tooldescription and toolparamdescription on their inputs, a live document.modelContext object (falling back to the legacy navigator.modelContext), inline scripts that call registerTool, and a loaded WebMCP polyfill or SDK. It also reports tool description quality, form agent-readiness and whether your element IDs are stable rather than hashed per render, which is what an agent needs to reference a control twice. The category uses a bonus model at a low weight, so a missing implementation is surfaced as a tip and does not drag your score down. The full breakdown is on the WebMCP Checker page.
Check your WebMCP setup
Glippy runs 240+ checks across 16 categories on any page, including Agent Interactivity (category 10). No sign-up required.
Frequently asked questions
WebMCP is Chromium only. Chrome shipped an early preview in Chrome 146 behind chrome://flags/#enable-webmcp-testing, and a Chrome origin trial has been running from Chrome 149 so you can enable it for real users on a registered origin. Microsoft is running a matching Edge origin trial that is scheduled to run to 17 November 2026. Safari and Firefox have no implementation, which is why the polyfill route matters if you want coverage today.
No. The declarative API works from HTML alone: add toolname and tooldescription to a form element and toolparamdescription to its named controls, and the browser derives the JSON Schema from the form itself. By default the agent fills the form and the user submits it, which keeps a person in the loop. You only need script if you want toolautosubmit plus a custom result, which the SubmitEvent.respondWith() method provides.
No. The tool registration surface moved from Navigator to Document, so the canonical entry point is document.modelContext and navigator.modelContext survives only as a deprecated alias that logs a warning. Two earlier methods, provideContext() and clearContext(), were removed from the draft entirely and no shipping implementation exposes them. Current code uses document.modelContext.registerTool(tool, { signal }) and removes the tool by aborting that signal.
No, the two cover different lifetimes. An MCP server is persistent and headless: it exposes your data and actions to an agent anywhere, at any time, with its own authentication. WebMCP tools are ephemeral and bound to an open tab, running inside the session the user is already signed in to and acting on the state they can see. Most teams that do both keep business logic in MCP and use WebMCP to expose the in-page actions that only make sense with the user's current view.
Reviewed against the primary sources on . These standards move quickly, so check the linked specs before you ship.