WebMCP is a way for a web page to hand an agent a function instead of a button. A site registers named tools in JavaScript or in HTML, each with a description and a JSON Schema for its inputs, and an agent running inside the browser can list those tools and call them. Instead of the agent looking at a screenshot and guessing which element submits the form, it calls lookup_ticket with a reference and gets a string back.
The name comes from Model Context Protocol, and the resemblance is real, but the shape is different. There is no server, no transport, no connection to maintain. The tools live in the tab, they run in the user’s session with the user’s cookies, and they disappear when the page unloads. Microsoft and Google co-edit the spec at the W3C. Chrome has it behind an origin trial that started in Chrome 149.
That is the whole idea, and it is a good one. Most of what has been written about it, though, no longer runs. The entry point moved from navigator.modelContext to document.modelContext while the explainers were being written, and two other pieces of the API were removed before that. So the first thing to check in any WebMCP article is whether the code says document. or navigator.. That single character tells you whether the author opened DevTools this quarter or summarised a post from February.
Agents already drive browsers. They have done it for a while, and the mechanism is unglamorous: take a screenshot, serialise the DOM or the accessibility tree, ask a model what to click, synthesise the click, screenshot again, repeat. Chrome’s docs call this actuation, and the framing is fair. The agent is pretending to be a pair of hands.
This breaks in ways that are boring and constant rather than dramatic. Selectors move when marketing ships a new hero section. A custom date picker built on divs presents no affordance the model recognises. An async state transition means the screenshot the model reasoned about is two states behind the page. Every one of those steps costs a full round trip through a large model, which is where the latency and the token bill come from: a five-field form is not one inference, it is five or six, each one carrying a fresh image or a fresh DOM dump. And nothing in that loop tells the agent whether the button it is about to press charges a credit card.
That last one is the part I care about most. Screen scraping gives an agent no schema and no notion of consequence. It cannot distinguish a filter from a purchase except by reading the label, which is exactly the kind of inference that fails silently at scale.
WebMCP’s answer is to let the page declare the actions instead. The site registers named tools with descriptions and JSON Schema inputs, the browser exposes them to whatever agent it hosts, and the agent calls a function rather than guessing at pixels. Chrome frames the ownership question well: with an MCP app your interface is a guest inside the agent’s UI, whereas here the agent visits your platform and your interface stays yours.
Which brings us to the money.
Google needs agents to work, because that is where the product bets are, and Google needs humans to keep looking at pages, because that is where the revenue is. WebMCP resolves that tension by design. Tools execute visibly in a real tab, in the user’s session, with the page rendered. The Chrome documentation lists headless browsing as a limitation, saying the API is primarily designed for local browser workflows with a human in the loop.
Read that carefully, because it is softer than the non-goal language the standard is often described as having. It does not say headless is forbidden. It says WebMCP is not designed for it. My reading is that the human-in-the-loop framing is doing two jobs at once, and both are honest on their own terms. It genuinely is a safety story: a tool call you can watch is a tool call you can cancel. It is also a business story, because a browser that renders the page is a browser that renders the ad, and an agent that completes a purchase invisibly on a server is an agent that has removed the merchant’s brand from the transaction. Google is not hiding either motive. I just think practitioners should name both instead of quoting the safety half.
If you own a site today, WebMCP is one of several options and not obviously the first. A server-side MCP server works when no tab is open, handles background jobs, and is a mature thing you can deploy this afternoon. A plain HTTP API you already have is still the highest-leverage integration surface for anyone who reads documentation. A2A, now hosted by the Linux Foundation with more than 150 supporting organisations as of April 2026, addresses a different problem entirely: your agent talking to someone else’s agent, not a model touching your UI. Structured data in your markup remains the cheapest thing you can ship and helps discovery rather than action. Doing nothing is a real option with real costs, and for most internal business applications it is still the right call this quarter.
WebMCP earns its place in exactly one situation: a human is on your page, an agent is helping them, and you want that help to be precise.
A tool is a name, a description, a JSON Schema for its inputs, and a function. The browser holds the registry per Document and hands it to whatever agent it is hosting.
The imperative API is the specified half:
// document.modelContext, not navigator.modelContext.
// The getter moved to Document in the May 2026 draft revision;
// Chrome deprecated the navigator surface in Chrome 150.
await document.modelContext.registerTool({
name: 'lookup_ticket',
description: 'Look up a support ticket by its reference and return status, owner and last update.',
inputSchema: {
type: 'object',
properties: {
reference: { type: 'string', description: 'Ticket reference, format INC-000000' },
},
required: ['reference'],
},
annotations: {
readOnlyHint: true,
untrustedContentHint: true, // customer-written text goes back to the model
},
execute: async ({ reference }, { signal }) => {
const res = await fetch(`/api/tickets/${encodeURIComponent(reference)}`, { signal });
if (!res.ok) return `No ticket found for ${reference}.`;
const t = await res.json();
return `${t.reference}: ${t.status}, owner ${t.owner}, updated ${t.updatedAt}.`;
},
});
Names are constrained: one to 128 characters, and only ASCII alphanumerics plus underscore, hyphen and full stop. Registering a name that already exists does not throw. It returns a rejected promise carrying an InvalidStateError, which is worth knowing if you were planning to wrap the call in a try/catch around synchronous code. So do empty names, empty descriptions, and input schemas that fail JSON serialisation.
The lifecycle is tied to an AbortSignal, which replaced the old unregisterTool(name) in the April 2026 draft:
const controller = new AbortController();
await document.modelContext.registerTool(cartTool, { signal: controller.signal });
// Later, when the cart empties and checkout no longer makes sense:
controller.abort();
This is the part that makes framework integration tolerable. Bind the controller to a component’s lifetime and the tool set follows the UI. Chrome’s docs note that as of Chrome 153 unregistration no longer kills in-flight executions, which was a real problem for anyone unmounting components mid-call.
The declarative API turns a form into a tool with three attributes:
<form toolname="createSupportRequest"
tooldescription="Submit a request for support."
action="/submit">
<label for="firstName">First name</label>
<input type="text" name="firstName" id="firstName">
<select name="team" required
toolparamdescription="Determines what team this request is routed to.">
<option value="Returns">Return my purchase.</option>
<option value="Logistics">Check where my package is.</option>
<option value="WebSupport">Get help on the website.</option>
</select>
<button type="submit">Submit</button>
</form>
The browser synthesises the JSON Schema from the form: field names become properties, required becomes required, <option> values become an enum with titles. Without toolparamdescription it falls back to the associated <label>, then to aria-description. Removing either toolname or tooldescription unregisters the tool. By default the agent fills the form and the human presses Submit. Adding toolautosubmit lets the agent submit, and SubmitEvent gains an agentInvoked boolean plus a respondWith(Promise) method so you can return a result to the model instead of navigating.
Two annotations exist and only two. readOnlyHint tells the agent nothing changes, which is how it decides whether to ask the user first. untrustedContentHint marks output that contains data you do not control, so the agent can wrap or encode it before it reaches the model. The classic MCP hints for destructiveness and idempotency are not in this spec.
What WebMCP is not. It is not MCP over the network, and the spec says browsers are free to expose tools to their agent through MCP, proprietary function calling, or anything else. It is not headless automation. It is not a replacement for your API, because the tools vanish when the tab closes. And the declarative half is not, at time of writing, specified at all: section 4.3 of the W3C draft is a TODO with the schema synthesis algorithm unwritten.
Here is how the options actually compare, as of today.
| who calls it | auth model | where the code runs | maturity, Sept 2026 | |
|---|---|---|---|---|
| screen scraping | any agent, uninvited | whatever the browser session has | agent’s harness | universal, works everywhere, unreliable |
| browser automation (Playwright, CDP) | your own scripts | credentials you inject | your CI or server | mature, well understood |
| WebMCP | in-browser agent, or an in-page agent via getTools() | the user’s live session cookies | the visitor’s tab | origin trial, Chromium plus one desktop client |
| server-side MCP | any MCP client, anywhere | OAuth or tokens you issue | your servers | production, wide client support |
| A2A | another agent, not a model | agent identity, TLS and JWT | your servers | Linux Foundation project, production deployments |

