AI
AIIntermediate

WebMCP Explained: How Chrome Turns Your Website Into an MCP Server for AI Agents

WebMCP explained: how the new browser API lets AI agents call a website’s JavaScript as MCP-style tools, how Chrome 149 implements registration, discovery and execution, how WebMCP differs from MCP servers, and what I learned shipping eight WebMCP tools to production.

Cover image: WebMCP Explained: How Chrome Turns Your Website Into an MCP Server for AI Agents
Contents

AI agents use most websites the way a person would use them blindfolded: take a screenshot, guess which box is the search field, click, type, wait, take another screenshot and hope the layout didn’t change. It works often enough to demo and fails often enough to be annoying.

WebMCP flips that around. Instead of the agent reverse-engineering your interface, your page tells the agent what it can do — as tools with names, descriptions and JSON Schemas, the same idea as an MCP server, but running inside the browser tab. Chrome shipped it as an origin trial in Chrome 149, and on September 27, 2026 I put five WebMCP tools live on the tools section of this site.

This guide explains how WebMCP works, what Chrome actually does between your page and the agent, how it differs from MCP, and what I ran into shipping it to production.

WebMCP explained: a website registers tools with document.modelContext.registerTool() and an AI agent in Chrome calls explain_cron directly instead of clicking through the page

WebMCP at a Glance

WebMCP at a Glance
What it is A proposed web standard for exposing a page’s JavaScript functions and HTML forms as tools for AI agents
Who is behind it W3C Web Machine Learning Community Group; the explainer was first published on August 13, 2025 by engineers from Microsoft and Google
Core API document.modelContext.registerTool(), getTools(), executeTool(), plus a toolchange event
Declarative variant toolname and tooldescription attributes on a <form>
Status in Chrome Origin trial from Chrome 149; local testing via chrome://flags/#enable-webmcp-testing
Other implementations Edge origin trial from Edge 150, ChatGPT Desktop, experimental in Brave Leo; Firefox and Safari have open standards-position requests
Security gates Origin-isolated documents only; tools Permissions Policy, default self

Why Websites Need WebMCP

Today an agent that wants to use a website has two options. If the service publishes an MCP server or an API, the agent talks to the backend and skips the site entirely. If it doesn’t, the agent falls back to actuation: it simulates mouse clicks and keyboard input from screenshots, the DOM and the accessibility tree.

Actuation is the fragile path. Every step is a chance to misread a label, click the wrong element or act before the page finished rendering, and a redesign can break an agent that worked yesterday. The WebMCP explainer describes the goal plainly: let agents interact “through well-defined client-side tools instead of through brittle UI actuation”.

Here is the same question asked of my Cron Translator both ways:

Actuation vs a WebMCP tool call: seven fragile UI steps to read a cron schedule versus one explain_cron call with typed input and a short text answer
Same page, same question. On the left the agent guesses; on the right the page answers.

The right-hand side is not a mock-up. That call and that answer come from the tool running on this site.

WebMCP doesn’t replace the UI or the backend integration. The explainer lists replacing human interfaces and replacing MCP as explicit non-goals. It adds a third path that keeps the user, the page and the agent in the same place: the agent works through the page, and the page updates visibly as it does.

How WebMCP Works

A WebMCP tool has four parts, and if you’ve written an MCP server they will look familiar:

const controller = new AbortController();
await document.modelContext.registerTool(
{
name: 'explain_cron',
description: 'Explain a cron expression in plain language and list its next runs in a time zone.',
inputSchema: {
type: 'object',
properties: {
expression: { type: 'string', description: 'Cron expression, e.g. "30 2 * * 1-5".' },
timezone: { type: 'string', description: 'IANA time zone, e.g. Europe/Berlin. Default UTC.' },
},
required: ['expression'],
},
annotations: { readOnlyHint: true },
execute: async ({ expression, timezone }) => explainCron(expression, timezone),
},
{ signal: controller.signal },
);
// Later: controller.abort() unregisters the tool.
  • name and description are what the model reads to decide whether the tool fits the user’s request.
  • inputSchema is a JSON Schema for the arguments. The agent fills it in; you don’t parse free text.
  • execute() is ordinary page JavaScript. It can reuse the functions your UI already calls, update the DOM and return text or JSON.
  • annotations are optional hints about side effects and trust, covered below.

For plain forms there is a declarative API that needs no JavaScript at all:

<form toolname="createSupportRequest"
tooldescription="Submits a request for customer support."
action="/support">
<label for="email">Email</label>
<input id="email" name="email" type="email" required>
<button type="submit">Send</button>
</form>

