What is WebMCP, and how is it different from MCP?
One runs on your server and needs its own credentials. The other runs in the tab the user already has open. The difference decides which one you should build.
MCP puts tools on a server you run: the agent connects to it, and you have to give it credentials of its own and keep a second surface in step with your product forever. WebMCP puts tools in the page the user already has open: the agent calls a function you registered, your existing client code runs it, and the interface updates because…
An agent trying to buy something on your site today takes a screenshot, guesses which rectangle is the checkout button, clicks, takes another screenshot, and hopes. It is slow, it is brittle, and when it goes wrong your support inbox finds out before you do.
WebMCP is the alternative. Your page registers typed tools; the agent calls them by name with structured arguments; and your own client code runs the action, so the interface stays in sync with whatever just happened. It is a specification from the W3C Web Machine Learning Community Group, developed by Google and Microsoft, and it is in origin trial in Chrome and Edge as of writing.
The most common assumption about it is that it is MCP with a different transport. Same idea, same tools, just running in a tab. Build one, get the other cheap.
That is wrong in a way that costs a rewrite.
Where each one actually lives
A classic MCP server runs on your infrastructure. The agent connects to it, and it talks to your backend over your own API. To make that work you have to give the server its own way in: an API key, a service account, some mechanism for acting as the user who is asking.
That is often the right call, and it is also three problems you now own. You are replicating the user's session somewhere it did not previously exist. You are maintaining a second surface that has to stay in step with your product indefinitely. And the agent is doing things your web application knows nothing about, so the page in front of the user goes stale the moment the agent acts.
WebMCP runs in the page the user already has open. Your page registers tools with the browser. The agent calls them. The function that executes is your existing client code, in the tab, using the session that is already there.
Nothing to authenticate separately, because the person is already signed in. Nothing to keep in step, because it is the same code path your buttons call. And the interface updates, because the tool did what the button does.
The differences that decide it
| Classic MCP | WebMCP | |
|---|---|---|
| Runs on | a server you operate | the user's open tab |
| Auth | credentials of its own, plus a story for acting on behalf of a user | inherits the session already in the browser |
| State | backend and front end drift apart during an agent session | one state, which the human and the agent both see |
| Setup for the user | a config file and a key before anything works | none; the tools are there when the page loads |
| Reach | headless, any client, no browser required | only where a tab is open |
That last row is the honest limit. If you want an agent acting on your product at three in the morning with nobody signed in, WebMCP cannot help you. Build the server.
The rule worth remembering
If the action makes sense with nobody watching, it belongs on the server. If it only makes sense in the context of what the person is currently looking at, it belongs in the page.
A nightly report is a server. Filtering the list currently on screen is the page. Taking a payment is arguably both, and the confirmation step belongs in the page where a human can see it.
What the API looks like
Before anything else: the entry point is document.modelContext, not navigator.modelContext. A great deal of the secondary writing about WebMCP says navigator. The specification and the shipped Chrome implementation both say document. Follow the wrong one and you get undefined, conclude your browser has not shipped support, and never write the integration at all.
const controller = new AbortController();
await document.modelContext.registerTool({
name: 'add-to-cart',
description: 'Add the product currently open to the basket',
inputSchema: {
type: 'object',
properties: {
quantity: { type: 'number', description: 'How many to add' },
},
},
async execute({ quantity }) {
await addToCart(quantity); // the page's own logic
return { content: [{ type: 'text', text: `Added ${quantity}.` }] };
},
}, { signal: controller.signal });
Abort the signal to unregister. That is the whole lifecycle.
Four things that are easy to get wrong
Tool count is a real budget. Every registered tool is prompt text on every single turn, so a catalogue of forty makes the model worse rather than more capable. Past roughly a dozen it starts choosing badly. Register per page state instead: the listing page registers buy this, and navigating away takes it back out.
Errors are instructions, not status codes. If execute throws, the agent stalls. If it returns a message saying what went wrong and how to fix the call, the agent corrects itself and retries. "Validation failed" is useless. "slug is required, call search first to get valid slugs" works.
Set untrustedContentHint on anything returning text other people wrote. Reviews, listings, comments, support tickets. A seller can put "ignore previous instructions" into a product description, and without the flag an agent may read that as an instruction rather than as the text of a listing. Say it in the result text too, because the annotation is a hint to the host while the sentence reaches the model either way.
Mark anything consequential. consequentialHint goes on whatever spends money, publishes, sends or deletes. Hosts use it to decide what needs a human to confirm, and being permissive here is how an agent buys something nobody asked for.
Testing it before browsers ship it
Stable Chrome returns undefined for document.modelContext. There is a testing flag at chrome://flags, but its command line feature name is not documented, and six plausible candidates against both --enable-features and --enable-blink-features produced nothing. So you cannot verify a WebMCP integration by opening a browser and looking at it.
What works is driving headless Chrome over the DevTools protocol and installing a specification-shaped document.modelContext before your bundle boots, using Page.addScriptToEvaluateOnNewDocument. Then assert that your code registered valid descriptors with a live abort signal. It is a simulation of the browser rather than the browser itself, and worth saying so out loud, but it does verify the half you actually control.
The other half is mirroring every tool onto a plain object on window. Firefox and Safari have not shipped, Chrome and Edge are both in trial, so a small registry with list() and call() means browser automation drivers and extension agents can use your tools today, and your test harness has something to drive.
Where to start
Inventory the outcomes a user can achieve on your site, not the buttons they can press. Group them by the page state that should own them. Register the small always-on set, and let each page add its own on mount. Then check that a tool nobody can call has not shipped looking finished.
loreto.io runs this in production: nine tools across discovery, account, listing and selling, with the panel in the corner showing what an agent can see on any given page.
Frequently asked questions
Does WebMCP replace my MCP server?
Which browsers support it today?
Is exposing tools to agents a security risk?
Turn this guide into a skill your agent can run
Stop re-explaining the same workflow. Loreto packages it as a Claude Code skill from any source.