Local setup is one flag. Open chrome://flags/#enable-webmcp-testing, enable it, relaunch. For real traffic you need an origin trial token served as a header or meta tag. Both APIs are gated by the tools Permissions Policy, which defaults to self, so cross-origin iframes need allow="tools" on the iframe element. WebMCP is also only available in origin-isolated documents: if you send Origin-Agent-Cluster: ?0 or otherwise enable document.domain, the API disappears, which is the kind of thing that will cost someone an afternoon on a legacy portal.
Feature-detect before you register, because the shape has moved before and may again:
if (typeof document.modelContext?.registerTool === 'function') {
await registerAllTools();
}
To see what a page has registered, call getTools() from the page itself. It returns tools alphabetically, same-origin by default:
const tools = await document.modelContext.getTools();
console.table(tools.map(t => ({
name: t.name,
readOnly: t.annotations?.readOnlyHint,
origin: t.origin,
})));
You can also execute one directly, which is how you test without waiting for an agent:
const [ticketTool] = await document.modelContext.getTools();
const result = await document.modelContext.executeTool(ticketTool, { reference: 'INC-004417' });
One warning here. The spec IDL declares executeTool(RegisteredTool tool, optional object inputObject = {}, optional options = {}) and its own example passes an object. Chrome’s imperative API doc shows the same call taking a JSON string. Those two pages were last touched six days apart and they disagree. If your call rejects for no obvious reason, try the other form before debugging your schema.
For agent-shaped testing without wiring up a model, Google publishes a Model Context Tool Inspector extension that lists registered tools, calls them with hand-written JSON, and will run natural-language prompts through gemini-3-flash-preview if you supply a key. Chrome also gates a testing surface behind --enable-features=WebMCPTesting,DevToolsWebMCPSupport, which third-party test harnesses use to list and invoke tools without an agent. That surface is not documented on developer.chrome.com, so treat its shape as unstable and do not build CI on it without pinning a Chrome version.
Instrument three things from day one. Count tool invocations separately from UI events, because otherwise agent traffic silently pollutes your funnel analytics and you will not know which conversions were assisted. Log the arguments the agent actually sent against the schema you published, because schema drift shows up as a rise in rejected calls long before anyone reports a bug. Track per-tool latency, since a tool that takes four seconds will get abandoned by the agent mid-plan and you will see it as an unexplained drop-off rather than a timeout.
Who should prototype now. Anyone whose users already sit in a browser tab with an agent open, running multi-field workflows: booking, structured intake, dashboards with filters that are hard to describe in prose. Anyone on Shopify, where the tools are already registered on every Liquid storefront whether you asked or not, and the useful work is auditing what the platform decided to expose on your behalf. Anyone whose competitor is doing it, in commerce specifically.
Who should wait. Regulated products where an authenticated tool call is a control you have to document to an auditor. Anything moving money or changing account state, at least until the consent primitive stops moving. Teams with no agent traffic in their logs, which is most teams.
The signal to watch is not the spec reaching Candidate Recommendation, and it is not Chrome shipping to stable. It is a second independent agent vendor implementing the client side. One vendor is a product decision. Two is an ecosystem, and only then does registering tools stop being a bet on a single company’s roadmap.
For most of 2026 the honest description of WebMCP was that you could register tools and nothing would call them. Google announced at I/O that Gemini in Chrome would support the APIs soon. As far as I can tell it still has not, more than three months later.
The thing that changed is that someone else went first. On 25 August 2026, OpenAI shipped Site tools in the ChatGPT desktop app’s built-in browser. ChatGPT Work and Codex discover and call page-provided tools. The constraints are worth reading before you plan around it: GPT-5.6 Sol or Terra only, disabled on Luna, unavailable in Enterprise and Edu workspaces, no support for the declarative API, and no discovery of tools registered inside iframes of any kind. That last pair matters. If you built your integration the declarative way because it was three HTML attributes, the first shipping consumer client cannot see it.
Shopify switched tools on across every Liquid storefront, and Cloudflare shipped an edge bridge that injects a same-origin script into HTML responses so a site gets tools without touching its origin code. Both are genuine deployments. Both also mean the agent-facing version of a site is being authored by a platform rather than by the site owner, and Shopify’s published docs do not say whether a merchant can disable an individual tool or rewrite its description. If your brand voice matters to you in the human UI, it is odd to accept a vendor’s defaults in the agent UI.
On adoption numbers, be careful with anything you read. The last clean measurement I could find is freeCodeCamp’s pull of Cloudflare Radar AI Insights for the week of 17 to 23 May 2026, covering 111,076 scanned domains from the top 200,000, which put WebMCP in the near-zero tier. That was before Shopify. Any chart published from September onwards is largely measuring one platform’s product decision.
Then there is the churn. provideContext({tools}) was removed in March, unregisterTool() in April, and the entry point moved off navigator in May. Three breaking changes inside one calendar year, in a spec whose status line still reads Draft Community Group Report and which is neither a W3C Standard nor on the Standards Track. Mozilla’s position is neutral and the issue is closed. WebKit’s is open with eight concern labels attached, covering API design, duplication, internationalisation, portability, privacy, security, use cases and venue. I have seen that state reported as Safari opposing the proposal. It is not a published position, and the distinction matters, but eight concerns from the vendor with a quarter of the mobile market is not a rounding error either.
The security surface is where I would push back hardest on the optimistic framing. Chrome’s own agent security guidance names two vectors: malicious manifests, meaning instructions hidden in a tool’s name or description, and contaminated outputs, meaning third-party data returned through an otherwise honest tool. The mitigation on the agent side is spotlighting, and Chrome’s comparison is refreshingly unglamorous: delimiters are cheap and break if an attacker guesses your closing tag, base64 encoding resists that and costs roughly 33% more tokens on every tool output. On the site side you get one boolean, untrustedContentHint, and a set of character budgets, 500 for a description and 1.5K per output, that exist to keep you under agent guardrails.
There is now WebMCP-specific research rather than generic MCP research. Lee and co-authors describe Mid-Session Tool Injection: a third-party script sharing your document mutates the tool surface during a live session. They split it into hijacking, which abuses AbortSignal and registration races to change which tools the agent can see, and framing, which leaves functionality alone and edits the metadata, including readOnlyHint, to change how the model interprets a tool. Hijacking reached 100% data exfiltration in their scenarios but disrupted the task enough to be noticeable. Framing preserved up to 85% task completion while still succeeding, which is the worse result. Their finding that protocol-level attacks did not weaken across model generations, while description-level ones did, is the part I keep coming back to: you cannot buy your way out of this with a better model.
And the primitive that would help most is missing. Chrome’s tool security page, updated 1 July, tells you the draft includes requestUserInteraction() for asking the user mid-execution, and links to a spec anchor that no longer exists. ModelContextClient and requestUserInteraction were removed by late June. Consent is still an open discussion. So the confirmation semantics that were supposed to distinguish WebMCP from screen scraping currently live in the agent vendor’s product decisions, not in the platform. OpenAI reviews each invocation and applies its normal rules to purchases and deletions, which is good of them and is not the same as a web standard.
So: prototype, do not commit. Register read-only tools on one surface where a broken call costs you nothing, put untrustedContentHint on anything returning user-generated text, keep the human clicking Submit, and instrument the calls so that when a second agent vendor ships you already have a baseline. The engineering cost of that is small and the option it buys is real. What I would not do is refactor application logic to expose a clean tool layer, or expose anything that moves money, on the strength of one client that shipped nine days ago and one spec section that is still marked TODO.