Chrome turns the form fields into a JSON Schema, using each field’s <label> (or a toolparamdescription attribute) as the parameter description. When the agent calls the tool, the browser focuses the form and fills it in while the user watches.

How Chrome Implements WebMCP

This is the interesting part. WebMCP is not a library that the agent loads into your page. The browser owns the registry and sits in the middle of every call.

How a WebMCP tool call flows through Chrome: the page registers a tool, Chrome notifies the agent, the agent calls executeTool, Chrome runs execute() in the page and returns the result
Six steps, and Chrome is in the middle of every one of them.

1. The gates come first

Before a single tool registers, three checks apply:

  • The feature must be on. Either your origin carries a valid origin trial token, or the user enabled the testing flag.
  • The document must be origin-isolated. If a page opts into document.domain — for example with Origin-Agent-Cluster: ?0 — Chrome disables the WebMCP APIs, so a tool’s origin can’t change during its lifetime.
  • The tools Permissions Policy must allow it. It defaults to self: top-level documents and same-origin iframes can register tools, cross-origin iframes can’t unless the embedder adds allow="tools". When the policy blocks it, registerTool() rejects with a NotAllowedError.

2. Registration builds a per-document registry

registerTool() hands Chrome the definition. Chrome stores it with the page’s origin and window and fires a toolchange event on document.modelContext. getTools() returns the list alphabetically and, by default, only includes tools from same-origin documents in the frame tree. A tool becomes visible to another origin only if you list it in exposedTo and the caller asks for it with fromOrigins.

3. The agent discovers tools while the user is on the page

The agent reads each tool’s name, description and schema into the model’s context. Discovery is tied to the visit: as Chrome’s documentation puts it, clients “must visit a site directly to know if it has callable tools”. There is no crawler-readable manifest yet.

4. Chrome mediates the call

When the model picks a tool, the agent calls executeTool(tool, args). Chrome checks that exposure and origins agree, then runs your execute() in the tool owner’s own context. That means it runs with the user’s logged-in session, cookies and current page state — exactly what a backend integration would have to rebuild on a server.

execute() receives an AbortSignal as options.signal. If the user hits stop in the agent’s UI, the signal fires and you can cancel a fetch() or any long task. If a tool triggers a navigation, executeTool() resolves with null.

5. Unregistering is just an abort

Tools are unregistered by aborting the signal you passed to registerTool(). Since Chrome 153, unregistering no longer cancels an execution that is already running, which matters in component frameworks that mount and unmount often. Chrome’s guidance is to register tools that fit the current page state and remove them when they stop applying, because every tool costs context tokens and increases the chance the model picks the wrong one.

6. Annotations tell the agent how careful to be

6. Annotations tell the agent how careful to be
Annotation Meaning What the agent can do with it
readOnlyHint The tool only reads, no side effects Call it without asking the user
consequentialHint Significant or irreversible action, such as a booking or payment Ask the user to confirm first
untrustedContentHint Output contains user-generated or third-party text Treat the payload as data, not instructions
debugging (Chrome 156+) Tool is meant for developer tooling Hide it from end-user agents

7. The declarative API gets real browser hooks

Declarative forms get platform features no library could add. SubmitEvent.agentInvoked tells your submit handler whether a human or an agent submitted the form, and respondWith() lets you return a result to the model. The toolactivated and toolcancel events fire when an agent fills or abandons a form. The CSS pseudo-classes :tool-form-active and :tool-submit-active let you style a form while an agent is working on it, and toolautosubmit lets the agent submit without the user clicking the button.

WebMCP vs MCP

The most common question about WebMCP is whether it replaces MCP. It doesn’t, and Chrome’s own guide opens by calling that a misunderstanding.

MCP vs WebMCP: MCP connects an agent to a backend server over JSON-RPC and runs persistently; WebMCP runs in the open browser tab through Chrome and exists only while the page is open
Both expose tools. MCP lives on your server; WebMCP lives in your page.
WebMCP vs MCP
MCP WebMCP
Where the tool runs A server or local process The page’s JavaScript
Lifecycle Persistent Ephemeral, bound to the tab
Reach Desktop, mobile, cloud, IDEs Browser agents only
Auth and state Rebuilt on the server The user’s live session
Transport JSON-RPC over stdio or HTTP Browser-mediated calls
Resources and prompts Yes No — tools only

The WebMCP explainer is explicit about why it didn’t simply put MCP in the browser: MCP “lacks native web concepts like origins, standard browser permissions, DOM integration, and tab-level lifecycle management”, and tying a web API to an evolving backend protocol would hurt backward compatibility. So WebMCP shares MCP’s vocabulary — tools, schemas, parameters — but is a web-native API.

