WebMCP

WebMCP lets a page hand tools to agents: functions it registers with document.modelContext.registerTool(), or forms it marks with <form toolname>. Each tool has a name, a description, an input schema and hints about what it does. ferridriver lists and calls them from scripts, the test runner, Node, Rust and the MCP server, through one API: page.webmcp and frame.webmcp.

Turning it on

WebMCP is still behind a flag in browsers.

  • Chromium: launch with --enable-features=WebMCP. Some Chromium builds keep listing and the DevTools calls behind their own features, WebMCPTesting and DevToolsWebMCPSupport; ferridriver enables both with WebMCP so it alone is enough. Chromium reads only the last --enable-features it is given; ferridriver merges every one, its own defaults included, into a single switch, so WebMCP can go in any of them.
  • Firefox: set the preferences dom.modelcontext.enabled and dom.modelcontext.testing.enabled.
const browser = await chromium().launch({ args: ['--enable-features=WebMCP'] });
# The MCP server's browser
[mcp.browser]
chromeArgs = ["--enable-features=WebMCP"]

The API needs a secure context: an https: page, or http://localhost and http://127.0.0.1. A document without WebMCP rejects with WebMCP discovery is unavailable in this document.

Listing tools

const tools = await page.webmcp.tools();
// [{
//   name: 'search_flights',
//   title: 'Search flights',
//   description: 'Find flights between two airports',
//   inputSchema: { type: 'object', properties: { from: {...}, to: {...} } },
//   annotations: { readOnly: true },
//   origin: 'https://example.com',
//   frame: { name: '', url: 'https://example.com/' },
// }]
FieldMeaning
name, descriptionAs registered.
titleA human-readable label, when the page gave one.
inputSchemaThe JSON Schema the input must match. A form tool's schema is built from its fields.
annotationsThe hints that are set: readOnly, untrustedContent (its output may carry third-party content), consequential (it acts with consequences, such as a payment), debugging, and autosubmit for a form tool that submits itself.
originThe origin of the document that registered it.
declarativetrue for a <form toolname> tool.
frameThe frame that registered it, by name and URL, as it was when listed.

page.webmcp covers the main frame, and frame.webmcp covers that frame only. Pass allFrames: true to include every descendant frame, cross-origin ones too:

const everywhere = await page.webmcp.tools({ allFrames: true });
const checkout = page.frame('checkout');
await checkout.webmcp.tools();

Calling a tool

const result = await page.webmcp.callTool('search_flights', { from: 'CAI', to: 'AMS' });

The page runs the tool itself. It may navigate, change state or wait for the user: a form tool without toolautosubmit fills its fields and then waits for a real submit. The result is whatever the tool returned, including { isError: true }, which is a result rather than a failure.

A call rejects when:

  • no tool has that name. The error lists the tools the frame does have;
  • the tool throws. The error carries the tool's own message, WebMCP tool "x" failed: Error: ..., wherever the browser reports it (see below);
  • the document navigates away, the frame detaches or the page closes while the tool runs;
  • it outlives timeout, which defaults to the page's default timeout. 0 waits without limit.

A call that times out is canceled in the page, not abandoned: the AbortSignal the tool received in its second argument aborts.

document.modelContext.registerTool({
  name: 'export_report', description: 'Builds a large report',
  execute: async (input, { signal }) => buildReport(input, { signal }),
});

With allFrames: true, callTool finds the tool in any frame, provided only one frame registers that name. Otherwise call it through the frame that owns it.

Waiting for a tool

Pages register tools when their code runs, and a <form toolname> registers on a later task than the one that inserted it. Wait for the tool rather than racing it:

await page.webmcp.waitForTool('subscribe', { timeout: 5000 });
await page.webmcp.callTool('subscribe', { email: 'sashoush@example.com' });

waitForTool resolves with the tool once it is registered. On timeout it reports that WebMCP is unavailable when the document never had it, and a plain timeout otherwise.

From the MCP server

The server exposes two tools, and lists the page's tools on its own when it navigates.

  • webmcp_tools lists the tools of every frame, with their schema, hints and frame.
  • webmcp_call calls one by name with an input object. frame (a frame name or URL from the list) picks the frame when several register the same name; timeout cancels the call in the page.

navigate and the page actions that change the page append a ### WebMCP tools section to their reply when the main frame registers any, so an agent learns a page offers tools without asking.

Backends

BackendListingCallingCancellation
cdp-pipe, cdp-rawIn an isolated world, unaffected by page scripts that replace getToolsThrough DevTools, with the tool's own error messageThe tool's signal aborts
WebDriver BiDi or Classic to Chromium (ChromeDriver)In the pageIn the page; errors read the invocation failedThe tool's signal aborts
bidi (Firefox)In a sandboxIn the pageNone: Firefox gives a tool no signal
webkitUnavailableUnavailable

Firefox also accepts tools from the top-level window only, and does not register <form toolname> tools.