WebMCP
WebMCP is a proposed browser API that lets a web page expose its own tools to AI agents. This page shows how to enable WebMCP in a Browserless session, list the tools a page registers, and call one from Puppeteer.
- A Browserless API token from your account dashboard
- Node.js 22.12+ with
puppeteer-coreinstalled
WebMCP is experimental in Chrome and the API is still changing. This page was tested on Chrome 156. Method names and return shapes may change in future Chrome releases.
What is WebMCP
WebMCP lets a page register tools (a name, a description, a JSON Schema for the input, and a function) on document.modelContext. An agent running in the browser can then call those tools directly instead of clicking through the UI.
It borrows MCP's tool shape, but it isn't an MCP server. The tools live in the page's JavaScript and only exist while that page is open.
When to use WebMCP
Use WebMCP when the site you're automating registers WebMCP tools, or when you're building a site that does and want to test those tools against a real browser. Calling a tool the site defines is faster and less brittle than driving its forms with selectors.
Most sites don't register WebMCP tools yet. For everything else:
- If you want an AI assistant to browse and act on any site, use the Browserless MCP Server and its Browser Agent.
- If you're writing your own automation, use Puppeteer or Playwright over BaaS.
How it works
- You connect to Browserless with
--enable-features=WebMCPin thelaunchparameter. Without it,document.modelContextisundefined. - The page's own JavaScript calls
document.modelContext.registerTool()to add its tools. - Your code runs
page.evaluate()to calldocument.modelContext.getTools(), which lists the registered tools. - You pass those tool definitions to your LLM. When it picks a tool, you run
document.modelContext.executeTool()inside the page and return the result to the model.
--enable-features is available on every plan, so this works on the /chromium, /chrome, and /stealth routes without an Enterprise plan.
Discover and call WebMCP tools with Puppeteer
Few public sites register WebMCP tools today, so this example registers a stand-in tool on example.com before reading it back. On a site that ships WebMCP tools, skip the registerTool step.
The example uses ES module syntax and top-level await, so save it as webmcp.mjs and run it with node webmcp.mjs.
import puppeteer from "puppeteer-core";
const TOKEN = "YOUR_API_TOKEN_HERE";
// WebMCP is off by default in Chrome, so enable it through the launch parameter.
const launch = JSON.stringify({ args: ["--enable-features=WebMCP"] });
const browser = await puppeteer.connect({
browserWSEndpoint: `wss://production-sfo.browserless.io/chromium?token=${TOKEN}&launch=${encodeURIComponent(launch)}`,
});
try {
const page = await browser.newPage();
await page.goto("https://example.com", { waitUntil: "domcontentloaded" });
// Stand-in for a site that ships WebMCP tools: register one the way the site's own JavaScript would.
await page.evaluate(async () => {
await document.modelContext.registerTool({
name: "add",
description: "Add two numbers",
inputSchema: {
type: "object",
properties: { a: { type: "number" }, b: { type: "number" } },
required: ["a", "b"],
},
execute: async ({ a, b }) => ({
content: [{ type: "text", text: String(a + b) }],
}),
});
});
// getTools() also returns tools from same-origin iframes, and names can repeat across frames,
// so keep only the top-level page's tools. Each tool holds a reference to its window, so
// return only the fields an LLM needs.
const tools = await page.evaluate(async () => {
const registered = await document.modelContext.getTools();
return registered
.filter((t) => t.window === window)
.map(({ name, description, inputSchema }) => ({ name, description, inputSchema }));
});
console.log("Tools:", JSON.stringify(tools, null, 2));
// executeTool takes the tool object, not its name, so look it up inside the page.
const result = await page.evaluate(async (name, args) => {
const registered = await document.modelContext.getTools();
const tool = registered.find((t) => t.window === window && t.name === name);
return document.modelContext.executeTool(tool, args);
}, "add", { a: 2, b: 3 });
console.log("Result:", result);
} finally {
await browser.close();
}
Response:
Tools: [
{
"name": "add",
"description": "Add two numbers",
"inputSchema": {
"type": "object",
"properties": {
"a": {
"type": "number"
},
"b": {
"type": "number"
}
},
"required": [
"a",
"b"
]
}
}
]
Result: {"content":[{"type":"text","text":"5"}]}
executeTool() returns the tool's result as a JSON string. Run JSON.parse() on it if you need the object. If the tool triggers a navigation, it returns null instead, and the new page has its own tools, so call getTools() again before the next call. Do the same when document.modelContext fires a toolchange event, since a tool your model picked earlier may be gone or replaced.
The name, description, and inputSchema fields map onto the tool definitions LLM SDKs accept. Rename inputSchema to the field your SDK expects, such as input_schema for the Anthropic SDK or parameters for OpenAI function tools.
A tool can also carry annotations, such as readOnlyHint, that describe whether it changes anything. Keep them next to your tool list if you want to require confirmation before consequential calls. They're hints from the site, not enforcement, so apply your own policy for what the model may run.
FAQ & Troubleshooting
Why is document.modelContext undefined?
WebMCP isn't enabled in the session. Add --enable-features=WebMCP to the args array in the launch parameter and URL-encode the value. See Encoding the launch value. navigator.modelContext was an earlier name for the same API and doesn't exist in current Chrome.
WebMCP also requires a secure context. On a plain http:// page, document.modelContext stays undefined even with the flag set, so load the page over HTTPS or from localhost.
getTools() returns an empty array
The page hasn't registered any tools, or it registered them before you navigated away. Tools only exist for the life of the page that registered them, so call getTools() after the page finishes loading and again after any navigation. Some sites register tools late, after a user action or a client-side route change.
By default, getTools() only returns tools from the calling page's origin. Tools registered in a cross-origin iframe show up only when you list that origin in getTools({ fromOrigins: ["https://tools.example.com"] }) and the iframe's site has shared them with your origin through the exposedTo option of registerTool(). The example above keeps only the top-level page's tools, so to use iframe tools, drop the t.window === window filter in both lookups and match on origin as well as name when selecting the tool to run.
registerTool() rejects with NotAllowedError
The page or frame isn't allowed to use WebMCP. A Permissions-Policy: tools=() response header turns it off, and cross-origin iframes need allow="tools" on the <iframe>. Run the call in the top-level page, or in a frame the site has allowed.
Why does the example map the tools before returning them from page.evaluate()?
Each tool object from getTools() holds a reference to its window, which can't be serialized back to Node.js. Return plain name, description, and inputSchema fields instead, as in the example above.