In practice you want both. If agents should reach your service from Claude Desktop, an IDE or a cloud workflow, that is an MCP server, and controlling which user gets which tool belongs in a gateway in front of it. If the user is on your site and wants an agent to help with what’s on screen, that is WebMCP.

My Implementation: Eight WebMCP Tools in Production

Every page under /en/tools on this site registers six read-only tools when the browser supports WebMCP, and since October 2026 every page on the site adds two more for searching and reading the blog (details below):

My Implementation: Eight WebMCP Tools in Production
Tool What it does Live page
check_gcp_iam_policy Audits a Google Cloud IAM policy for least-privilege problems IAM Checker
recommend_gcp_compute Picks Cloud Run, GKE, Compute Engine and so on for a workload Compute Selector
recommend_gcp_database Picks Cloud SQL, AlloyDB, Spanner, Firestore and so on Database Selector
explain_cron Explains a cron expression and lists the next runs in a time zone Cron Translator
convert_cron Translates cron syntax between nine platforms Cron Translator
analyze_jwt Decodes a JSON Web Token, audits it for alg: none, weak HMAC secrets, algorithm confusion and missing claims, and optionally verifies the signature JWT Analyzer

All six run the same engines the UI uses. The input never leaves the browser: the IAM policy or JWT an agent passes in is analysed locally, just like one a person pastes in.

Loading: zero cost for everyone else

Most visitors’ browsers don’t have WebMCP, so the tool module must never load for them. The site uses Astro’s client-side router, which means the loader also has to register tools when you navigate into /tools and remove them when you leave:

let controller = null;
let registeredFor = '';
async function syncWebMcp() {
const onTools = /^\/(en|de)\/tools(\/|$)/.test(location.pathname);
const key = onTools ? document.documentElement.lang : '';
if (key === registeredFor) return;
controller?.abort(); // leaving /tools or switching language
controller = null;
registeredFor = '';
if (!onTools || !('modelContext' in document)) return;
const current = new AbortController();
controller = current;
registeredFor = key;
const { registerWebMcpTools } = await import('./webmcp-tools.js');
await registerWebMcpTools(current.signal);
}
document.addEventListener('astro:page-load', syncWebMcp);

Three details matter here. The feature check ('modelContext' in document) runs before the dynamic import(), so unsupported browsers download nothing. The AbortController unregisters every tool in one call when the route changes. And the language is part of the key, so switching from /en/tools to /de/tools re-registers the tools with German output.

Schemas come from the UI’s own content

The two selector tools take the same questions the interactive quiz asks. Instead of writing the schema by hand, it is generated from the question list the UI renders:

export function selectorSchema(kind) {
const properties = {};
for (const q of SELECTORS[kind].questions.en) {
properties[q.id] = {
type: 'string',
enum: q.options.map((o) => o.value),
description: `${q.label} ${q.options.map((o) => `${o.value} = ${o.label}`).join('; ')}`,
};
}
return { type: 'object', properties };
}

Add a question to the quiz and the agent sees it on the next deploy. The schema can’t drift from the UI because there is only one source.

Output: under 1,500 characters, with a way back

Chrome’s security guide recommends about 1.5K characters per tool output, 500 per tool description and 150 per parameter description. An IAM policy with forty findings would blow that easily, so every tool builds its answer line by line and stops when the next line would cross the budget:

export const MAX_OUTPUT = 1500;
function fitLines(head, lines, footer) {
let out = head;
for (let i = 0; i < lines.length; i += 1) {
const rest = lines.length - i - 1;
const tail = rest ? `\n… and ${rest} more` : '';
if (`${out}\n${lines[i]}${tail}\n${footer}`.length > MAX_OUTPUT) {
return `${out}\n… and ${lines.length - i} more\n${footer}`;
}
out += `\n${lines[i]}`;
}
return `${out}\n${footer}`;
}

The footer is always a link to the full interactive report, so the agent can hand the user the complete result instead of a truncated one.

Errors the agent can fix itself

The WebMCP best practices say to validate “strictly in code, loosely in schema” and return errors the model can act on. A bad value doesn’t throw; it tells the agent exactly what to send instead:

Invalid value "h100" for hardware. Use one of: none, gpu, tpu.

Two tools with untrustedContentHint: true

Four tools return text that I wrote. Two are different. check_gcp_iam_policy quotes role names and member strings from the policy the user passed in. A policy is JSON anyone can edit, so a member string could contain an instruction aimed at the model. analyze_jwt returns the decoded header and claims of the token it was given, and anyone who can mint a token chooses what those claims say. Only these two are marked untrustedContentHint: true; all six carry readOnlyHint: true and consequentialHint: false.

