DEV Community

Ali Suleyman TOPUZ
Ali Suleyman TOPUZ

Posted on Originally published at topuzas.Medium on

I Tried Building With WebMCP Before Gemini in Chrome Could Even Call It

You’ve probably seen a few posts by now telling you WebMCP is “the future of AI-native websites.” Most of them read the same way: a paragraph on what MCP is, a paragraph on how agents currently scrape HTML like barbarians, a bold claim that WebMCP fixes all of it, and not a single line of code. I read three of these back to back last week and still couldn’t have told you what document.modelContext actually looks like.

So I spent a weekend building an actual tool with it, testing it two different ways, and reading the spec repo instead of the summaries of the summaries. Here’s what’s real, what’s still wobbly, and where I landed on whether this is worth your time right now.

What WebMCP actually is

WebMCP is a browser API, currently a Draft Community Group Report under the webmachinelearning group (with people from Google and Microsoft driving it), that lets a web page register structured “tools” an AI agent can call directly, instead of the agent taking screenshots and guessing which

is a button.

The name is deliberate. It borrows the vocabulary of Anthropic’s Model Context Protocol, tools, schemas, structured results, but it isn’t the same protocol running in your browser. Backend MCP is about a client talking to a server over stdio or SSE. WebMCP is about a page exposing tools to whatever agent is sitting in the same browser tab, using the user’s existing session. The spec is explicit that it’s meant to complement backend MCP, not replace it. If you already expose an MCP server for your product, WebMCP is the client-side sibling, not a rewrite.

Here’s the part almost none of the explainer posts mention, because most of them were written before it happened: on July 21, 2026, the API moved from navigator.modelContext to document.modelContext. If you're reading an older tutorial (including, ironically, some published this year), the code won't run. First thing I'd check in any WebMCP article going forward is whether it uses document. or navigator.. It's a small thing, but it's the kind of detail that tells you whether someone actually opened DevTools or just summarized a blog post.

Setting it up locally

You don’t need an origin trial token to poke at this yourself. There’s a dedicated testing flag:

chrome://flags/#enable-webmcp-testing

You’ll need Chrome 146 or newer (Canary if your stable channel hasn’t caught up yet). Enable the flag, relaunch, and check the console:

console.log(document.modelContext ?? navigator.modelContext);

If you get an object back instead of undefined, you're in business. Production sites go through the real origin trial instead, which opened with Chrome 149 and runs through 156 as of this writing, but for learning the API the flag is faster and doesn't require registering a domain.

Building an actual tool

I built the most boring possible example on purpose, a to-do list, because I wanted to see the mechanics, not be distracted by a clever demo. There are two ways to register a tool: imperative (JavaScript) and declarative (HTML attributes on an existing form). I tried both.

Imperative

document.modelContext.registerTool({
  name: "addTodo",
  description: "Add a new item to the user's to-do list. Use when the user asks to create, add, or remember a task.",
  inputSchema: {
    type: "object",
    properties: {
      text: {
        type: "string",
        description: "The task description"
      },
      priority: {
        type: "string",
        enum: ["low", "medium", "high"],
        description: "Task priority level. Default to 'medium' if not specified by the user."
      }
    },
    required: ["text"]
  },
  execute: ({ text, priority = "medium" }) => {
    const newItem = { id: Date.now(), text, priority, done: false };
    todoApp.addItem(newItem);
    todoApp.renderList();
    return {
      content: [{
        type: "text",
        text: `Added task: "${text}" with ${priority} priority.`
      }]
    };
  }
});

The detail that actually matters here: update the UI before you return the result. Agents seem to check page state to confirm something happened, not just trust the return value. That tripped me up on my first pass, my tool returned a success message while the list on screen hadn’t re-rendered yet, and the inspector extension I was testing with flagged it as inconsistent.

Declarative

If you already have a normal HTML form, you can skip the JavaScript API entirely and annotate the markup:

