WebMCP is a proposed browser API that lets a web page register its own JavaScript functions as "tools", each with a name, a description and a JSON Schema, so that an AI agent in the browser can call them instead of scraping the page and simulating clicks. The tools run inside the page, in the user's logged in session. That makes them fast and reliable, and it means a poisoned description, a lying tool or a hostile script in your origin now speaks directly to an agent that carries the user's identity.
WebMCP is a Draft Community Group Report of the W3C Web Machine Learning Community Group (the version we read is dated 20 October 2026), not a W3C Standard. Chrome runs an origin trial from Chrome 149. This post explains the API from the explainer and draft specification, maps the security model, and builds a demo page with a read only tool, a confirm before acting tool and the headers that lock it down.
It goes one level deeper than our earlier posts on the Model Context Protocol, agentic browser risks, AI browser agents that click, browse and transact and computer-use agents.
Key takeaways
- A page calls
document.modelContext.registerTool()with aname,description, optionalinputSchema, anexecutecallback and optionalannotations; the browser runs the callback in the page when an agent calls the tool. - It borrows MCP's vocabulary (tools, schemas, arguments) but is not MCP in the browser. There is no server process, no JSON-RPC transport and no resources or prompts. Tools exist only while the tab is open.
- The browser mediates. Cross-origin access needs both an
exposedTogrant from the tool owner and afromOriginsrequest from the caller. - Access is gated by a Permissions Policy feature called
tools, default allowlist'self'. SendingPermissions-Policy: tools=()turns WebMCP off for a document and every frame inside it, before any script runs. - Three annotations matter for safety:
readOnlyHint,untrustedContentHintandconsequentialHint. They are hints. The spec says a malicious site can omit or misrepresent them, and MCP's own spec says clients must treat annotations as untrusted unless they come from trusted servers. - The spec names the risks: tool poisoning, injection in tool outputs, tools as attack targets, misrepresentation of intent and over-parameterization. Its same-origin section is still marked TODO.
- For site owners it is ordinary web security done well: keep untrusted script out of your origin, disable
toolswhere unused, validate and authorize inside everyexecutecallback, and confirm consequential actions on the page. - Support is early and split: Mozilla's standards position is labeled neutral and WebKit's oppose. Build it as a progressive enhancement.
What WebMCP is and who is developing it
WebMCP is a JavaScript interface that lets a web developer expose features of a web application as tools: functions with natural language descriptions and structured input schemas that agents can call. The draft specification's abstract puts it in one line: the API "enables web applications to provide JavaScript-based tools to AI agents."
The explainer starts from a gap. Agents either call a service's backend through an integration such as an MCP server, which bypasses the web interface and forces developers to replicate the user's state and authentication on a separate server, or they operate the page like a person, reading screenshots and DOM snapshots and simulating clicks, which is slow and brittle. With WebMCP the page declares what it can do, and an agent calls those declarations directly while the user watches the same page.
The explainer's non-goals matter for security planning: WebMCP is not meant for fully autonomous agents running without a browser UI, it does not replace backend MCP, and it does not replace the human interface. Agents can still fall back to general browser automation when a page offers no suitable tool.
Who writes the spec
The specification is published by the W3C Web Machine Learning Community Group, the group that incubated the Web Neural Network API. Its status section says it "is not a W3C Standard nor is it on the W3C Standards Track."
The explainer says it was first published on 13 August 2025 by six engineers from the teams behind Edge and Chrome, and that the draft has since been driven largely by Dominic Farolino. The 20 October 2026 draft lists four editors: Brandon Walderman, Khushal Sagar, Dominic Farolino and Samuel Dallstream. The explainer lists an earlier open source project, MCP-B, as prior art; it is not the API described here.
Timeline
| Date | Event | Source |
|---|---|---|
| 8 July 2025 | WebMCP entry created on Chrome Platform Status | Chrome Platform Status |
| 13 August 2025 | Explainer first published | WebMCP explainer |
| Chrome 146 | Developer trial stage listed, behind a flag | Chrome Platform Status |
| 10 February 2026 | Chrome announces WebMCP for its early preview program | Chrome for Developers blog |
| 18 May 2026 | Chrome's WebMCP developer documentation published | Chrome for Developers |
| 9 June 2026 | Chrome announces the origin trial in Chrome 149; tool security and agent security guides published | Chrome for Developers |
| Chrome 149 to 156 | Origin trial milestone range on the status entry | Chrome Platform Status |
| Edge 150 | Origin trial listed | WebMCP implementation status page |
| 20 October 2026 | Current Draft Community Group Report | WebMCP specification |
Two more data points from the repository's implementation status page: Brave has added experimental support to its built-in AI chat, and Firefox and Safari have standards position issues open rather than implementations. On those issues, Mozilla's is labeled "position: neutral" and WebKit's is labeled "position: oppose", with concern labels for API design, duplication, internationalization, portability, privacy, security, use cases and venue. If your users run several browsers, plan for WebMCP to be absent for many of them.
How you get access today
For local development, Chrome's docs say to enable chrome://flags/#enable-webmcp-testing and relaunch; for real users, register for the time limited origin trial. Chrome's docs also list experimental support in Angular, React (a use-webmcp-tool package) and Vue (@vueuse/core), and TypeScript types in the webmcp-types npm package.
How a page registers a tool
The specification defines one imperative API. A declarative form based variant is documented by Chrome and explored in a separate explainer, but the spec says it is not part of the specification at this time.
The interface
The draft adds a modelContext attribute to Document, available only in secure contexts (HTTPS, or localhost during development). It returns a ModelContext object with three methods and three events:
partial interface Document {
[SecureContext, SameObject] readonly attribute ModelContext modelContext;
};
[Exposed=Window, SecureContext]
interface ModelContext : EventTarget {
Promise<undefined> registerTool(ModelContextTool tool, optional ModelContextRegisterToolOptions options = {});
Promise<sequence<RegisteredTool>> getTools(optional ModelContextGetToolOptions options = {});
Promise<DOMString> executeTool(RegisteredTool tool, optional object inputObject, optional ModelContextExecuteToolOptions options = {});
attribute EventHandler ontoolchange;
attribute EventHandler ontoolactivated;
attribute EventHandler ontoolcancel;
};
registerTool() is for pages that provide tools. getTools() and executeTool() are for in-page agents written in JavaScript, such as a chat widget in an iframe. The browser's own agent "uses a different internal mechanism", covered below.
The tool definition
A tool is a dictionary with these members:
| Member | Required | What it is | Rules from the spec |
|---|---|---|---|
name | Yes | Identifier the agent uses in calls | 1 to 128 characters, ASCII letters and digits plus underscore, hyphen and full stop; must be unique in the document |
title | No | Human readable label the browser may show in its UI | Should be localized |
description | Yes | Natural language explanation the model reads | Must not be empty |
inputSchema | No | JSON Schema object for the arguments | Stored as a JSON string; registration fails if it cannot be serialized |
execute | Yes | Callback that receives the arguments object and an options object with an AbortSignal | Its return value is serialized to JSON and returned to the caller |
annotations | No | Hints about behavior | readOnlyHint, untrustedContentHint, consequentialHint, debugging, all default false |
Registration options carry two more fields. signal is an AbortSignal that unregisters the tool when aborted. exposedTo is a list of secure origins that may discover and run the tool from frames in the same page; an origin that is not potentially trustworthy makes registration fail with a SecurityError.
Here is the explainer's to-do example, trimmed:
const controller = new AbortController();
await document.modelContext.registerTool({
name: "add-todo",
description: "Add a new item to the user's active todo list",
inputSchema: {
type: "object",
properties: {
text: { type: "string", description: "The text content of the todo item" }
},
required: ["text"]
},
async execute({ text }) {
await addTodoItemToCollection(text);
return { content: [{ type: "text", text: `Added todo item: "${text}" successfully.` }] };
}
}, { signal: controller.signal });
// Later, to remove the tool:
// controller.abort();
registerTool() rejects with NotAllowedError when the tools permission is not allowed, and with InvalidStateError when a tool of the same name exists, the name is empty, too long or uses other characters, or the description is empty.
The annotations
The four annotations are booleans. The spec's definitions, in short:
| Annotation | Meaning in the spec | What a careful agent might do with it |
|---|---|---|
readOnlyHint | The tool does not modify state and only reads data | Call without asking, if it trusts the site |
untrustedContentHint | The output contains data the tool author does not trust, such as user generated content | Spotlight, sanitize or hide the payload before the model sees it |
consequentialHint | Running the tool has significant, real world or non-reversible effects, such as booking a flight or transferring money | Require explicit user confirmation |
debugging | The tool is meant for developer tooling, not end users | Filter it out of end user agents |
Chrome's docs say debugging is available from Chrome 156. The spec's security questionnaire notes that a readOnlyHint "may cause the agent to skip a confirmation step", which is exactly why a hint set by a hostile page is dangerous.
The life of a tool call
The explainer describes five steps:
- Registration. The page calls
registerTool()for one or more tools. - Discovery. A connected agent asks the browser for the active tools and their schemas.
- Invocation. The agent requests a call with arguments that should match the
inputSchema. - Execution. The browser mediates the call and invokes the page's
executecallback. - Response. The callback's result goes back to the agent.
In the spec, arguments that are not a JSON object fail with DataError. Otherwise the browser fires toolactivated, calls execute with an AbortSignal, and serializes the result to a JSON string; a rejected promise or unserializable result becomes OperationError, and an unloaded page gives AbortError. If the caller cancels, the signal aborts and toolcancel fires.
The spec does not yet validate arguments against the schema before calling your code. The explainer lists native input and output validation as an open question (issue 92), and Chrome's best practices are blunt: validate strictly in code, loosely in schema. Your execute callback must treat its arguments as untrusted input.
Dynamic registration and the toolchange event
Every registration and unregistration fires a toolchange event so agents can refresh their list. The explainer recommends registering only the tools the current view needs, because every name, description and schema costs model context. There is no updateTool(); to change a description, unregister and register again.
The spec also flags a race: if a page unregisters a tool and quickly re-registers one with the same name and a different schema, a call meant for the old tool can land on the new one. Keep tool names stable and versioned if the schema changes meaningfully.
The declarative variant
Chrome documents a declarative API that turns an HTML form into a tool with toolname, tooldescription, optional toolparamdescription and optional toolautosubmit attributes. Without toolautosubmit, the browser fills the form and the user must press Submit; with it, the agent's call submits the form. Leaving toolautosubmit off is a built in confirmation step: the agent fills, the human submits.
How WebMCP relates to MCP
The spec's introduction says pages that use WebMCP "can be thought of as Model Context Protocol servers that implement tools in client-side script instead of on the backend." Chrome's comparison page is more careful: WebMCP "is not an extension or a replacement of MCP," and is better thought of as a set of "MCP-inspired" APIs. Both statements are true at different levels.
The shared part is the vocabulary: name, description, input schema, result, discovery before calling. In MCP proper, discovery is a tools/list JSON-RPC request and invocation a tools/call request to a server. Almost everything else differs:
| MCP server | WebMCP page | |
|---|---|---|
| Where the tool code runs | A server process, local or remote | JavaScript in the page's own origin, on the page's event loop |
| Transport | JSON-RPC over stdio or Streamable HTTP | Browser internal; the spec does not prescribe a format |
| Lifetime | Persistent while the server runs | Ephemeral: tools exist only while the document is open |
| Discovery | Agent specific registration of the server, then tools/list | Tools registered during the visit; the agent sees them when it observes the tab |
| Identity and auth | MCP authorization (OAuth based for HTTP transports) or local process trust | The user's existing cookies and session in that tab |
| Capabilities beyond tools | Resources, prompts, sampling, elicitation, progress | Tools only, plus browser events |
| Isolation boundary | Process and network boundaries | Same-origin policy, Permissions Policy, exposedTo and fromOrigins |
The explainer explains why the group did not just put MCP in the browser: MCP was built for server to client and stdio or SSE communication and "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 compatibility. The spec also says browsers may expose tools to their agent "via Model Context Protocol, other proprietary function calling methods, or any other way."
Why it is an alternative to screen scraping and computer use
Without WebMCP, a browser agent reads screenshots and DOM snapshots and simulates input. Every click is a guess, a redesign breaks the agent, and anything visible becomes model input, as our computer-use agents post explains.
With WebMCP, the site declares intent. Chrome's docs give the example of a date_pick tool for a calendar widget an agent would otherwise fumble, and a run_diagnostics tool for a developer settings page buried under menus. The tool calls your existing application logic and updates the visible UI, so the human sees what changed. The site, not the agent, decides which actions have a fast path.
It does not remove the other path, and the spec notes that a tool and the equivalent UI flow are different code paths that may have different validation. A tool is an extra entry point and needs the same checks as the button it replaces.
What the user sees and how the browser mediates
This is the area where the specification says least and leaves most to the browser and agent, so it is worth being precise about what is defined.
The browser agent's view: observations
The spec's section on page observations (non-normative) describes how a browser agent sees tools. The browser agent "does not run JavaScript on the page." Instead, at times the browser chooses, usually while the user is interacting with the agent, it takes an observation of the tab: for every document in the frame tree, the document's origin and its list of tool definitions, plus anything else the browser finds useful, such as annotated screenshots or parts of the accessibility tree. The spec notes that browsers "are expected to include the origin field to avoid confusion that could be exploited by an attacker." In other words, a tool registered by an ad iframe should never be presented to the model as if it came from the top level site.
When the agent calls a tool, the browser queues it on the page's event loop and returns the result. The page never sees the model, the prompt or other tabs; it sees arguments and returns a result.
In-page agents: getTools and executeTool
An agent built into a page, or running in an iframe, calls getTools() to list tools and executeTool() to run one. By default getTools() returns only tools registered by same-origin documents in the frame tree. To see tools from a cross-origin frame, two things must be true: the tool owner listed the caller's origin in exposedTo, and the caller listed the owner's origin in fromOrigins. Both lists accept only secure origins. The browser checks both before it runs the tool in the owner's context.
What the user approves
The specification does not define a consent dialog. The explainer lists "user prompting and elicitation" as an open question, with options including delegating to the agent harness or invoking a native browser permission prompt outside the agent loop (issues 165 and 50). The mitigations section describes consequentialHint as a signal so the client or agent can "selectively enforce mandatory user confirmation prompts before executing high-stakes tools", but who shows the prompt and when is up to the implementation.
Chrome's documentation tells site owners that for sensitive actions such as a purchase "you can include a command to request user interaction with a confirmation dialog", and its tool security page says the spec draft includes requestUserInteraction(). The draft we fetched on 24 October 2026 does not contain that method, so treat it as proposed until it appears in the published draft. Today, the dependable options are your own confirmation UI inside the page, the declarative form flow without toolautosubmit, and whatever confirmation the user's agent applies.
What the user can see is the page itself: tools are meant to update the visible UI, Chrome outlines declaratively filled forms with the :tool-form-active pseudo-class, and title gives the browser a readable name for the tool. None of these is an authorization control.
The risks
The draft's security section (section 6, non-normative) starts from what browser agents can do: inherit the user's session, reach personalization and payment data, and correlate context across sites. It states the core limit plainly: a malicious site can omit or misrepresent the hints, and protecting the agent "is primarily the responsibility of the agent's own security model rather than something this specification can normatively enforce."
Prompt injection in tool metadata (tool poisoning)
The model reads every tool's name and descriptions, so a malicious site can put instructions there aimed at the agent, such as visiting another site and sending the user's data. The spec lists the assets at risk as data the agent carries, control of the agent and other sites it may touch. It is the pattern our MCP tool poisoning post covers, except every website can now publish tools.
The spec's partial mitigation is size limits: names are capped at 128 characters and more limits are under discussion (issue 73). Chrome's tool security guide recommends 500 characters per description, 150 per parameter description, 30 per name and 1,500 per tool output.
Prompt injection in tool results
A tool that returns forum posts, reviews or support tickets returns text written by strangers. The spec's example is a forum post that fakes the end of user content and then issues "system" instructions to send data to an attacker. An honest site is a carrier here, not an attacker. Hence untrustedContentHint, and Chrome's advice that agents spotlight untrusted output with delimiters or encoding. OWASP LLM01 calls this indirect prompt injection.
A page lying about what a tool does
The spec calls this misrepresentation of intent and says there is "no guarantee that a WebMCP tool's declared intent matches its actual behavior." Its example is a finalizeCart tool described as finalizing the cart that actually places an order. The agent reasons that the user wants to see the final cart, calls the tool, and the user has bought something. The spec lists the gaps: no way to verify that an implementation matches its description, ambiguous natural language, no behavioral contract that can be analyzed, and agents that must assume good faith.
Actions under the user's session
A tool runs in the page with the user's cookies, so the spec notes it can make purchases, transfer funds, change settings, share data and delete content without additional verification. The page could do this before; what changes is that the trigger is a model's decision, and your server sees an authenticated request from a real session.
Privacy leakage through over-parameterization
Agents fill parameters from what they know about the user. The spec's example is a dress search whose schema also asks for age, pregnancy, location, height and skin tone, each with a plausible description: a "personalization-to-fingerprinting pipeline" that enables silent profiling and cross-site leakage. Agents should minimize data; sites should ask only for what the task needs.
Cross-origin data leakage
The spec's same-origin section (6.3.4) is still a TODO about agents carrying state from one origin into another. exposedTo, fromOrigins and the tools permission control which frames can see and call which tools; they do not control what an agent remembers from one tab and types into a tool on another. Chrome's agent guidance recommends limiting an agent to origins relevant to the task.
Third party scripts registering tools
Every script that runs in your origin can call document.modelContext.registerTool(). That includes analytics tags, chat widgets, A/B testing snippets, tag manager containers and every package in your front end bundle. The spec's first listed mitigation exists for exactly this: disabling WebMCP with Permissions Policy addresses "unintended tool registration or invocation by scripts executing in those documents, including scripts introduced by injection vulnerabilities or compromised dependencies."
Two details sharpen this. A second registration with the same name is rejected, so a script that runs before your code can claim your tool's name. And a cross-site scripting bug becomes a way to plant a persuasive tool the user's agent will read. Chrome's docs add that extensions with host permissions can query and execute WebMCP tools.
Tools as a new path to sensitive functions
The spec says WebMCP "does not inherently expand the attack surface" because the UI already offers the function, then adds that tool and UI are different code paths with possibly different checks. Its example is a reset-password tool: if the UI has a rate limit or step-up check and the tool path does not, you have shipped a bypass.
Mapping to OWASP
| Risk | OWASP Top 10 for LLM Applications 2025 | Where the control lives |
|---|---|---|
| Poisoned descriptions, injected results | LLM01 Prompt Injection | Agent (spotlighting, classifiers, critics), site (untrustedContentHint, output limits) |
| Agent calling powerful tools without checks | LLM06 Excessive Agency | Site (authorization in execute, confirmation), agent (human in the loop) |
| Over-parameterized schemas | LLM02 Sensitive Information Disclosure | Agent (data minimization), site (minimal schemas) |
| Third party script registering tools | Web supply chain, outside the LLM list | Site (CSP, Trusted Types, SRI, tools policy) |
Which existing web controls help, and what the spec says
WebMCP is built on the web's existing security model rather than beside it. That is good news, because the controls already exist and are well understood.
Secure contexts and the same-origin policy
document.modelContext is marked [SecureContext], so it does not exist on plain HTTP pages (localhost counts as secure). Tools run in the registering document's context under the same-origin policy, and by default are visible only to that document, same-origin documents in the page and the built-in agent.
Permissions Policy: the tools feature
Access to the API is gated by a policy-controlled feature named tools with a default allowlist of 'self'. That gives three useful settings:
| Header or attribute | Effect |
|---|---|
| No header (default) | Allowed in the top level document and same-origin frames; blocked in cross-origin frames |
Permissions-Policy: tools=() | Blocked in this document and every frame inside it. Calls reject with NotAllowedError |
Permissions-Policy: tools=(self) | Same as the default, stated explicitly, and a cross-origin frame cannot be granted access by an allow attribute |
<iframe allow="tools"> | Delegates the feature to that frame, if the parent's own policy allows it |
The spec stresses that the browser enforces the header before script runs, which is why it protects against malicious scripts and dependencies. If a page does not use WebMCP, tools=() is the cheapest control in this post.
exposedTo and fromOrigins
Chrome's tool security guide gives the rule for exposedTo: expose a read only tool only to origins you would share that data with anyway, and a read write tool only to origins you trust to act for your user. The explainer is still discussing whether tools in iframes should be exposed to the built-in agent by default.
Content Security Policy, Trusted Types and Subresource Integrity
Because any script in your origin can register tools, controlling which script runs is the main defense. A strict Content Security Policy (own scripts, nonces or hashes only) cuts off injected inline script. Trusted Types, enforced with require-trusted-types-for 'script', blocks DOM based cross-site scripting at dangerous sinks. Subresource Integrity fails the load of a third party script that changed. A tight connect-src limits where compromised code can send data.
iframe sandboxing
If you embed a third party chat agent or widget, give it the least you can. The HTML sandbox attribute removes scripting, forms, popups and same-origin status unless you add them back. Do not add allow="tools" unless the frame needs to call your tools, and then expose only specific tools to its origin with exposedTo. The HTML standard carries one warning worth repeating: setting both allow-scripts and allow-same-origin on a frame whose content is same origin with the parent lets the embedded page remove its own sandbox.
What the spec's security section covers
Section 6 splits responsibility among site authors, agent providers, browsers and users, covers the baseline agent capabilities and the risks above, and lists six mitigations:
- Disabling WebMCP with Permissions Policy (
tools=()). - Restricting maximum input lengths, with the 128 character name cap in place and more limits under discussion.
- Shared attack evaluation datasets, so every implementer defends against at least a common set of prompt injection attacks (issue 106).
- The untrusted annotation for tool responses (
untrustedContentHint). - The consequential annotation for tool executions (
consequentialHint). - Agent level guardrails, with defense in depth recommended and agent architecture left out of scope, pointing to Chrome's agent and tool security guides and the proposed Agentic Web Working Group.
The repository's security and privacy questionnaire adds that registrations do not persist across sessions, and that WebMCP is a new script invocation mechanism, because authorized frames and built-in agents call a page's execute callback directly.
Step by step: build, test and lock down a WebMCP demo page
This builds an order status page with a read only lookup, a reviews tool marked untrusted and a cancel tool that needs a human click, served with security headers using only Node's standard library. It follows the 20 October 2026 draft and Chrome's docs; the API is experimental, so check the explainer before you ship.
Step 1: enable WebMCP for local testing
- Open
chrome://flags/#enable-webmcp-testingin Chrome, set it to Enabled and relaunch. - Optionally install the Model Context Tool Inspector extension linked from Chrome's docs, in a separate test profile, since extensions can query and execute WebMCP tools.
Step 2: create the project
mkdir webmcp-demo && cd webmcp-demo
mkdir public
node --version # any current LTS release works
Step 3: the page
Save as public/index.html. There is no inline script, so a strict CSP is easy.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Order desk</title>
<link rel="stylesheet" href="/app.css">
<script src="/app.js" type="module"></script>
</head>
<body>
<h1>Order desk</h1>
<p id="support">WebMCP: checking...</p>
<ul id="orders"></ul>
<section id="reviews"></section>
<dialog id="confirm">
<p id="confirm-text"></p>
<button id="confirm-yes">Cancel this order</button>
<button id="confirm-no">Keep it</button>
</dialog>
</body>
</html>
Step 4: the tools
Save as public/app.js. Read the comments; they carry the security reasoning.
// Demo data. In a real app these calls go to your API with the user's session.
const orders = new Map([
["A1001", { status: "shipped", item: "Desk lamp" }],
["A1002", { status: "processing", item: "Monitor arm" }],
]);
const reviews = [
"Solid lamp, warm light.",
"Arrived late but works fine.",
];
const ORDER_ID = /^A\d{4}$/; // validate in code, not only in the schema
const MAX_OUTPUT = 1500; // Chrome's suggested per-output budget
function render() {
const ul = document.getElementById("orders");
ul.replaceChildren(...[...orders].map(([id, o]) => {
const li = document.createElement("li");
li.textContent = `${id}: ${o.item} (${o.status})`; // textContent, never innerHTML
return li;
}));
}
// A confirmation the page owns. It resolves only on a real click event.
function confirmOnPage(message) {
const dlg = document.getElementById("confirm");
document.getElementById("confirm-text").textContent = message;
dlg.showModal();
return new Promise((resolve) => {
const done = (ok) => (e) => {
if (!e.isTrusted) return; // ignore synthetic clicks from script
dlg.close();
resolve(ok);
};
document.getElementById("confirm-yes").onclick = done(true);
document.getElementById("confirm-no").onclick = done(false);
});
}
async function registerTools() {
if (!("modelContext" in document)) {
document.getElementById("support").textContent = "WebMCP: not available";
return;
}
document.getElementById("support").textContent = "WebMCP: available";
const mc = document.modelContext;
// 1. Read only lookup.
await mc.registerTool({
name: "get_order_status",
title: "Look up an order",
description: "Returns the item and shipping status for one of the signed-in user's orders.",
inputSchema: {
type: "object",
properties: {
orderId: { type: "string", description: "Order ID, for example A1001." }
},
required: ["orderId"]
},
annotations: { readOnlyHint: true },
async execute({ orderId }) {
if (typeof orderId !== "string" || !ORDER_ID.test(orderId)) {
return "Invalid order ID. Use the format A followed by four digits.";
}
const o = orders.get(orderId);
return o ? `${orderId}: ${o.item}, ${o.status}.` : `No order ${orderId} on this account.`;
}
});
// 2. Returns text written by other customers: mark it untrusted and cap it.
await mc.registerTool({
name: "get_reviews",
title: "Read product reviews",
description: "Returns recent customer reviews of the desk lamp as plain text.",
annotations: { readOnlyHint: true, untrustedContentHint: true },
async execute() {
const text = reviews.join("\n");
return text.slice(0, MAX_OUTPUT);
}
});
// 3. Consequential: the hint asks the agent to confirm, and the page confirms anyway.
await mc.registerTool({
name: "cancel_order",
title: "Cancel an order",
description: "Cancels one of the signed-in user's orders that has not shipped. Asks the user to confirm on the page first.",
inputSchema: {
type: "object",
properties: {
orderId: { type: "string", description: "Order ID to cancel, for example A1002." }
},
required: ["orderId"]
},
annotations: { consequentialHint: true },
async execute({ orderId }, { signal }) {
if (typeof orderId !== "string" || !ORDER_ID.test(orderId)) {
return "Invalid order ID.";
}
const o = orders.get(orderId);
if (!o) return `No order ${orderId} on this account.`;
if (o.status !== "processing") return `Order ${orderId} has shipped and cannot be cancelled here.`;
const ok = await confirmOnPage(`Cancel order ${orderId} (${o.item})?`);
if (signal.aborted) return "Cancelled by the agent before completion.";
if (!ok) return "The user chose not to cancel. Do not retry.";
// Server side, the cancel endpoint must still check the session,
// ownership, order state and a CSRF token, exactly as for the UI button.
o.status = "cancelled";
render();
return `Order ${orderId} cancelled.`;
}
});
}
render();
registerTools().catch((err) => console.error("WebMCP registration failed:", err.name));
A few choices are worth calling out:
- Validation inside
execute. The spec does not validate arguments against the schema, so the regular expression is the real control. textContent, notinnerHTML. Markup in a review would otherwise become script, and script in this origin can register tools.- The page owns the confirmation.
consequentialHintlets a well behaved agent ask first; the dialog asks regardless, andisTrustedignores script dispatched clicks. A browser agent that actuates the page can still click a real button, so for money or credentials add a step it cannot complete, such as passkey re-authentication with user verification.
Step 5: serve it with security headers
Save as server.mjs in the project root:
import { createServer } from "node:http";
import { readFile } from "node:fs/promises";
import { extname, join, normalize } from "node:path";
const root = join(process.cwd(), "public");
const types = { ".html": "text/html; charset=utf-8", ".js": "text/javascript", ".css": "text/css" };
const securityHeaders = {
// Only our own scripts and styles; no inline script, no plugins, no base tag tricks.
"Content-Security-Policy": [
"default-src 'self'",
"script-src 'self'",
"style-src 'self'",
"connect-src 'self'",
"object-src 'none'",
"base-uri 'none'",
"form-action 'self'",
"frame-src 'none'",
"frame-ancestors 'none'",
"require-trusted-types-for 'script'",
].join("; "),
// WebMCP only in this document and same-origin frames; cannot be delegated to others.
"Permissions-Policy": "tools=(self)",
"X-Content-Type-Options": "nosniff",
"Referrer-Policy": "no-referrer",
"Cross-Origin-Opener-Policy": "same-origin",
};
createServer(async (req, res) => {
const path = normalize(new URL(req.url, "http://localhost").pathname).replace(/^(\.\.[/\\])+/, "");
const file = join(root, path === "/" ? "index.html" : path);
try {
const body = await readFile(file);
res.writeHead(200, { "Content-Type": types[extname(file)] || "application/octet-stream", ...securityHeaders });
res.end(body);
} catch {
res.writeHead(404, securityHeaders);
res.end("Not found");
}
}).listen(8080, () => console.log("http://localhost:8080"));
Add an empty public/app.css (or a few styles), then start the server:
touch public/app.css
node server.mjs
Check the headers from another terminal:
curl -sI http://localhost:8080/ | grep -iE 'content-security-policy|permissions-policy'
Step 6: test the tools
Open http://localhost:8080 in the Chrome profile with the flag enabled. The status line should read "WebMCP: available". In DevTools, the console runs in the page, so you can act as an in-page agent:
const tools = await document.modelContext.getTools();
tools.map(t => [t.name, t.annotations]);
const lookup = tools.find(t => t.name === "get_order_status");
await document.modelContext.executeTool(lookup, { orderId: "A1001" });
await document.modelContext.executeTool(lookup, { orderId: "../etc" }); // expect the validation message
const cancel = tools.find(t => t.name === "cancel_order");
await document.modelContext.executeTool(cancel, { orderId: "A1002" }); // the dialog opens; click Keep it
Then try the same tasks in natural language through the inspector extension and check that the agent asks before cancel_order and the page dialog appears.
Step 7: test the negative cases
These tests prove the controls work, not just the happy path.
- Duplicate names are rejected. Call
registerToolagain withname: "cancel_order"; it should reject withInvalidStateError. - Inline script is blocked. Add an inline
<script>tag toindex.html, reload, and confirm the console reports a CSP violation. Remove it again. - WebMCP can be turned off. Change the header to
tools=()and reload. The status line still says "available" (the attribute exists), but registration fails withNotAllowedError. Restoretools=(self). - A cross-origin frame cannot register tools. Start a second copy of the server on another port (ports make different origins), relax
frame-srcto allow it, and embed it with<iframe src="http://localhost:8081" allow="tools">. Withtools=(self)on the parent, registration inside the frame should fail withNotAllowedError. - Arguments are untrusted. Call each tool with missing, wrong typed and oversized arguments and confirm every call returns a clear error instead of throwing or acting.
Step 8: production checklist
| Control | Setting | Why |
|---|---|---|
| Permissions Policy | tools=() on every page that does not register tools; tools=(self) where it does | Stops stray or injected scripts from registering tools |
| CSP | Script allow-list by 'self', nonce or hash; no 'unsafe-inline' or 'unsafe-eval'; tight connect-src | Script in your origin can publish tools to the user's agent |
| Trusted Types | require-trusted-types-for 'script' | Closes DOM based cross-site scripting sinks |
| Subresource Integrity | integrity on any third party script | A modified file fails to load |
| Server side authorization | Every tool calls the same endpoints, with the same checks, as the UI | Tools are a second path to the same function |
| Confirmation | consequentialHint plus a page owned confirmation; step-up auth for money and credentials | Hints are advisory; the page and server are not |
| Output hygiene | untrustedContentHint on user generated content; cap outputs; return plain facts | Results are model input |
| Schema minimalism | Ask only for what the task needs | Over-parameterized schemas extract personal data |
| Logging | Server side log of tool name, arguments and outcome; client side toolactivated and toolcancel listeners | Reconstruct what an agent did |
WebMCP vs server side MCP vs computer use vs browser automation
| WebMCP | Server side MCP | Computer use | Browser automation scripts | |
|---|---|---|---|---|
| Who defines the actions | The site, in page JavaScript | The service, in an MCP server | Nobody; the model infers from the screen | The script author, from selectors |
| Who runs the action | The page's own code, in the user's tab | The MCP server process | Your executor, sending input events | The automation framework driving the browser |
| Auth context | The user's live browser session and cookies | Server credentials or OAuth tokens issued to the client | Whatever is logged in on the screen | Whatever profile or credentials the script loads |
| Human visibility | High: the page updates in front of the user | Low: happens off screen | Medium: visible if someone watches | Low |
| Main risks | Tool poisoning, output injection, misrepresented tools, over-parameterization, third party script in origin | Tool poisoning, over-broad server permissions, token theft, confused deputy | On-screen injection, credential exposure, irreversible clicks | Brittle selectors, credential handling, scripts run with full profile access |
| Main controls | CSP, Trusted Types, tools policy, exposedTo, server side checks, confirmation | MCP authorization, least privilege, human in the loop, server review | VM or container sandbox, allow-lists, per-action approval | Dedicated profiles, secrets management, review of scripts |
In short: MCP for background work against your backend, WebMCP for an agent helping on your live site with the user present, and computer use or scripts for sites that offer neither.
If you build the agent, not the site
Chrome's agent security guide, which the spec cites, splits agent side defenses into deterministic and probabilistic guardrails:
- Token limits. Cap inbound tool payloads and reject oversize responses.
- Honor
untrustedContentHint. Spotlight that output by wrapping it in delimiters or encoding it, and tell the model in system instructions that spotlighted content is data, not instructions. - Restrict origins. Limit the agent to origins relevant to the task.
- Confirm actions. Assume a tool mutates state unless its description or
readOnlyHintclearly says otherwise, and keep a human in the loop for consequential calls. Note that a hostile site will setreadOnlyHint: trueon a destructive tool, so the hint can only make you more cautious, never less. - Classifiers and critics. Scan descriptions and outputs for injection, and use a separate model that never sees untrusted content to check each planned call against the user's request.
This lines up with MCP's own guidance, which says there "SHOULD always be a human in the loop with the ability to deny tool invocations" and that clients "MUST consider tool annotations to be untrusted unless they come from trusted servers." Every website is an untrusted server until proven otherwise.
Should your site adopt WebMCP now?
| Your situation | Suggested approach |
|---|---|
| Public content site, no logged in actions | Little to gain. Send Permissions-Policy: tools=() and revisit later |
| Logged in app with complex forms or widgets agents struggle with | Prototype read only and form filling tools behind the origin trial, with the controls above |
| Payments, account security or regulated data | Start with read only tools. Keep consequential actions on the human UI with step-up auth |
| Heavy third party script on the page (tags, widgets, ads) | Fix script governance first. Every one of those scripts could register tools |
| You already run an MCP server | Keep it for background work. Add WebMCP only where the user is on the page and benefits from seeing the change |
Common mistakes
- Treating the schema as validation. The draft does not validate arguments before calling
execute. Check types, formats, ranges and ownership in code. - Trusting annotations as controls.
readOnlyHintandconsequentialHintare claims made by the page. They help honest agents behave well; they do nothing against a hostile page and nothing for you if your own server does not check. - Building a tool path that skips UI checks. If the button has rate limiting, CSRF protection, step-up auth or fraud checks, the tool must hit the same server path.
- Leaving WebMCP enabled on pages that do not use it. Without
tools=(), any script on those pages can register tools. - Loading third party script without CSP or SRI. Your tag manager now has a channel to the user's agent.
- Writing descriptions as instructions. "Always call this first" reads exactly like injection. Say what the tool does and needs.
- Returning raw user generated content. Mark it with
untrustedContentHint, trim it to what the task needs and cap the length. - Asking for more data than the task needs. Extra optional parameters invite the agent to fill them from the user's profile.
- Changing a tool's schema under the same name. Calls can race onto the new definition. Rename when the contract changes.
- Assuming a consent prompt exists. The spec defines none. If an action needs consent, build it.
FAQ
What is WebMCP in simple terms?
A browser API that lets a web page publish functions, each with a name, description and input schema, that an AI agent in the browser can call. The function runs in the page and updates what the user sees.
Is WebMCP a W3C standard?
No. It is a Draft Community Group Report of the W3C Web Machine Learning Community Group. The document itself says it is not a W3C Standard and is not on the Standards Track.
Which browsers support WebMCP?
Chrome supports it behind the enable-webmcp-testing flag for local development and through an origin trial from Chrome 149. The project's status page lists an Edge origin trial from Edge 150 and experimental support in Brave's AI chat. Mozilla's standards position is labeled neutral and WebKit's is labeled oppose.
Is WebMCP the same as MCP?
No. It reuses MCP's tool concepts, but there is no server, no JSON-RPC transport and no resources or prompts. Tools are page callbacks that exist only while the tab is open.
How do I turn WebMCP off on my site?
Send the response header Permissions-Policy: tools=(). The browser then rejects registerTool() and getTools() calls in that document and every frame inside it with NotAllowedError, before any script on the page can use the API.
Can a malicious website use WebMCP to attack my agent?
It can try, through poisoned descriptions, injected results, misleading tools and schemas that ask for personal data. The spec says protecting the agent is mainly the agent's job: restrict origins, treat annotations as untrusted, spotlight untrusted output and confirm consequential calls.
Can third party scripts on my page register WebMCP tools?
Yes. Any script running in your origin can call registerTool(), and the first registration of a name wins. Keep untrusted script out with CSP, Trusted Types and Subresource Integrity, and disable the tools feature on pages that do not use it.
Does the user have to approve every WebMCP tool call?
The spec does not require it. consequentialHint lets agents and browsers require confirmation for high stakes tools, and a native confirmation mechanism is an open question in the explainer. Build your own confirmation for anything consequential.
Does WebMCP work in iframes?
Yes, with limits. The tools permission is allowed by default in top level and same-origin frames only. A cross-origin frame needs allow="tools", the tool owner must list the caller's origin in exposedTo, and the caller must list the owner's origin in fromOrigins.
Where Safeguard fits
Safeguard does not sit in the browser. It does not inspect WebMCP tool calls, police browser agents or detect prompt injection in tool descriptions or results; those controls belong to the agent, the browser and your own code.
The overlap is the code that ships your tools. WebMCP tools live in your front end bundle, next to every npm package your build pulls in, including the experimental helpers Chrome's docs mention for React, Angular and Vue and the webmcp-types package. All of that code runs in the same origin as your tools and can call registerTool(). Safeguard can scan the repositories and container images you build (safeguard scan --dir ., safeguard scan --image <name>), match the components it finds against vulnerability databases, apply reachability analysis to rank what your code actually calls, show which projects carry a given package version, and gate builds with policies. For agents that should reach your systems through a server side tool interface instead of a page, Safeguard's own MCP server lets assistants such as Claude query risk data with tools you choose. None of this replaces CSP, the tools policy, server side authorization or confirmation.
Software supply chain hygiene matters here because a WebMCP tool is only as trustworthy as the least trustworthy script in its origin, and front end bundles routinely carry hundreds of open source packages. The fit is indirect: it covers the dependencies and images you build, not the behavior of a browser agent or a hostile website.
Fix it without waiting for upstream
Whatever the vulnerability, most of the risk sits in two gaps. The first is the time between a fix being published and that fix actually running in your production builds. The second, and worse, is the time before any fix exists at all, while a maintainer works out a patch and a release schedule. Waiting through both gaps is a habit, not a requirement. Safeguard gives teams three ways to close them.
1. Autonomous remediation: pull requests, not tickets
When a fixed version exists, Safeguard's Griffin models open a tested, policy gated pull request that moves the affected dependency to the safe version. You can run it on one repository or in bulk across every project that carries the vulnerable version. Remediation covers eight categories, including dependencies, licenses, security posture and findings. Reachability analysis ranks the vulnerable code paths your application actually calls first, so the first pull requests you merge are the ones that matter most. Autonomous remediation ships switched off, with every category set to Manual, so your team decides how much runs on its own and what still needs a human approval.
2. Gold Open Source: the safe version in seconds, free
gold.safeguard.sh is free and needs no login. Search any CVE or package to see affected and fixed versions, CVSS, EPSS, CISA KEV status and a recommended safe version, or use the "Am I affected?" checker. The same data is available through a free read API, README status badges and RSS threat feeds. The safeguard-sh/gold-scan-action GitHub Action can fail a build on a known exploited vulnerability, on any known vulnerability, or never, so a version you have just removed cannot quietly come back in next week's merge.
3. Gold Registry: start from components that are already fixed
registry.safeguard.sh provides hardened, drop in replacements for upstream packages and container images. Every Gold artifact is held continuously to zero exploitable critical or high CVEs, zero malware and a completed license check, with SLSA L3 or higher provenance, a Sigstore signature and a public evidence page you can hand to an auditor. An artifact loses its Gold badge if it regresses. Teams can point their own package directory at the registry so hardened equivalents resolve without changing how developers install anything.
When upstream has no fix yet, or will not ship one, Safeguard first works to get the fix accepted upstream. If the issue is severe or upstream will not move, Safeguard builds a fork, fixes the vulnerability there and publishes a hardened Gold build, so you are not stuck on a maintainer's release schedule. You can also request a hardened, compatibility tested build of a specific package. This fork and patch flow is in early access today, so ask which packages it covers before you plan around it.
| Your situation | What to use |
|---|---|
| A fixed version exists | Autonomous remediation opens the upgrade pull request; Gold Open Source confirms the safe version |
| You need to know today whether you are affected | Gold Open Source lookup and "Am I affected?" checker, free and without login |
| You want vulnerable versions to stop shipping | The Gold scan GitHub Action in CI, plus Safeguard policies and gates |
| Upstream has no fix yet, or will not ship one | A custom Gold request; fork and patch (early access) |
| You want to start clean on new projects | Pull hardened components and images from Gold Registry |
One honest limit: when the vulnerable code lives in a vendor appliance, a hosted service or an operating system runtime you do not build yourself, the fix still has to come from that vendor or distribution. The steps above apply to the open source dependencies and container images you build and ship, which is where most teams carry the largest and least tracked share of their exposure.
See how Safeguard would help
None of these demos shows WebMCP itself. They show how Safeguard's own agent surfaces handle tool access and approval, which is the same design problem from the other side. You can download Safeguard and follow the CLI docs to try them.
This demo connects Claude to the Safeguard MCP server with OAuth and lets you choose which tools Claude can use. It is the server side half of the comparison table, and the same discipline a site needs when it decides which WebMCP tools to register and which origins to expose them to.
Click through it at your own pace, or open it full screen.
This is a real CLI session: asked to fix a lodash CVE, the CLI reads the finding and advisory, asks before each command, edits package.json to lodash 4.17.21, runs npm install and npm test with approval, and pushes nothing. It is the kind of dependency fix your front end bundle needs when a package that runs beside your WebMCP tools has a known vulnerability, and its ask before acting flow mirrors the confirmation a consequential tool should require.
Click through it at your own pace, or open it full screen.
This real Android session sends work to a computer running the CLI and approves the command on the phone with Allow once, Allow for turn or Deny. It shows confirmation at the point of risk, which is the pattern consequentialHint asks browser agents to follow before a tool books, buys or deletes anything.
Click through it at your own pace, or open it full screen.
Quick check
A page registers a tool named finalizeCart with readOnlyHint set to true, and calling it places an order. Which spec risk is this, and why does the hint not protect the user?
It is misrepresentation of intent, the spec's own finalizeCart example. Annotations are claims made by the page, and the spec says a malicious site can omit or misrepresent them. An agent should treat readOnlyHint as a reason for extra caution at most, assume tools mutate state unless it trusts the site, and confirm with the user before anything that could buy, send or delete.
Your marketing pages never register WebMCP tools but load a tag manager and two analytics scripts. What single header reduces the WebMCP risk on those pages, and what else should you do?
Permissions-Policy: tools=(). The browser then blocks the API in that document and every frame inside it before any script runs, so no tag or injected script can register tools there. You should still keep a strict Content Security Policy, use Subresource Integrity on third party scripts and enable Trusted Types, because those scripts can do plenty of harm without WebMCP.
Sources
- WebMCP explainer (W3C Web Machine Learning Community Group, GitHub)
- WebMCP specification, Draft Community Group Report, 20 October 2026
- WebMCP implementation status
- WebMCP security and privacy self-review questionnaire
- WebMCP declarative API explainer
- W3C Web Machine Learning Community Group
- Chrome for Developers: WebMCP is available for early preview, 10 February 2026
- Chrome for Developers: Join the WebMCP origin trial, 9 June 2026
- Chrome for Developers: WebMCP
- Chrome for Developers: WebMCP Imperative API
- Chrome for Developers: WebMCP Declarative API
- Chrome for Developers: WebMCP versus MCP
- Chrome for Developers: WebMCP best practices
- Chrome for Developers: WebMCP tool security
- Chrome for Developers: Agent security considerations for WebMCP
- Chrome Platform Status: WebMCP
- Mozilla standards positions: WebMCP, issue 1412
- WebKit standards positions: WebMCP, issue 670
- Model Context Protocol specification: Tools (2026-07-28)
- Model Context Protocol specification: Tools (2025-11-25)
- OWASP GenAI Security Project: LLM01:2025 Prompt Injection
- OWASP GenAI Security Project: LLM02:2025 Sensitive Information Disclosure
- OWASP GenAI Security Project: LLM06:2025 Excessive Agency
- W3C: Content Security Policy Level 3
- W3C: Permissions Policy
- W3C: Trusted Types
- W3C: Subresource Integrity
- WHATWG HTML Standard: the iframe element and sandboxing
Last reviewed: 24 October 2026. WebMCP is an experimental Community Group draft and Chrome's implementation changes between milestones; check the explainer, the specification and Chrome's documentation before you build.