The origin trial token expires

The token is a meta tag in the page head, bound to https://www.alekseialeinikov.com:

<meta http-equiv="origin-trial" content="…token…">

Mine expires on November 17, 2026, and there is no API to renew it. A weekly CI job checks the expiry date and fails 21 days before it, so GitHub sends me an email in time. An expired token breaks nothing — WebMCP simply switches off — but the tools would quietly disappear.

Update, October 2026: search and read the blog from any page

The tools above only exist under /tools. An agent on any of the 100+ articles had nothing to call, even though the obvious request on a blog is “find the article about X and tell me what it says”. So every page now registers two more tools:

Update, October 2026: search and read the blog from any page
Tool What it does
search_blog_articles Takes the user’s words, an optional language and a limit of up to 8, and returns the best matches with topic, date, a one-line summary and URL
read_blog_article Without section, returns the title, summary and a numbered list of sections; with section (a number or a heading), returns that section’s text

Splitting reading into outline and section is the only way to stay inside the 1,500-character output budget: a full article runs to 15,000–20,000 characters. The agent reads the outline, picks the section the user cares about and asks for it. It is the same progressive disclosure that Agent Skills use.

Neither tool needed new infrastructure. Search reads the static JSON index the blog archive already loads for its search box, cached in memory after the first call. Reading fetches the article’s markdown twin, the .md version every article already has for AI crawlers. Both are same-origin requests, and read_blog_article rejects any URL that is not an article on this domain.

Ranking is deliberately simple. A query term found in the title counts three times, in the keywords twice and in the description once, each weighted by how rare the term is across the blog, and matches far below the best one are dropped. “gke autopilot cost” returns the Autopilot vs Standard comparison and the managed-Kubernetes article, not every post that mentions cost.

The loader gained three lines:

let blogTools = null;
async function syncWebMcp() {
const modelContext = document.modelContext ?? navigator.modelContext;
if (!modelContext) return; // no WebMCP: nothing is downloaded
blogTools ??= import('./webmcp-blog-tools.js')
.then(({ registerBlogTools }) => registerBlogTools(modelContext));
// …the /tools logic from above, unchanged
}

The blog tools are registered once per document and never removed, because Astro’s router keeps the same document across navigations. The navigator.modelContext fallback covers the earlier shape of the API, which some tools still probe.

Passing Cloudflare’s Agent Readiness check

Cloudflare’s Agent Readiness scanner (isitagentready.com, also in the Cloudflare dashboard) has a WebMCP check, and its audit log shows exactly what it does: load the home page in a browser and look for tools registered through document.modelContext or navigator.modelContext on page load. With tools only under /tools, the result was “No WebMCP tools detected on page load”. The scan after the blog tools shipped passed.

Two caveats. The check passes because the scanner’s browser exposes modelContext; the feature detection above still keeps the module away from every ordinary visitor. And a green check is not usage: as of October 19, 2026, real agents have barely touched any of the seven tools.

Measuring agent calls

Every execute() sends a consent-free beacon to the site’s own /api/agent-call endpoint with three allow-listed fields: tool, mode and page language. No cookies, IP addresses or IDs. The endpoint logs the call and forwards it to GA4 as a tool_use event with tool_action: 'agent_call', so my weekly report shows agent calls on their own line, split by tool and mode (blog:search, blog:read, cron-translator:explain), separate from people using the same tools in the UI. Three weeks after launch the count is still in single digits. That isn’t a surprise: WebMCP is still an origin trial, and an agent has to be connected to the tab before anything calls a tool. The point of shipping now is to be ready and measurable when that changes.

Try It Yourself

  1. Open chrome://flags/#enable-webmcp-testing in Chrome, enable it and relaunch.
  2. Install the Model Context Tool Inspector extension.
  3. Open /en/tools/cron-translator and open the extension. It lists the six tools with their schemas, plus the two blog tools. On any other page, such as this article, you see only search_blog_articles and read_blog_article.
  4. Call explain_cron with {"expression": "30 2 * * 1-5", "timezone": "Europe/Berlin"}, or type a question in plain English and let the extension’s agent pick the tool.

The extension also shows exactly what your own tools return, which is the fastest way to check that your descriptions and outputs read well to a model.

WebMCP Security

WebMCP narrows who can see your tools, but it doesn’t make an agent safe. Chrome’s security guide is direct about it: models are probabilistic, repeatable prompt injection attacks against agents exist, and “it’s impossible to guarantee safety inside of a large language model”.

