The shape of it
A page registers tools on document.modelContext. The browser is the host: it
holds the registry and mediates between the page and an in-browser agent. No server, no JSON-RPC, no
socket — same-origin JavaScript.spec
Older name navigator.modelContext is being renamed to document.modelContext.
Turn it on
| Mechanism | How |
| Flag (local dev) | chrome://flags/#enable-webmcp-testing → Enabled → relaunch (Canary 146+) |
| Origin trial (real site) | Register domain (Chrome 149+), embed the token — no flag for visitors |
| Check it's live | typeof document.modelContext → "object" |
Author API — register tools
| Call | Does |
registerTool(tool, opts) | Publish a tool. Returns a Promise. |
await document.modelContext.registerTool({
name: "add_todo", // 1–128 chars, [A-Za-z0-9_-.]
title: "Add to-do", // optional human label
description: "Add an item to the list", // NL prompt the agent reads
inputSchema: { type:"object", properties:{ text:{type:"string"} }, required:["text"] },
execute: async ({ text }, { signal }) => `Added: ${text}`, // returns a string
annotations: { readOnlyHint:false, untrustedContentHint:false } // optional hints
}, { signal: controller.signal, exposedTo: ["https://trusted.example"] });
controller.abort(); // ← unregister the tool (no remove() method)
Tool descriptor fields
| Field | Role |
name | Stable id the agent calls (1–128 chars). |
description | Natural language — the agent reads this to pick the tool. Write it well. |
inputSchema | JSON Schema for the arguments; browser validates input against it. |
execute | async (input, {signal}) => result. Your handler; returns a string (current Chrome). |
title | Optional human-readable label. |
annotations | readOnlyHint, untrustedContentHint — safety signals. |
Return-shape churn: current Chrome returns a plain string from
execute; earlier drafts used {content:[{type:"text",text:…}]} (MCP-style).
Consumer API — discover & call
| WebMCP | MCP analogue | Does |
getTools({fromOrigins}) | tools/list | List registered tools as RegisteredTool records. |
executeTool(name, input, {signal}) | tools/call | Run a tool; returns its result. |
toolchange event | list-changed notification | Fires when the registry changes. |
// discover, then call
const tools = await document.modelContext.getTools(); // [{name, description, inputSchema, origin, …}]
const out = await document.modelContext.executeTool("add_todo", { text:"buy milk" });
// out === "Added: buy milk"
Tooling — the consumer stand-in
| Tool | Use |
| Model Context Tool Inspector (extension) | Lists a page's tools, calls them by hand, validates JSON Schemas; NL prompts default to gemini-3-flash-preview.chrome |
| Gemini in Chrome | The production in-browser agent that consumes WebMCP tools. |
| Raw API | getTools / executeTool in the DevTools console. |
WebMCP vs MCP vs CDP
| Where tools live | Host | Transport |
| MCP | Remote server | MCP client process | JSON-RPC over HTTP/stdio |
| CDP | — (agent scrapes/drives) | External driver | Raw WebSocket to debug port |
| WebMCP | In the page (same origin) | The browser | In-process JS API |
Trust
Tools are origin-scoped. exposedTo limits which origins may call a
tool; the Permissions-Policy "tools" feature (default ['self']) governs
whether a frame may register at all. Treat execute input as untrusted — an agent may be
steered by injected content. Validate intent, not just schema shape.