TL;DR

WebMCP is Google’s proposed web standard that lets websites tell AI agents exactly what they can do — no screen-scraping, no brittle CSS selectors. You annotate HTML forms with a few attributes or register JavaScript functions through navigator.modelContext, and browser-based agents call your tools directly. Chrome 149’s origin trial opened in May 2026 and early third-party benchmarks report 8–12x faster task completion compared to vision-based agents on the same page. This tutorial walks you through both APIs with working code you can test today.

Why WebMCP Changes Browser-Agent Interaction

I spent a week building a Chrome extension that automated form filling on a client’s internal dashboard. The extension broke three times in five days because the frontend team renamed CSS classes, swapped a <select> for a custom dropdown, and changed the form’s action URL. Every browser agent (Gemini, OpenClaw, custom Playwright scripts) faces the same problem: they treat websites like screenshots to be visually parsed rather than APIs to be called.

WebMCP attacks that problem at the root. Instead of forcing agents to reverse-engineer your UI from DOM nodes and pixel coordinates, your website declares structured tools that agents call directly. A form becomes a callable function with typed parameters. A JavaScript handler becomes an API endpoint that lives in the browser tab.

Google announced the standard at I/O 2026 and opened the origin trial in Chrome 149. Early adopters include Expedia, Booking.com, Shopify, Etsy, Instacart, and Target. The only agent that currently consumes WebMCP tools natively is Gemini in Chrome (if you haven’t tried Google’s CLI counterpart yet, see our Gemini CLI tutorial), but the standard is open and browser-agnostic by design.

8–12x
Faster agent task completion
Chrome 149
Origin trial live now
2 APIs
Declarative HTML + imperative JS

What You’ll Build

This tutorial builds a small recipe-search app that exposes three WebMCP tools:

  1. search_recipes — a declarative HTML form that agents can query by ingredient and cuisine
  2. add_to_meal_plan — an imperative JavaScript tool that adds a recipe to the user’s weekly plan
  3. get_meal_plan — a read-only tool that returns the current meal plan as structured data

By the end you’ll have a page where Gemini in Chrome (or any future WebMCP-compatible agent) can search for recipes, add them to a plan, and read the plan back, all without touching a single button.

Prerequisites

  • Chrome 149+ (Canary or Dev channel as of May 2026)
  • Enable the flag: navigate to chrome://flags/#enable-webmcp-testing and set it to Enabled
  • A basic HTML file, no build tools or frameworks needed

If you don’t have Chrome 149 yet, you can still follow along and test the tool registration with the WebMCP Model Context Tool Inspector extension, which lets you invoke tools manually with hand-written JSON parameters.

Part 1: Declarative API — Annotate Your HTML Forms

The declarative API is the fastest path. If your website already has HTML forms, you can make them agent-readable by adding three attributes. No JavaScript required. Here’s what each attribute does:

AttributeWherePurpose
toolname<form>Unique identifier the agent uses to invoke this tool
tooldescription<form>Natural language description of what the tool does
toolparamdescription<input>, <select>, <textarea>Explains each parameter’s purpose to the agent
toolautosubmit<form>Skips the user confirmation step (use only for read-only searches)

The browser reads these attributes and auto-generates a JSON schema from the form structure. Enum values come from <select> options, required comes from HTML’s native required attribute, and types are inferred from <input type>.

Building the Recipe Search Form