<form toolname="search_products"
      tooldescription="Search the product catalog by keyword and optional category filter"
      action="/search" method="GET">
      <label for="query">Search term</label>
  <input type="text" name="query" id="query" required
         toolparamdescription="The keyword or phrase to search for in product titles and descriptions">
  <label for="category">Category</label>
  <select name="category" id="category"
          toolparamtitle="Product Category"
          toolparamdescription="Filter results to a specific product category. Use 'all' for no filter.">
    <option value="all">All categories</option>
    <option value="electronics">Electronics</option>
    <option value="books">Books</option>
  </select>
  <button type="submit">Search</button>
</form>

The browser builds the JSON Schema for you from the form structure, including enum values pulled straight from your options. You can intercept the submission and branch on whether an agent or a human triggered it: document.querySelector("form").addEventListener("submit", (e) => { e.preventDefault(); const query = new FormData(e.target).get("query"); if (!query || query.trim().length === 0) { if (e.agentInvoked) { e.respondWith(Promise.resolve({ error: "Search query cannot be empty. Please provide a keyword." })); } return; } const results = performSearch(query); if (e.agentInvoked) { e.respondWith(Promise.resolve({ content: [{ type: "text", text: JSON.stringify(results) }] })); } }); Honestly, the declarative approach is the one I’d reach for first on an existing site. You’re not building a parallel API surface, you’re annotating the form you already have. The imperative version is better when the “tool” doesn’t correspond to a form at all, like my to-do example, or when you want tighter control over the response shape. Testing it without waiting for Gemini in Chrome The official way to test is the Model Context Tool Inspector extension from the Chrome Web Store. You point it at your page, it lists your registered tools, and you can either fill in JSON parameters by hand or paste a Gemini API key so it can turn a natural-language request into a tool call for you. I didn’t want to wire a cloud API key into a local testing loop just to check whether my schema was sane, so I built a small local alternative using Playwright and Ollama instead. If you’d rather not depend on Gemini at all, this is a decent stand-in, and it’s a nice pattern for CI later, no external API, no rate limits, runs entirely on your machine. Pull a model with decent tool-calling support first: ollama pull qwen2.5:7b ollama serve On the page side, I kept a parallel lookup table alongside the real registration, since there’s currently no DevTools command to invoke a tool by name once it’s registered, only the browser’s own agent-facing path can call it: window. __webmcpTools = window.__ webmcpTools || {}; function defineTool(tool) { const ctx = document.modelContext ?? navigator.modelContext; ctx.registerTool(tool); window.__webmcpTools[tool.name] = tool; // so we can invoke it manually while testing } defineTool({ name: "addTodo", description: "Add a new item to the user's to-do list.", inputSchema: { type: "object", properties: { text: { type: "string", description: "The task description" }, priority: { type: "string", enum: ["low", "medium", "high"] } }, required: ["text"] }, execute: ({ text, priority = "medium" }) => { todoApp.addItem({ id: Date.now(), text, priority, done: false }); todoApp.renderList(); return { content: [{ type: "text", text: `Added task: "${text}" with ${priority} priority.` }] }; } }); Then a small bridge script drives the browser, asks the local model which tool to call, and executes it inside the real page (start Chrome yourself with --remote-debugging-port=9222 so Playwright can attach to it): // bridge.mjs import { chromium } from "playwright"; const OLLAMA_URL = "http://localhost:11434/api/chat"; const MODEL = "qwen2.5:7b"; async function main() { const browser = await chromium.connectOverCDP("http://localhost:9222"); const [context] = browser.contexts(); const page = context.pages()[0] ?? (await context.newPage()); await page.goto("http://localhost:5500/todo.html"); const tools = await page.evaluate(() => Object.values(window.__webmcpTools).map((t) => ({ type: "function", function: { name: t.name, description: t.description, parameters: t.inputSchema } })) ); const userRequest = process.argv[2] ?? "add a high priority task to renew my passport"; const res = await fetch(OLLAMA_URL, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ model: MODEL, messages: [{ role: "user", content: userRequest }], tools, stream: false }) }); const data = await res.json(); const call = data.message.tool_calls?.[0]; if (!call) { console.log("Model didn't pick a tool:", data.message.content); return; } const result = await page.evaluate( ({ name, args }) => window.__webmcpTools[name].execute(args), { name: call.function.name, args: call.function.arguments } ); console.log("Tool result:", result); await browser.close(); } main(); Run node bridge.mjs "add a high priority task to renew my passport" and watch the actual to-do list on screen update. Nothing about this is production-grade, it's a dev loop, not a runtime, but it let me iterate on my schema and descriptions in about two seconds per change instead of round-tripping through a hosted model every time. The part that actually worries me Every one of these tools runs inside the user’s real session, with their real cookies and their real auth. That’s the whole point, an agent doesn’t need a separate service account, it just uses what’s already loaded in the tab. It’s also the part I keep coming back to as the risk. If a page has a prompt injection surface anywhere (a product description, a review, a support ticket an agent is asked to summarize), and that page also exposes a WebMCP tool that changes state, you’ve built a fairly direct path from “text an agent reads” to “action an agent takes with the user’s own credentials.” A tool description is also just… text a model reads and trusts. A misleading description field is a legitimate attack vector, not a hypothetical one. None of the mitigations here are exotic, they’re the same discipline you’d apply to any authenticated API: scope tools narrowly, put a confirmation gate in front of anything that mutates data or spends money, validate input server-side even though the schema already validated it client-side, and log every agent-invoked call separately from human ones so you can actually audit what happened. The agentInvoked flag on form submissions is useful exactly for this, you can require confirmation only on the agent path and leave the human flow untouched. Is this worth adopting now? Here’s my honest read, not the “this changes everything” framing you’ll see elsewhere. WebMCP has a bootstrapping problem. Chrome’s origin trial is live and a few travel sites (Expedia and Booking.com among them) are piloting it, but no mainstream consumer agent, not Claude, not ChatGPT, not Perplexity, actually calls these tools yet. Google has said Gemini in Chrome will be the first real consumer. Until that ships, you’re registering tools that nothing calls, which means there’s no market pressure yet to register them at all. That’s not a reason to ignore it. It’s a reason to prototype now, on a low-stakes surface, while the cost of being wrong is small, rather than rewrite your product around it. Here’s how I’d frame the comparison against what you’re probably already doing: +----------------------+---------------------------+---------------------------+----------------------------+ | Approach | Who calls it | Auth model | Maturity (Aug 2026) | +----------------------+---------------------------+---------------------------+----------------------------+ | Screen scraping / | Any browser-automation | Reuses whatever session | Works today, brittle, | | vision-based agents | agent, no cooperation | the browser already has | breaks on every redesign | | | needed from the site | | | +----------------------+---------------------------+---------------------------+----------------------------+ | Backend MCP server | Any MCP client, agent | Server-issued credentials,| Stable, widely adopted, | | | doesn't need a browser | separate from end-user | requires you to build and | | | at all | session | host a server | +----------------------+---------------------------+---------------------------+----------------------------+ | WebMCP | Agents running inside | Inherits the user's real | Draft spec, Chrome-only | | | the same browser tab | browser session | origin trial, effectively | | | (Chrome only, for now) | | zero consumers today | +----------------------+---------------------------+---------------------------+----------------------------+ If you already run a backend MCP server, WebMCP doesn’t replace it, it just gives you a second, cooperative path for agents that happen to be sitting in a browser with your page already open, form pre-filled, session already authenticated. That’s a genuinely different use case than a headless agent hitting your API. My actual plan: I’m adding declarative tool annotations to one form on a side project this week, the one with the least blast radius if something goes wrong, and leaving it there to see what, if anything, calls it once Gemini in Chrome ships. I’m not touching anything that moves money or changes account state until the spec stabilizes past Draft Community Group Report and I can see real traffic hitting it. If you’re earlier than that in your own stack, I wouldn’t feel behind. Right now, nothing is behind, because almost nothing is here to call the tools yet. Tags: WebMCP, AI Agents, Chrome, JavaScript, Web Development, Model Context Protocol, Ollama

Top comments (0)