DEV Community

Cover image for WebMCP: Make Your Site Callable by AI Agents (With Working Code)
Furqan Khalid
Furqan Khalid

Posted on

WebMCP: Make Your Site Callable by AI Agents (With Working Code)

Right now, when an AI agent books a table on your site, it does it the hard way. It reads the page, guesses which input is "party size", types into it, finds something that looks like a submit button, clicks, and hopes.

WebMCP is a proposal to skip the guessing. Your page tells the browser: "here are the actions I support, here's the JSON Schema for each one, call this function." The agent calls the function. No DOM archaeology.

Chrome's team announced an early preview in February 2026, the spec is being developed at the W3C with Microsoft involved, and Chrome's docs were last updated in May 2026 (Chrome for Developers). It's still experimental. That makes now a good time to prototype it and a bad time to bet a launch on it.

⚠️ Version note (October 2026): early demos used navigator.modelContext. Chrome's current docs use document.modelContext. Method names and options have shifted between Chrome versions. Check the current docs before copying anything, including from this post.


The mental model

If you've used MCP (Model Context Protocol) on the server side, WebMCP is the same idea moved into the page:

Server MCP WebMCP
Where tools live A separate MCP server process JavaScript in your page
Who calls them An AI app connected to your server An agent running in the user's browser
Auth You build it (OAuth, API keys) The user's existing session and cookies
Best for Back-end integrations Actions a logged-in user takes on your site

The second-to-last row is the important one. A WebMCP tool runs as the user, in their tab. You don't need a new auth system. You also inherit every security problem that comes with that, which we'll get to.

Comparison of an agent guessing at the DOM in five fragile steps versus calling a WebMCP tool in three explicit steps
Image: diagram by the author.

The API surface

From Chrome's imperative API docs, document.modelContext gives you:

  • registerTool(tool, options?): add a tool. Options include an AbortSignal (to unregister) and exposedTo (origins allowed to see it).
  • getTools() and executeTool(): used by the agent side, and handy for testing.
  • A toolchange event when the available tools change.

A tool definition has:

  • name: a stable identifier like search_products
  • description: plain-language text the model reads to decide when to call it
  • inputSchema: JSON Schema for the arguments
  • annotations (optional): hints such as readOnlyHint and consequentialHint
  • execute(input, { signal }): your function

There's also a declarative flavour that annotates existing <form> elements so you don't write JS at all. This post sticks to the imperative API because it's easier to reason about.

Example: a store search and add-to-cart

Here's a realistic pair of tools for an e-commerce page: one read-only, one that changes state.

// webmcp-tools.js
// Progressive enhancement: do nothing if the browser doesn't support WebMCP.
const mc = document.modelContext ?? navigator.modelContext; // older previews used navigator

if (mc?.registerTool) {
  const controller = new AbortController();

  mc.registerTool(
    {
      name: "search_products",
      description:
        "Search this store's catalogue. Returns up to 10 products with id, name, price in USD and stock status.",
      inputSchema: {
        type: "object",
        properties: {
          query: { type: "string", description: "Free-text search, e.g. 'trail running shoes'" },
          maxPrice: { type: "number", description: "Optional upper price limit in USD" },
        },
        required: ["query"],
      },
      annotations: { readOnlyHint: true },
      async execute({ query, maxPrice }, { signal }) {
        const params = new URLSearchParams({ q: query });
        if (maxPrice) params.set("max_price", String(maxPrice));

        const res = await fetch(`/api/search?${params}`, { signal });
        if (!res.ok) return `Search failed with status ${res.status}.`;

        const items = await res.json();
        return JSON.stringify(
          items.slice(0, 10).map(({ id, name, price, inStock }) => ({ id, name, price, inStock }))
        );
      },
    },
    { signal: controller.signal }
  );

  mc.registerTool(
    {
      name: "add_to_cart",
      description:
        "Add a product to the signed-in user's cart. Use a product id returned by search_products.",
      inputSchema: {
        type: "object",
        properties: {
          productId: { type: "string" },
          quantity: { type: "integer", minimum: 1, maximum: 10 },
        },
        required: ["productId"],
      },
      annotations: { consequentialHint: true },
      async execute({ productId, quantity = 1 }, { signal }) {
        const res = await fetch("/api/cart", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ productId, quantity }),
          signal,
        });
        if (!res.ok) return `Could not add item (status ${res.status}).`;

        // Keep the visible UI in sync so the human sees what the agent did.
        document.dispatchEvent(new CustomEvent("cart:updated"));
        return `Added ${quantity} × ${productId} to the cart.`;
      },
    },
    { signal: controller.signal }
  );

  // Unregister when the SPA route changes, e.g. controller.abort();
}
Enter fullscreen mode Exit fullscreen mode

A few choices worth copying:

  1. Feature-detect and exit quietly. Most browsers won't have this API for a while. The page must work without it.
  2. Reuse your existing API routes. The tool is a thin wrapper. Your server still validates everything, because the server can't tell whether a human or an agent clicked.
  3. Return small, structured results. Agents pay for every token you return. Don't send the whole product object.
  4. Update the UI. If the agent changes state, the human watching the tab should see it.

Testing it locally

Chrome exposes WebMCP behind an origin trial and/or flags, depending on version. Check the Chrome Status entry linked from the docs for your channel. Once it's on, you can poke your tools from DevTools:

const tools = await document.modelContext.getTools();
console.table(tools.map(t => ({ name: t.name, description: t.description })));

const result = await document.modelContext.executeTool(
  tools.find(t => t.name === "search_products"),
  { query: "trail shoes", maxPrice: 120 }
);
console.log(result);
Enter fullscreen mode Exit fullscreen mode

Security: treat every tool like a public API endpoint

WebMCP doesn't create new permissions. It makes existing ones easier for an automated caller to use. So:

  • Server-side authorization stays the source of truth. Never put "is this user allowed?" logic only in execute.
  • Mark consequential tools. Anything that spends money, sends messages, or deletes data should carry consequentialHint so the agent and browser can ask the user to confirm. Don't count on that, though. Add your own confirmation step for irreversible actions.
  • Don't trust strings that come back in. The agent's arguments are model output, which may have been steered by a prompt injection on another page. Validate against your schema on the server.
  • Limit cross-origin exposure. Only use exposedTo for origins you control.
  • Rate-limit. An agent can call add_to_cart 500 times faster than a human can click.

The W3C repo has open issues about tool-registration security (for example, an early provideContext() call that could clear and replace another script's tools). Third-party scripts on your page can register tools too. Audit what your tag manager loads.

So is this SEO?

Not directly. WebMCP doesn't affect crawling, indexing or ranking, and Google hasn't said anything to suggest it will. Discovery still happens the old way: an agent has to find your page through search or a link before it can call your tools. Any blog post claiming "WebMCP is a ranking factor" is guessing.

Where it does matter is the step after discovery. If an agent is choosing between two stores to complete a task, the one with a clean, callable search_products is the one that finishes the job, and probably the one that gets used again. That's conversion optimization for a new kind of user.

What I'd do this quarter

  1. Fix the basics first: semantic forms and buttons (next post in this series). They help every agent today, with or without WebMCP.
  2. Pick one or two read-only tools (search, availability, store finder) and prototype them behind a feature flag.
  3. Log tool calls separately from normal API calls so you can see whether any agent actually uses them.
  4. Wait on write tools until the spec and the browser confirmation UX settle down.

Next in the series: Your next visitor is an agent: semantic HTML is SEO again.

Top comments (0)