Create an index.html file:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Recipe Finder — WebMCP Demo</title>
  <style>
    body { font-family: system-ui, sans-serif; max-width: 640px; margin: 2rem auto; padding: 0 1rem; }
    form { display: flex; flex-direction: column; gap: 0.75rem; }
    label { font-weight: 600; }
    input, select, button { padding: 0.5rem; font-size: 1rem; }
    button { cursor: pointer; background: #1a73e8; color: white; border: none; border-radius: 4px; }
    #results { margin-top: 1.5rem; }
    .recipe-card { border: 1px solid #ddd; padding: 1rem; border-radius: 8px; margin-bottom: 0.75rem; }
  </style>
</head>
<body>
  <h1>Recipe Finder</h1>

  <form toolname="search_recipes"
        tooldescription="Search the recipe catalog by ingredient and optional cuisine filter. Returns matching recipes with titles, ingredients, and prep times."
        toolautosubmit
        action="/search" method="GET"
        id="searchForm">

    <label for="ingredient">Main Ingredient</label>
    <input type="text" name="ingredient" id="ingredient" required
           toolparamdescription="The primary ingredient to search for (e.g., 'chicken', 'tofu', 'salmon')">

    <label for="cuisine">Cuisine</label>
    <select name="cuisine" id="cuisine"
            toolparamdescription="Filter results to a specific cuisine. Use 'any' for no filter.">
      <option value="any">Any cuisine</option>
      <option value="italian">Italian</option>
      <option value="japanese">Japanese</option>
      <option value="mexican">Mexican</option>
      <option value="indian">Indian</option>
      <option value="thai">Thai</option>
    </select>

    <button type="submit">Search</button>
  </form>

  <div id="results"></div>
</body>
</html>

The declarative setup is done. The toolname="search_recipes" tells the browser this form is a callable tool. The tooldescription gives the agent enough context to know when and how to use it. Each field’s toolparamdescription explains the parameter semantics in natural language. The toolautosubmit attribute is appropriate here because searching is a read-only operation and no data gets modified.

When an agent calls this tool, the browser fills in the form fields, submits it as a normal GET request, and returns the page result. The agent doesn’t need to know which CSS class the search button has or what the form’s action URL is. It just says: “call search_recipes with ingredient: chicken, cuisine: thai.”

What the Browser Generates

Behind the scenes, Chrome translates the annotated form into a JSON schema equivalent:

{
  "name": "search_recipes",
  "description": "Search the recipe catalog by ingredient and optional cuisine filter. Returns matching recipes with titles, ingredients, and prep times.",
  "inputSchema": {
    "type": "object",
    "properties": {
      "ingredient": {
        "type": "string",
        "description": "The primary ingredient to search for (e.g., 'chicken', 'tofu', 'salmon')"
      },
      "cuisine": {
        "type": "string",
        "enum": ["any", "italian", "japanese", "mexican", "indian", "thai"],
        "description": "Filter results to a specific cuisine. Use 'any' for no filter."
      }
    },
    "required": ["ingredient"]
  }
}

You never write this schema yourself. The browser infers it from the HTML. The <select> options become an enum, the required attribute becomes a required entry, and each toolparamdescription populates the corresponding description field. This is how agents discover your tools without you maintaining a separate API spec.

Part 2: Imperative API — Register JavaScript Tools

The declarative API works for existing forms. But many interactions aren’t forms: adding an item to a cart, toggling a setting, fetching data from client-side state. For these, the imperative API lets you register arbitrary JavaScript functions as tools.

WebMCP lives under navigator.modelContext. Always guard against environments where it doesn’t exist:

function getModelContext() {
  if (typeof navigator !== "undefined" && navigator.modelContext) {
    return navigator.modelContext;
  }
  return null;
}

Registering the Meal Plan Tools

Add this script block before the closing </body> tag in your index.html:

<script>
  // In-memory meal plan state
  const mealPlan = [];

  const mc = navigator.modelContext ?? null;

  if (mc) {
    // Tool 1: Add a recipe to the meal plan
    mc.registerTool({
      name: "add_to_meal_plan",
      description: "Add a recipe to the weekly meal plan. Specify the recipe name and the day of the week.",
      inputSchema: {
        type: "object",
        properties: {
          recipe: {
            type: "string",
            description: "The name of the recipe to add"
          },
          day: {
            type: "string",
            enum: ["monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"],
            description: "The day of the week to schedule this recipe"
          }
        },
        required: ["recipe", "day"]
      },
      execute: ({ recipe, day }) => {
        const existing = mealPlan.findIndex(m => m.day === day);
        if (existing !== -1) {
          mealPlan[existing] = { recipe, day };
        } else {
          mealPlan.push({ recipe, day });
        }
        renderMealPlan();
        return {
          content: [{
            type: "text",
            text: `Scheduled "${recipe}" for ${day}.`
          }]
        };
      }
    });

    // Tool 2: Read the current meal plan
    mc.registerTool({
      name: "get_meal_plan",
      description: "Returns the current weekly meal plan as a list of day-recipe pairs.",
      inputSchema: {
        type: "object",
        properties: {}
      },
      execute: () => {
        if (mealPlan.length === 0) {
          return {
            content: [{
              type: "text",
              text: "The meal plan is empty. Use add_to_meal_plan to add recipes."
            }]
          };
        }
        const sorted = [...mealPlan].sort((a, b) => {
          const days = ["monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"];
          return days.indexOf(a.day) - days.indexOf(b.day);
        });
        return {
          content: [{
            type: "text",
            text: JSON.stringify(sorted, null, 2)
          }]
        };
      }
    });

    console.log("[WebMCP] Registered: add_to_meal_plan, get_meal_plan");
  } else {
    console.log("[WebMCP] navigator.modelContext not available — tools not registered");
  }

  function renderMealPlan() {
    let container = document.getElementById("meal-plan");
    if (!container) {
      container = document.createElement("div");
      container.id = "meal-plan";
      container.innerHTML = "<h2>Meal Plan</h2>";
      document.body.appendChild(container);
    }
    const list = mealPlan
      .sort((a, b) => {
        const days = ["monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"];
        return days.indexOf(a.day) - days.indexOf(b.day);
      })
      .map(m => `<li><strong>${m.day}:</strong> ${m.recipe}</li>`)
      .join("");
    container.innerHTML = `<h2>Meal Plan</h2><ul>${list}</ul>`;
  }
</script>

The execute callback does the actual work. It receives the parameters as a plain object matching the inputSchema, modifies the application state, updates the UI, and returns a structured response. The agent reads that response to confirm the action succeeded.

One thing that tripped me up: update the UI before returning the response. Agents verify success by checking the page state after a tool call. If you return “Added chicken curry for Tuesday” but the meal plan list doesn’t update visually, the agent may retry or report failure. The renderMealPlan() call before the return statement handles this.

Part 3: Detecting Agent Interactions

WebMCP includes safety mechanisms that let you distinguish agent-triggered actions from human ones. You need this for rate limiting, analytics, and authorization checks. If you’re giving agents deeper browser access with tools like Chrome DevTools MCP, this detection becomes even more important.

The agentInvoked Property

When an agent submits a form through the declarative API, the submit event carries an agentInvoked flag:

document.getElementById("searchForm").addEventListener("submit", (e) => {
  if (e.agentInvoked) {
    console.log("Agent submitted the search form");
    // Apply rate limits, log separately, adjust behavior
  } else {
    console.log("Human submitted the search form");
  }
});

The browser also fires two events on the window object:

window.addEventListener("toolactivated", (e) => {
  console.log(`Tool "${e.toolName}" was invoked by an agent`);
});

window.addEventListener("toolcancel", (e) => {
  console.log(`Tool "${e.toolName}" was cancelled`);
});

Chrome 149 also adds two CSS pseudo-classes that activate while an agent interacts with a form:

form:tool-form-active {
  outline: 2px solid #1a73e8;
  background: rgba(26, 115, 232, 0.05);
}

button:tool-submit-active {
  opacity: 0.7;
  pointer-events: none;
}

These give users a visual cue that an agent is working. The :tool-form-active state applies while the agent fills fields; :tool-submit-active fires during submission. I found the outline effect useful during testing — it made it obvious which form Gemini was interacting with when multiple tools were registered on the same page.

Part 4: Dynamic Tool Lifecycle in React

In a React app, tools should register when a component mounts and unregister when it unmounts. Tools should also reflect the current application state — you wouldn’t want a checkout_cart tool to be callable when the cart is empty.

import { useEffect } from "react";

function CartPanel({ items, onCheckout }) {
  useEffect(() => {
    const mc = navigator.modelContext ?? null;
    if (!mc || items.length === 0) return;

    mc.registerTool({
      name: "checkout_cart",
      description: "Complete the purchase for all items in the cart.",
      inputSchema: { type: "object", properties: {} },
      execute: async () => {
        onCheckout();
        return {
          content: [{ type: "text", text: `Order placed for ${items.length} items.` }]
        };
      }
    });

    return () => mc.unregisterTool("checkout_cart");
  }, [items.length, onCheckout]);

  // ... render cart UI
}

The tool only exists while the cart has items. When items drop to zero the cleanup function fires, removing checkout_cart from the agent’s available tools. This prevents agents from attempting impossible actions, and since tools are tab-bound, they disappear when the user navigates away from the page.

Declarative vs Imperative: When to Use Which

Declarative (HTML)Imperative (JS)
Best forExisting HTML forms, search pages, filtersDynamic actions, client-side state, SPAs
Setup effortAdd 2-4 attributes to existing markupWrite registerTool config + execute function
SchemaAuto-generated from form structureYou define inputSchema manually
LifecycleTied to form presence in the DOMYou control register/unregister timing
Auto-submittoolautosubmit for read-only formsNot applicable — you handle execution
Framework supportAny HTML — no JS requiredReact, Vue, vanilla JS
Agent detectione.agentInvoked on submit eventsReturn value from execute

My recommendation: start with the declarative API for any existing forms on your site. It takes five minutes and covers the most common agent interaction pattern (fill a form, submit it, read the result). Move to the imperative API when you need tools that don’t map to forms: state mutations, API calls, client-side computations.

Testing Your Implementation

Three ways to test without waiting for full Gemini integration:

1. Chrome DevTools Console

Check that your tools registered correctly:

// List all registered tools
navigator.modelContext.getTools().then(tools => {
  tools.forEach(t => console.log(t.name, t.description));
});

2. WebMCP Model Context Tool Inspector Extension

Install the inspector extension from the Chrome Web Store. It provides a panel that lists all registered tools on the current page, lets you invoke them with hand-written JSON parameters, and shows the return value. This is the fastest way to debug your inputSchema and execute logic without a live agent.

3. Gemini in Chrome

If your Chrome 149 instance has Gemini integration enabled, open the page and ask Gemini to interact with it. For the recipe demo above, try: “Search for chicken recipes in Thai cuisine, then add the first result to my meal plan for Wednesday.”

Security: What to Lock Down

WebMCP tools run with the same privileges as your page’s JavaScript. An agent calling add_to_meal_plan executes the same code a human click would. That means your existing server-side validation, CSRF protection, and rate limiting still apply, and they should.

Three rules I follow:

  1. Validate inside execute even though you defined an inputSchema. Schemas tell agents what to send. Validation ensures they sent something sane. Don’t trust agent input more than human input.

  2. Use toolautosubmit only for read-only operations. Any form that modifies data (deletes, purchases, account changes) should require user confirmation. Skipping confirmation on a destructive action is the same mistake that made the Copilot Cowork file exfiltration possible.

  3. Check e.agentInvoked and apply stricter throttling for agent calls. An agent can invoke tools far faster than a human clicks buttons.

What Browsers Support WebMCP Today

As of May 2026, only Chrome has an implementation. The origin trial in Chrome 149 is the first time the standard is testable on production traffic. Chrome Canary 146+ has had it behind a flag since early 2026.

No other browser has announced WebMCP support. Firefox and Safari haven’t published positions on the proposal. The standard is open (not proprietary to Google), which makes cross-browser adoption possible in theory, but there’s no timeline for it.

The Chrome team has discussed a .well-known/webmcp manifest file for pre-visit discovery (letting agents know what tools a site offers before loading the page), but nothing along those lines is specified yet.

FAQ

What is WebMCP and how does it differ from MCP?

MCP (Model Context Protocol) is a server-side standard where an MCP server exposes tools to AI clients over a transport layer (stdio, HTTP/SSE). WebMCP brings the same concept to the browser: the website itself declares tools that browser-based agents can call, without a backend MCP server. MCP runs server-to-server. WebMCP runs page-to-agent inside a browser tab.

How do I implement WebMCP on my existing website?

For existing HTML forms, add toolname and tooldescription to the <form> tag, then add toolparamdescription to each input field. For JavaScript-driven interactions, use navigator.modelContext.registerTool() with a name, description, input schema, and execute function. Both APIs can coexist on the same page.

Which browsers support WebMCP?

Only Chrome 149+ (origin trial) as of May 2026. Chrome Canary 146+ supports it behind the chrome://flags/#enable-webmcp-testing flag. No other browser has an implementation. The standard is open but cross-browser adoption doesn’t have a timeline.

Is WebMCP safe? Can agents take destructive actions?

WebMCP tools run the same code a human click would trigger. Destructive actions (purchases, deletes, account modifications) should not use toolautosubmit because the user confirmation step is the safety mechanism. Server-side validation, CSRF tokens, and rate limiting apply the same way they do for human interactions. Use e.agentInvoked to detect and separately throttle agent-triggered actions.

Can I use WebMCP with React, Vue, or other frameworks?

The imperative API (navigator.modelContext.registerTool()) works with any JavaScript framework. In React, register tools inside useEffect and unregister in the cleanup function. In Vue, use onMounted and onUnmounted. The declarative API works in any templating system that outputs standard HTML.

Sources

Bottom Line

WebMCP solves a real problem I’ve hit repeatedly: making websites machine-readable without maintaining a separate API surface. The declarative API is a five-minute add for existing forms. The imperative API gives you full control for anything more complex. The standard is early (Chrome only, Gemini only, no .well-known discovery yet) but the direction is right. Websites that expose WebMCP tools now get a head start once more agents ship with support. Start with your search forms, add toolname and tooldescription, and see what happens when Gemini can actually call your code instead of guessing at your UI.