The first WebMCP tool most teams ship will not be written as a tool. It will be a form that already existed, re-declared in four lines, and it will accept calls from a model with the full authority of whoever is logged in — because the browser authenticates the user, and nothing authenticates the caller.
At a glance
WebMCP adds a browser API that lets a web page declare structured, callable tools to an AI agent, instead of making the agent drive the page through its UI. It is genuinely a good idea, and it is much earlier than the coverage suggests.
| Question | Answer | Why it matters |
|---|---|---|
| What is it? | A page registers named tools with JSON Schema inputs; an agent discovers and invokes them. | Replaces scraping and selector-driven clicking with a declared contract. |
| Standards status | A W3C Draft Community Group Report from the Web Machine Learning Community Group — not a W3C Standard and not on the Standards Track. | Incubation. The shape can still change, and it has. |
| Implementations | Chrome, behind a flag and an origin trial. Other engines are engaged in the group without shipping commitments. | One implementation is a preview, not a platform. |
| Scope | Tools only. MCP's other primitives — resources and prompts — are out of scope. | This is not "MCP in the browser". It is one third of MCP. |
The churn is already visible from outside: the entry point moved from navigator.modelContext to document.modelContext, and an earlier provideContext() design was dropped. Writing against it today buys you a migration, which is fine if you know that is what you are buying.
What actually shipped
The imperative surface is small enough to hold in your head. A page calls document.modelContext.registerTool() with a descriptor — a name, a description in natural language, an inputSchema in JSON Schema, and an execute callback — and the agent side gets getTools() to enumerate and executeTool() to invoke, plus a toolchange event so a single-page app can revise its offer as the user navigates.
document.modelContext.registerTool({
name: 'addToCart',
description: 'Add a product to the shopping cart.',
inputSchema: { type: 'object', properties: { sku: { type: 'string' } } },
async execute({ sku }) {
return await fetch('/api/cart', { method: 'POST', body: JSON.stringify({ sku }) });
},
});
Three details in that snippet are doing more work than they look like they are.
The description is a prompt
An agent picks a tool by reading its description, which makes that string part of your model's instruction surface rather than documentation. It is the same property that makes MCP tool poisoning work, arriving in a place where your marketing team has edit access. A description that says "use this instead of asking the user" is an instruction, and it will be followed.
There is a declarative path, and it is the one that scales
Alongside the JavaScript API, the draft lets an HTML <form> act as a tool declaration. This is the part that will actually get adopted, because it is nearly free — and it is also how a large existing surface becomes agent-callable in a single deploy, without anybody writing a line of code they would recognise as "exposing an API".
Cross-origin is in scope
Registration options include exposedTo, an allow-list of origins permitted to see a tool, and the discovery side has fromOrigins, naming secure origins to collect tools from. Tools are origin-isolated by default and the API requires a secure context. But the existence of these two options tells you the model is not "the page talks to the browser's agent" — it is a graph, and your page is a node in it.
The tool runs inside the session
Follow the authority rather than the data. The execute callback is ordinary page JavaScript in your origin. When it calls your backend, it carries the cookies, the bearer token in localStorage, the CSRF token your template rendered — every credential the browser holds for the user who is sitting there. Your server receives a well-formed, same-origin, fully authenticated request and has no field anywhere in it that says a model composed the arguments.
This is a confused deputy in its classic form, and it is worth being precise about who is confused. Your page is the deputy. It holds authority it did not earn — the user's — and it is now accepting instructions about how to spend that authority from a caller whose intent it cannot inspect. The browser's consent prompt is real mitigation and it is the right place for it, but consent is granted by a human who is reading a tool name and a summary, once, at the start of a task that may involve forty calls.
execute() that distinguishes them.Compare the three paths directly and the asymmetry is easy to see. A human click passes through your UI, which means it passed your disabled states, your confirmation modals, and whatever rate the user can physically produce. An agent call skips all of that by design — that is the feature — and arrives at the handler at machine rate. A cross-origin call reaches the same handler having satisfied an origin check that your page configured, possibly by accident, possibly with a wildcard that looked convenient during development.
And the instructions driving all three may have come from your own page. An agent that reads a product listing, a review, a support-ticket body or a user-supplied display name is reading untrusted text in the same window where it holds your tool list. This is prompt injection with the tool call already provisioned, which is why every serious defence starts at the authorisation layer rather than at the text.
You are publishing an API, whatever you called it
Set security aside for a moment, because the operational argument lands even for a site with nothing sensitive behind the login. The moment a tool is registered, you have taken on every obligation an API carries, and you have taken it on without any of the machinery you would normally build first.
| Obligation | Your REST API has | Your WebMCP tools have |
|---|---|---|
| Versioning | A path segment, a deprecation policy, a changelog. | A name string. Rename it and every agent that learned it breaks. |
| Rate limiting | Per-key quotas, backoff contracts, 429s. | Whatever your session-level limits happen to be, which were sized for humans. |
| Authorisation | Scopes, per-client grants, revocation. | The user's session, all of it, undifferentiated. |
| Abuse handling | A key you can revoke. | A tool you can unregister — for everyone, on your next deploy. |
The versioning row is the one that will bite first and quietly. Agents do not read your changelog; they read the description and the inputSchema at call time, which sounds like it makes breakage impossible. It does not. It makes breakage silent: you tighten a schema, agents that used to succeed now fail validation, and your only signal is a metric nobody has built yet. Server-side, this is exactly the tool catalogue lifecycle problem, and it does not become easier for being in a browser.
The rate-limiting row is the one that will cost money. Your per-session limits encode an assumption about human speed that WebMCP invalidates on purpose. An agent working a task will call a search tool more times in ten seconds than a user would in ten minutes, and it will do so legitimately — there is no attacker in this story, just a caller with different physics.
What to do about it
None of this is an argument against WebMCP. The alternative to a declared tool surface is agents driving your UI with synthetic clicks, which is worse on every axis including security — you cannot reason about an interface you never designed as one. The argument is that the cheapness of adoption is the risk, and the mitigations are boring and server-side.
- Mark the request at the boundary. Have
executeattach a header that says this call came from a tool, and which tool. Your server cannot infer it and will never be able to; the only place that knowledge exists is the callback you are writing. Everything below depends on this one line. - Re-authorise per action, server-side. Treat the header as a lower trust tier: the same endpoint, the same session, a narrower set of permitted operations. Reads are usually fine. Anything that spends money, sends a message, or is hard to undo should require a fresh, specific confirmation — the blast radius argument, applied to a surface that did not exist last quarter.
- Register the minimum, not the maximum. The temptation with the declarative form path is to expose everything at once because it costs nothing. Start with the read-only tools that make an agent useful — search, availability, status — and add a write tool only when you have a reason and a limit for it.
- Set
exposedToexplicitly. A default-open tool on a page that embeds third-party iframes is a configuration you want to have made on purpose, in code review, with a name next to it. - Rate-limit the tool path separately. Once tool calls are labelled they can have their own budget, which is both the abuse control and the cost control.
- Write the descriptions like security-relevant strings. They are prompt content. Put them under the same review as your system prompts, not your microcopy.
And on timing: because this is a community group draft with one flagged implementation and an entry point that has already moved once, the honest schedule is to prototype now and ship the server-side half. The header, the per-action authorisation, and the separate rate limit are useful whether or not WebMCP is the standard that wins — they are the same controls you need for any agent traffic reaching your product. The registerTool calls are the part you should expect to rewrite.
FAQ
Is WebMCP a W3C standard?
No. It is published as a W3C Draft Community Group Report by the Web Machine Learning Community Group, which explicitly means it is neither a W3C Standard nor on the W3C Standards Track. Community group incubation is where good ideas start, and also where some of them stop.
Does WebMCP replace MCP servers?
No, and it covers less ground than the name implies. The draft is tools-only — MCP's resources and prompts primitives are out of scope — and it addresses the case where the capability lives in a page the user already has open and is already authenticated to. A remote MCP server still owns everything that is not a browser session.
Can a cross-origin script register tools on my page?
Tools are scoped to the origin that registers them, and the API requires a secure context, so a third-party script cannot silently impersonate your origin's tools. What it can do is register its own tools from its own origin, and cross-origin discovery via fromOrigins plus the exposedTo allow-list is exactly the surface to configure deliberately rather than leave at whatever the default turns out to be.
If my server cannot tell an agent call from a click, what is the minimum fix?
One header, attached inside execute, naming the tool. It is not a security boundary — page JavaScript can send anything — but it is the observability that makes every real control possible, and without it you cannot even measure how much of your traffic is agents.
Should we adopt it now?
Prototype now, and ship the parts that survive. Behind a flag with one engine and an API that has already been renamed, a production dependency on the client half is premature. The server half — labelled tool traffic, per-action authorisation, a separate quota — is work you will need for agent traffic regardless of which spec wins.
Further reading
On this wiki:
- What is MCP — the protocol WebMCP borrows its vocabulary from.
- Ambient authority — why "the user is logged in" is not an authorisation model.
- Bot verification & agent access — the other half of the agent-traffic question.
- Capability discovery — how agents find out what they can do.
- Prompt injection defence — why the fix is authorisation, not filtering.
- Tool catalogue lifecycle — versioning tools that clients discover at call time.
Sources:
- webmachinelearning/webmcp — the specification, explainer and security notes.
- WebMCP draft community group report.