What you control as the site owner:

  • Exposure. By default tools are visible only to your page, same-origin frames and built-in browser agents. Add origins to exposedTo only if you’d share the same data with that site directly.
  • Annotations. Set consequentialHint: true on anything that spends money, sends messages or deletes data, and untrustedContentHint: true on anything that returns text you didn’t write.
  • Validation. Treat every execute() call like an API request from an untrusted client. The schema is a hint to the model, not a guarantee.
  • Extensions. A Chrome extension with host permissions can call your WebMCP tools — but it could already run arbitrary JavaScript on your page without them. WebMCP doesn’t open a new hole there; it makes the intended path cleaner.

If you’re thinking about agents acting on real systems, what happened when autonomous agents went rogue on a government website is a good reminder of why the guardrails belong in your code.

Should You Add WebMCP Now?

Add it now if:

  • your site has tasks, not just content — search, filters, configurators, forms, calculators, dashboards;
  • the logic already runs in the browser, so tools are mostly wrappers around existing functions;
  • you can keep the first tools read-only and add write actions later with consequentialHint.

Wait if:

  • your pages are mostly articles — agents read those fine today, and an llms.txt file helps more;
  • your key actions need server-side state the page doesn’t have — that’s an MCP server’s job;
  • you can’t commit to renewing an origin trial token every few months.

Because the whole thing sits behind a feature check, the cost to users without WebMCP is zero. The real cost is designing good tools: short descriptions, clear schemas and outputs a model can use.

The Bottom Line

WebMCP turns a website from something an agent has to decipher into something it can call. The page declares its tools, Chrome keeps the registry, checks the gates and runs your execute() in the user’s own session, and the agent gets a typed, short answer instead of a screenshot to interpret.

It is early: an origin trial in Chrome and Edge, one desktop assistant, and zero agent calls in my own analytics so far. But the API is small, the cost for everyone else is zero, and the tools on this site’s tools section took less code than the UI around them. If your site has tasks and the logic already runs in the browser, it’s worth shipping one read-only tool now and seeing what calls it.

Frequently asked questions

What is WebMCP?

WebMCP is a proposed browser standard from the W3C Web Machine Learning Community Group, first published in August 2025 by engineers from Microsoft and Google. It lets a website expose JavaScript functions or HTML forms as tools with natural-language descriptions and JSON Schemas, so AI agents in the browser can call them directly instead of simulating clicks and typing.

What is the difference between WebMCP and MCP?

MCP (Model Context Protocol) connects agents to backend servers over JSON-RPC and is available anywhere, at any time. WebMCP runs in the browser: tools are registered by the page's JavaScript, the browser mediates every call, and the tools exist only while the user has the tab open. WebMCP borrows MCP's vocabulary of tools and schemas but omits server concepts such as resources. Chrome's guidance is to use both: MCP for core logic, WebMCP for the live website.

Which browsers support WebMCP?

As of September 2026, Chrome runs a WebMCP origin trial from Chrome 149 and Edge from Edge 150. For local development you can enable chrome://flags/#enable-webmcp-testing. ChatGPT Desktop supports WebMCP, Brave has experimental support in Leo, and Firefox and Safari have open standards-position requests but no implementation.

How do I add WebMCP to my website?

Register an origin trial token for your origin, feature-detect with 'modelContext' in document, then call document.modelContext.registerTool() with a name, a description, an inputSchema and an async execute() function that returns text or JSON. Pass an AbortSignal so you can unregister the tool, add annotations such as readOnlyHint, and test with the Model Context Tool Inspector extension. For simple forms, the declarative API only needs toolname and tooldescription attributes.

Is WebMCP safe?

WebMCP limits who can see tools: by default only the page itself, same-origin frames and built-in browser agents, with cross-origin exposure only through an explicit exposedTo list. It does not solve prompt injection. Mark tools that return user-generated or external content with untrustedContentHint, mark irreversible actions with consequentialHint so the agent asks for confirmation, and validate every input in execute() as you would for any untrusted caller.

Does Cloudflare's Agent Readiness scanner check WebMCP?

Yes. Its WebMCP check loads your home page in a browser and looks for tools registered through document.modelContext or navigator.modelContext on page load. Tools registered only on deeper pages are not detected, so register at least one tool that makes sense on every page, such as site search.

From the community

Discussion on Mastodon & Bluesky

Replies to this article from the open web, pulled in live. No ads, no tracking scripts.

Loading replies …
Reply on MastodonReply on Bluesky

Reply there — it shows up here automatically.

ENDE