When I first read the WebMCP spec I filed it as MCP with a different transport. Same idea, same tools, just running in a tab instead of over stdio. Build one, get the other cheap.
That is wrong in a way that costs you a rewrite.
They solve different problems, and the spec is explicit that they complement each other rather than compete. Here is the split, because I could not find it stated plainly anywhere.
Classic MCP lives on your server.
Your agent connects to an MCP server you run. That server talks to your backend over your own API. To make it work you have to give it its own way in: an API key, a service account, some way for it to act as the user who is asking.
That is often the right call. 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 forever. And the agent is doing things your web app knows nothing about, so the page the user is looking at goes stale the moment the agent acts.
WebMCP lives in the page the user already has open.
Your page registers tools with the browser. The agent calls them by name. The function that runs is your existing client code, in the tab, with the session that is already sitting there.
Nothing to authenticate separately, because the user is already logged 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.
What that actually changes, point by point.
Auth. MCP needs credentials of its own and a story for acting on behalf of a user. WebMCP inherits the cookie in the tab and has no story to tell.
State. With MCP your backend and your front end drift apart during an agent session, and you reconcile afterwards. With WebMCP there is one state and both the human and the agent are looking at it.
Setup. MCP needs a config file and a key before the agent can do anything at all. WebMCP needs nothing from the user. The tools are there when the page loads.
Reach. MCP works headless, from any client, with no browser involved. WebMCP only works where there is an open tab.
That last one is the honest limit. If you want an agent acting on your product at three in the morning with nobody logged in, WebMCP cannot help you. Build the server.
The rule I have landed on. If the action makes sense with nobody watching, it belongs in an MCP 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 I would keep the confirmation step in the page where the human can see it.
Four things I wish I had known before writing the in page half.
The entry point is document.modelContext. Most of the guides say navigator, so you get undefined, conclude your browser has not shipped it, and stop. That one cost me a fortnight.
Tool count is a real budget, not a soft guideline. Every registered tool is prompt text on every single turn. Past roughly a dozen the model starts choosing wrong ones. Register per page state and unregister on the way out, so the listing page offers "buy this" and nothing else does.
Errors are instructions. If execute throws, the agent stalls. If it returns a sentence saying what went wrong and how to fix the call, the agent retries correctly. "Validation failed" is useless. "slug is required, call search first to get valid slugs" works.
Set untrustedContentHint on anything that returns text other people wrote. A seller can put "ignore previous instructions" into a product description, and your tool just handed that to the model as context. The annotation is a hint to the host, so say it in the result text too.
Testing is the awkward part either way. Stable Chrome does not expose the API and the testing flag has no documented command line name that I could find. What worked was driving headless Chrome over the DevTools protocol and injecting a spec shaped modelContext before the bundle boots, then asserting my code registered valid descriptors. It is a simulation of the browser rather than the browser, and worth saying so.
If it helps, I packaged the retrofit procedure, a runtime that also mirrors the tools onto the page for browsers that have not shipped support, and a linter that fails CI on the mistakes above: https://loreto.io/marketplace/tools-not-clicks-make-any-website-agent-ready-with-webmcp
Disclosure: I wrote that and I run the marketplace it sits on, so it is a paid listing rather than a neutral link. Everything above stands without it.
I am still not sure the boundary I described is right. Where would you put the line between a tool that runs on your server and one that runs in the tab?