WebMCP Is Not MCP: I Tested Chrome's New Agent-Tool API With Google's Own Polyfill
WebMCP isn't MCP. It's a separate, browser-native standard for exposing page-level tools to AI agents, now backed by a Chrome origin trial and this week's OpenAI-run hackathon. I tested the real API with Google's own polyfill against a Kiwi-style form: the declarative form API, the imperative registerTool API, real generated JSON Schema, real tool calls, and why WebMCP's trust model differs sharply from a backend MCP server.

Trong bài này
WebMCP is not MCP. It's not a new version, a rebrand, or a browser-flavored wrapper around the Model Context Protocol I've written about on this blog before — it's a completely separate proposal, incubated in a different standards body, solving a different problem. And as of this week it stopped being theoretical: OpenAI, Google Chrome, Cloudflare, Shopify, Vercel, Render, and Netlify are jointly running the WebMCP Challenge, a hackathon open from August 25 to September 3, 2026, specifically to get developers building on it. So I stopped reading explainers and tried to actually run the API myself, on a page modeled after my own project. Here's what I found, with real commands and real output, not a rewrite of someone else's launch post.
The name collision is doing real damage
If you search "WebMCP" right now, you'll get a wall of explainer posts, most of them written before the API was testable, and a good number of them treating it as if it's just "MCP, but for the browser." It isn't. I've written before about the OAuth and DPoP roadmap for the actual Model Context Protocol — the one my own MCP server uses, the one that filed the draft for this very post. That protocol connects an agent to a backend: a server process, usually behind OAuth, that can hit a database, call an API, write a file. It runs independently of any browser tab, and it keeps running whether or not a human is looking at anything.
WebMCP solves a narrower, different problem: an agent that's already driving a browser — clicking, typing, scrolling on behalf of a person — currently has to do that by guessing at DOM structure, which is slow and breaks the moment a class name changes. WebMCP lets the page itself declare, in plain JavaScript or in annotated HTML, a small set of named functions with JSON Schema inputs that an agent can call directly instead of pretending to be a mouse. It's incubated in the W3C Web Machine Learning Community Group, not the MCP steering committee, and it only exists inside a loaded page in a browser tab — there's no persistent connection, no server process, and (so far) no browser other than Chrome shipping it, even experimentally.
Confusing the two matters because the trust model is completely different, which I'll get to after the code.
What Chrome actually ships today
The honest first step was checking whether any of this is real yet, on infrastructure I could actually touch. This sandbox has Playwright with a stock Chromium build, so I pointed it at a blank page and asked for the API directly:
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch({
executablePath: '/opt/pw-browsers/chromium-1194/chrome-linux/chrome',
});
const page = await browser.newPage();
await page.goto('about:blank');
const result = await page.evaluate(() => ({
hasModelContext: typeof document.modelContext,
hasNavigatorModelContext: typeof navigator.modelContext,
}));
console.log(JSON.stringify(result, null, 2));
await browser.close();
})();
Chromium 141.0.7390.37
{
"hasModelContext": "undefined",
"hasNavigatorModelContext": "undefined"
}
Nothing. I tried again with every experimental-platform flag I could think of:
--enable-experimental-web-platform-features
--enable-features=WebMCP,WebMachineLearningModelContext
Same result — still "undefined". That's expected, not a bug on my end: WebMCP ships behind an origin trial in Chrome 149, gated further by the chrome://flags/#enable-webmcp-testing flag, and stock open-source Chromium builds (the kind Playwright ships, version 141 as of this test) don't carry Google's proprietary trial tokens or the feature at all. If you've read a WebMCP tutorial that shows document.modelContext working in "any recent Chrome," either they were on Chrome 149+ Canary/Dev with the flag flipped, or they were quoting docs without running anything — which, going by how much of the WebMCP content online reads like the sequel of itself, feels like the more likely explanation.
Testing it anyway, with Google's own polyfill
Waiting for a stable channel release wasn't an option if I wanted to write about the actual API surface rather than paraphrase documentation, so I went to the source. Google publishes the reference implementation itself, and it includes a polyfill meant for exactly this situation — testing WebMCP semantics in browsers that don't have the native API yet:
curl -sL -o webmcp-polyfill.js \
"https://raw.githubusercontent.com/GoogleChromeLabs/webmcp-tools/main/demos/shared/webmcp-polyfill.js"
It's a ~570-line script from the GoogleChromeLabs/webmcp-tools repo that checks if (window.document.modelContext) return; up front — meaning it gets out of the way the moment a real Chrome 149 ships the native API — and otherwise implements registerTool, getTools, executeTool, and a toolchange event on document.modelContext in plain JS. Loading it into a page with Playwright's addScriptTag gave me a working, spec-shaped document.modelContext in ordinary Chromium 141, no flags required.
The declarative API: annotate a form, get a tool for free
WebMCP's simplest surface is meant for exactly the kind of form every CRUD app already has. I built a small standalone page modeled on the "add a debt entry" flow from Kiwi, the debt-splitting app I wrote about here:
<form toolname="addDebtEntry"
tooldescription="Ghi nhận một khoản nợ mới giữa hai người trong nhóm Kiwi."
action="/api/debts" method="post">
<input type="text" name="debtor" toolparamdescription="Tên người nợ tiền" required>
<input type="text" name="creditor" toolparamdescription="Tên người cho nợ" required>
<input type="number" name="amount" toolparamdescription="Số tiền nợ, đơn vị VND" required>
<select name="category" toolparamdescription="Loại chi tiêu">
<option value="food">Ăn uống</option>
<option value="travel">Di chuyển</option>
<option value="rent">Tiền nhà</option>
</select>
<button type="submit">Ghi nợ</button>
</form>
No JavaScript at all — toolname, tooldescription, and toolparamdescription are just HTML attributes. The browser (or, here, the polyfill standing in for it) is supposed to walk the form and build a JSON Schema tool definition from it automatically. I called getTools() after loading the page and got exactly that back, unedited:
{
"name": "addDebtEntry",
"description": "Ghi nhận một khoản nợ mới giữa hai người trong nhóm Kiwi.",
"inputSchema": {
"type": "object",
"properties": {
"debtor": { "type": "string", "description": "Tên người nợ tiền" },
"creditor": { "type": "string", "description": "Tên người cho nợ" },
"amount": { "type": "number", "description": "Số tiền nợ, đơn vị VND" },
"category": {
"type": "string",
"description": "Loại chi tiêu",
"enum": ["food", "travel", "rent"]
}
},
"required": ["debtor", "creditor", "amount"]
}
}
The <select> became a proper enum, the required attribute mapped straight to the schema's required array, and the <input type="number"> was correctly typed as "number" rather than left as a string. That's the whole appeal of the declarative form: if your Next.js or Nuxt app already renders semantic forms, you get an agent-callable tool by adding three attributes and shipping zero extra JavaScript.
The imperative API: a real function, a real return value
The declarative path only covers form submission. For anything read-only or more complex — searching, filtering, computing — WebMCP has an imperative API that looks almost exactly like an MCP tool definition, minus the transport:
await document.modelContext.registerTool({
name: 'searchExpenseHistory',
description: 'Tìm lịch sử chi tiêu của một người trong nhóm Kiwi theo tên.',
inputSchema: {
type: 'object',
properties: { person: { type: 'string' } },
required: ['person'],
},
execute: async ({ person }) => {
return `Tìm thấy 3 khoản nợ liên quan đến ${person}.`;
},
});
registerTool takes the same three ingredients you'd hand to any MCP tool — name, description, inputSchema — plus an execute function that runs in the page's own JS context, with access to whatever state, cookies, or in-memory data that page already has. I fetched it back through getTools() to confirm the shape round-trips correctly, then actually invoked it the way an agent would:
const tools = await document.modelContext.getTools();
const tool = tools.find(t => t.name === 'searchExpenseHistory');
const result = await document.modelContext.executeTool(tool, { person: 'Loc' });
console.log(JSON.stringify(result));
"Tìm thấy 3 khoản nợ liên quan đến Loc."
One gotcha worth flagging if you go build this yourself: executeTool takes the tool object returned by getTools(), not a bare string name. I initially called executeTool('searchExpenseHistory', { person: 'Loc' }) — reasonable, since that mirrors how you'd call most RPC-style APIs — and got Error: Tool undefined not found for my trouble. The polyfill (and, per the docs, the native API it mirrors) expects the whole descriptor object, not just its name. Small thing, but it's exactly the kind of detail you only catch by running the code instead of skimming a blog post about it.
Why the trust model is the part that matters
A backend MCP server sits behind whatever auth you put in front of it — mine uses OAuth, which is the entire subject of my last post on this. You control what capabilities exist and who can reach them, independent of what any particular webpage says about itself.
A WebMCP tool has no such boundary by construction. The tool's name, description, and schema are just page content — the same trust tier as any other text on that page. If a page can render an infobox, it can register a tool that claims to do one thing and does another, and the agent has no independent way to verify the claim beyond whatever sandboxing the browser itself provides around execute. That's not a defect in the proposal — the spec is explicit that WebMCP tools operate inside the page's existing security boundary, same as any other script that page already runs — but it does mean "expose a tool to agents" and "expose an API to the internet" are not the same risk category, even though the JSON Schema on both looks identical. Anyone treating a WebMCP tool description as more trustworthy than the page's own copy, just because it's shaped like a function signature, is making the same mistake as trusting an unauthenticated robots.txt directive to enforce access control — a mistake I've already written about in the context of my own site's agent-discovery files.
What this means if you build on Next.js or React today
Nothing here is production-ready, and none of the sources I checked claim otherwise: the spec lives in W3C incubation, Chrome is the only browser running an origin trial, and even that requires an explicit flag outside the trial itself. But three things make it worth prototyping now rather than waiting:
First, the shape of both APIs maps directly onto patterns a Next.js or Nuxt app already has. If a page's forms already have semantic name attributes and a required list, the declarative API is close to free. If a component already exposes a search or filter function, wrapping it in registerTool is a few lines.
Second, feature detection costs nothing: if (document.modelContext) { ... } degrades to a no-op in every browser that hasn't shipped this, including Chromium 141 with no polyfill loaded, so there's no real downside to shipping the imperative registration behind that check today — as long as you don't also ship the polyfill to real users. The polyfill is explicitly a dev/test tool; its executeTool implementation includes a postMessage-based remote-execution path meant for demos and cross-frame testing, not a hardened one meant for production traffic from browsers that never asked for this feature.
Third, the roster backing the WebMCP Challenge — OpenAI on the agent side, Chrome on the browser side, Shopify, Vercel, Netlify, Render, and Cloudflare on the hosting and platform side — is a wider coalition than most experimental web APIs get this early. That doesn't guarantee it ships, but it's a stronger signal than a single-vendor pitch deck.
If you want a service built around exactly this kind of agent-facing product work — deciding where an agent should get a proper backend tool versus a page-level one, and building the guardrails either way — that's the sort of thing I do in AI product development work day to day, separate from anything Easy AI–specific.
What I'd actually check before shipping this
If you're going to spend your ten days on the WebMCP Challenge, or just want to try this on a real project, here's the checklist I'd run through myself, based on what actually broke while I was testing:
Confirm which channel you're testing in — chrome://version will tell you immediately whether you're anywhere near 149, and the flag is chrome://flags/#enable-webmcp-testing, not a generic "experimental web platform features" toggle (I tried that first; it does nothing for this specific feature).
Treat the polyfill as a local verification tool, not a dependency — load it in tests and demos, strip it before deploying, and gate your own registerTool calls behind if (document.modelContext) so nothing breaks for the 100% of real visitors who won't have this natively for a while yet.
Pass the tool descriptor object back into executeTool, not a name string — the signature is executeTool(tool, args, options) where tool is what getTools() handed you, and the error message when you get it wrong (Tool undefined not found) won't tell you why.
Decide, per tool, whether it actually belongs on the page at all. A read-only lookup like searchExpenseHistory is a reasonable WebMCP candidate because it only needs data the page already has loaded. Anything that touches money, writes across sessions, or needs to run when nobody has that tab open — the addDebtEntry example above included — is a better fit for a real backend endpoint that a proper MCP server, or just a normal authenticated API, calls into. WebMCP is for actions inside the page a person is already using, not a replacement for the backend contract that action ultimately has to satisfy.
The API works exactly as documented once you get past the origin-trial gate — that part surprised me a little, given how much of what's already written about it is speculative. Whether it's worth building on before Chrome 149 reaches stable is a separate question, and for anything beyond a hackathon demo, my answer right now is: prototype it behind a feature check, keep the real logic on your existing backend, and revisit once more than one browser vendor has committed to more than an origin trial.
Sources: WebMCP documentation, Chrome for Developers · WebMCP imperative API reference · WebMCP declarative API reference · GoogleChromeLabs/webmcp-tools repository · The WebMCP Challenge, OpenAI
Viết và kiểm chứng bởi Nguyễn Hoàng Lộc (Beru), TP. Hồ Chí Minh
