helloandy.net Research

How to Add WebMCP to Your Website

By Andy · March 14, 2026 · 14 min read

Your website already does useful things. It searches products, fetches data, processes forms, runs calculations. But right now, the only way an AI agent can access any of that is by rendering your page, screenshotting it, and guessing where to click. That's expensive, slow, and breaks every time you change a button's position.

WebMCP gives your site a way to tell agents exactly what it can do. You register tools — named functions with typed parameters — and any browser-based agent can discover and call them directly. No DOM scraping. No pixel hunting. Just structured input and structured output.

This guide walks through the full implementation process: from your first navigator.modelContext.registerTool() call to testing your tools and getting listed on webmcplist.com. If you want background on what WebMCP is and why it exists, read What is WebMCP? first. This article assumes you're ready to write code.

1. Quick Overview: What WebMCP Does

WebMCP adds a new API to the browser: navigator.modelContext. Your JavaScript calls navigator.modelContext.registerTool() with a tool definition, and any AI agent running in that browser can discover and invoke it. The tool definition includes a name, a human-readable description, a JSON Schema for inputs, and an async handler function that does the actual work.

When an agent visits your page, it reads the registered tools, picks the right one based on the description, calls it with structured parameters, and gets JSON back. The entire exchange happens inside the browser session — the user's cookies, auth tokens, and permissions all apply automatically.

That's it. The rest of this article is about doing it well.

2. Prerequisites

Before you start writing WebMCP code, you'll need a few things in place.

If you need to support browsers that don't have native WebMCP, the MCP-B polyfill provides navigator.modelContext as a drop-in shim. It's a small script that adds the API surface so your tools work everywhere.

3. Step 1: Register Your First Tool

Every WebMCP integration starts with navigator.modelContext.registerTool(). This function takes a single object with four properties:

Here's the simplest possible example:

navigator.modelContext.registerTool({
  name: "get-site-info",
  description: "Returns basic information about this website, " +
               "including the site name, page count, and contact email.",
  inputSchema: {
    type: "object",
    properties: {}
  },
  execute: async () => {
    return {
      siteName: "My Website",
      pageCount: 42,
      contactEmail: "hello@example.com"
    };
  }
});

This tool takes no parameters and returns a static object. Not very useful on its own, but it proves the wiring works. An agent visiting your page would see a tool called get-site-info, call it, and get back clean JSON.

4. Step 2: Define Input Schemas

Most tools need parameters. The inputSchema property uses standard JSON Schema — the same format that OpenAI, Anthropic, and Google use for function calling. If you've defined tool schemas for any LLM API, this will look familiar.

inputSchema: {
  type: "object",
  properties: {
    query: {
      type: "string",
      description: "Search keywords to match against product names"
    },
    category: {
      type: "string",
      description: "Filter results to a specific category",
      enum: ["electronics", "clothing", "books", "home", "garden"]
    },
    maxPrice: {
      type: "number",
      description: "Maximum price in USD. Omit for no price limit."
    },
    inStock: {
      type: "boolean",
      description: "If true, only return products currently in stock"
    }
  },
  required: ["query"]
}

A few things to notice. The description on each property matters — agents read these to understand what to pass. The enum constraint on category tells the agent exactly which values are valid. And required is an array at the schema level, not on individual properties.

Keep your schemas tight. Don't accept free-form strings where an enum would work. Don't make everything optional if some parameters are genuinely needed. The more constrained your schema, the more likely the agent will call your tool correctly on the first try.

5. Step 3: Write the Handler Function

The execute function is where your tool does its work. It receives the validated input as its argument and should return a plain JavaScript object (which gets serialized to JSON for the agent).

Your handler can do anything a normal JavaScript function can do: call your backend API, query localStorage, manipulate the DOM, or compute something locally. Here's a handler that calls a REST API:

execute: async ({ query, category, maxPrice, inStock }) => {
  const params = new URLSearchParams({ q: query });
  if (category) params.set("category", category);
  if (maxPrice) params.set("max_price", maxPrice);
  if (inStock) params.set("in_stock", "true");

  const response = await fetch(`/api/products?${params}`);
  if (!response.ok) {
    return { error: "Search failed", status: response.status };
  }

  const data = await response.json();
  return {
    resultCount: data.products.length,
    products: data.products.map(p => ({
      name: p.name,
      price: p.price,
      category: p.category,
      inStock: p.available,
      url: `/products/${p.slug}`
    }))
  };
}

Some important conventions. Always return structured data — never return HTML strings or raw text dumps. Include an error field when something goes wrong instead of throwing exceptions. And only return data the agent actually needs. A product listing doesn't need the full description, review history, and warehouse location. Name, price, and a URL are enough.

6. Step 4: Complete Working Example

Let's build something real. Here's a complete weather widget tool that an agent could call to get the current forecast for any city. This example uses the Open-Meteo API, which is free and doesn't require an API key.

// Weather forecast tool — complete working example
navigator.modelContext.registerTool({
  name: "get-weather-forecast",
  description: "Get the current weather and 3-day forecast for a city. " +
               "Returns temperature, conditions, wind speed, and humidity. " +
               "Use this when a user asks about weather in a specific location.",
  inputSchema: {
    type: "object",
    properties: {
      city: {
        type: "string",
        description: "City name, e.g. 'London' or 'New York'"
      },
      units: {
        type: "string",
        description: "Temperature units",
        enum: ["celsius", "fahrenheit"]
      }
    },
    required: ["city"]
  },
  execute: async ({ city, units = "celsius" }) => {
    // Step 1: Geocode the city name to coordinates
    const geoUrl = "https://geocoding-api.open-meteo.com/v1/search" +
                   `?name=${encodeURIComponent(city)}&count=1`;
    const geoRes = await fetch(geoUrl);
    const geoData = await geoRes.json();

    if (!geoData.results || geoData.results.length === 0) {
      return { error: `City not found: ${city}` };
    }

    const { latitude, longitude, name, country } = geoData.results[0];

    // Step 2: Fetch weather data
    const tempUnit = units === "fahrenheit" ? "fahrenheit" : "celsius";
    const wxUrl = "https://api.open-meteo.com/v1/forecast" +
      `?latitude=${latitude}&longitude=${longitude}` +
      `¤t=temperature_2m,relative_humidity_2m,` +
      `wind_speed_10m,weather_code` +
      `&daily=temperature_2m_max,temperature_2m_min,weather_code` +
      `&temperature_unit=${tempUnit}&forecast_days=3`;

    const wxRes = await fetch(wxUrl);
    const wx = await wxRes.json();

    return {
      location: `${name}, ${country}`,
      current: {
        temperature: wx.current.temperature_2m,
        unit: tempUnit,
        humidity: wx.current.relative_humidity_2m,
        windSpeed: wx.current.wind_speed_10m,
        windUnit: "km/h"
      },
      forecast: wx.daily.time.map((date, i) => ({
        date,
        high: wx.daily.temperature_2m_max[i],
        low: wx.daily.temperature_2m_min[i]
      }))
    };
  }
});

Drop this into a <script> tag on any page, enable the WebMCP flag in Chrome Canary, and you've got a working tool. An agent can call get-weather-forecast with {"city": "Tokyo", "units": "celsius"} and get back structured temperature data. No screen scraping involved.

Why Open-Meteo? It's free, has no API key requirement, and returns clean JSON. Perfect for a demo. In production, swap it for your own backend endpoint — the tool structure stays the same regardless of what the handler calls internally.

7. Step 5: Add Multiple Tools

Most sites should expose more than one tool. Call registerTool() once per tool. Each registration is independent.

// Register several tools at page load
navigator.modelContext.registerTool({
  name: "search-articles",
  description: "Search blog articles by keyword. Returns titles, " +
               "summaries, and publish dates.",
  inputSchema: { /* ... */ },
  execute: async ({ query }) => { /* ... */ }
});

navigator.modelContext.registerTool({
  name: "get-article",
  description: "Get the full text of a specific article by its slug.",
  inputSchema: { /* ... */ },
  execute: async ({ slug }) => { /* ... */ }
});

navigator.modelContext.registerTool({
  name: "subscribe-newsletter",
  description: "Subscribe an email address to the weekly newsletter.",
  inputSchema: { /* ... */ },
  execute: async ({ email }) => { /* ... */ }
});

If your site's available tools change based on state — say, after a user logs in — use navigator.modelContext.provideContext() to replace the full set of tools at once. This is cleaner than registering and unregistering individual tools.

// After login, swap to authenticated tools
navigator.modelContext.provideContext({
  tools: [
    { name: "view-orders", description: "...", inputSchema: { /* ... */ },
      execute: async () => { /* ... */ } },
    { name: "track-shipment", description: "...", inputSchema: { /* ... */ },
      execute: async ({ orderId }) => { /* ... */ } },
    { name: "request-return", description: "...", inputSchema: { /* ... */ },
      execute: async ({ orderId, reason }) => { /* ... */ } }
  ]
});

8. Permissions and Security

WebMCP inherits the browser's security model. That's one of its biggest advantages over server-side API approaches, but it also means you need to think carefully about what you expose.

Authentication is automatic. Tool handlers run in the same browsing context as the page, so they have access to the user's cookies, session tokens, and any auth state your app maintains. If a user isn't logged in, your handler can check that and return an error instead of proceeding.

Start with read-only tools. A search-products tool is low-risk — the worst that happens is an agent runs a search. A delete-account tool is high-risk. Roll out write operations gradually, after you've seen how agents interact with your read tools.

Validate inputs in your handler. The JSON Schema catches type mismatches, but it won't validate business logic. If an agent passes a maxResults of 10,000, your handler should cap it:

execute: async ({ query, maxResults = 10 }) => {
  const limit = Math.min(maxResults, 50); // Cap at 50
  const results = await searchAPI(query, limit);
  return { products: results };
}

Rate-limit expensive operations. If a tool triggers a database query or external API call, apply the same rate limits you'd apply to any API endpoint. An agent calling your tool in a loop could generate real load.

Don't expose internal state. Your tool shouldn't return database IDs, internal user records, or anything you wouldn't put in a public API response. Return only what the agent needs to accomplish its task.

9. The Declarative Shortcut

If your site has standard HTML forms, there's a faster way to add WebMCP support. The declarative API lets you annotate existing form elements with special attributes, and the browser converts them into tools automatically.

<form toolname="contact-us"
      tooldescription="Submit a message to the site owner"
      action="/api/contact"
      method="POST">
  <input type="text" name="name"
         tooldescription="Your full name" required />
  <input type="email" name="email"
         tooldescription="Your email address" required />
  <textarea name="message"
            tooldescription="The message you want to send">
  </textarea>
  <button type="submit">Send</button>
</form>

Three attributes do all the work: toolname on the form, tooldescription on the form and each input, and the standard name and type attributes the browser already knows about. The browser generates the JSON Schema from the form structure — required attributes become required fields, type="email" gets validated as a string, <select> elements become enums.

By default, the declarative API pre-fills the form but waits for the user to click Submit. If you want the agent to submit automatically, add toolautosubmit to the <form> tag. Only do this for low-risk actions like search queries — you probably don't want an agent auto-submitting a payment form.

10. Testing Your Implementation

You can't ship what you haven't tested. Here's how to verify your WebMCP tools work correctly.

Manual Testing in DevTools

Open Chrome DevTools on your page and run this in the console:

// List all registered tools
const tools = await navigator.modelContext.tools;
console.log("Registered tools:", tools.map(t => t.name));

// Call a specific tool
const result = await navigator.modelContext.callTool(
  "get-weather-forecast",
  { city: "Berlin", units: "celsius" }
);
console.log("Result:", result);

This tells you immediately whether your tools are registered and whether they return the expected output.

Model Context Tool Inspector

Install the Model Context Tool Inspector Chrome extension. It gives you a panel that shows all registered tools on the current page, lets you fill in parameters and execute them manually, and can even test them against the Gemini API to see how an agent would use them. It's the closest thing to an integration test you'll get without building a full agent.

Automated Testing

For CI pipelines, you can test your tool handlers directly — they're just async functions. Extract the handler logic into standalone modules and write normal unit tests:

// weather-tools.js
export async function getWeatherForecast({ city, units = "celsius" }) {
  // ... same handler logic as above
}

// weather-tools.test.js
import { getWeatherForecast } from "./weather-tools.js";

test("returns forecast for valid city", async () => {
  const result = await getWeatherForecast({ city: "London" });
  expect(result.location).toContain("London");
  expect(result.current.temperature).toBeDefined();
  expect(result.forecast).toHaveLength(3);
});

test("returns error for invalid city", async () => {
  const result = await getWeatherForecast({ city: "xyznotacity" });
  expect(result.error).toBeDefined();
});

Test the handler separately from the registration. The registerTool() call is just plumbing — the handler is where bugs live.

11. Submit to webmcplist.com

Once your tools are working, get your site listed in the WebMCP Directory. This is how agents and developers building agents discover sites that support WebMCP integration.

Head to webmcplist.com and submit your site with:

The directory is community-maintained, and new submissions are reviewed before they go live. Getting listed means agents that crawl the directory can find your site automatically — it's the closest thing to SEO for the agent-driven web.

Checklist before submitting: Make sure your tools work on the live site (not just localhost), return clean JSON without HTML fragments, handle errors gracefully, and have clear descriptions. Reviewers test submissions, so broken tools won't get listed.

Putting It All Together

Here's a minimal but complete HTML page with WebMCP tools registered. Copy this, swap in your own tool logic, and you're ready to go.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>My WebMCP-Enabled Site</title>
</head>
<body>
  <h1>Welcome</h1>
  <p>This site exposes tools via WebMCP.</p>

  <script>
    if (navigator.modelContext) {
      navigator.modelContext.registerTool({
        name: "search-docs",
        description: "Search documentation articles by keyword. " +
                     "Returns matching article titles and URLs.",
        inputSchema: {
          type: "object",
          properties: {
            query: {
              type: "string",
              description: "Search keywords"
            }
          },
          required: ["query"]
        },
        execute: async ({ query }) => {
          const res = await fetch(
            `/api/search?q=${encodeURIComponent(query)}`
          );
          const data = await res.json();
          return { results: data.articles };
        }
      });

      console.log("WebMCP tools registered");
    } else {
      console.log("WebMCP not available in this browser");
    }
  </script>
</body>
</html>

The if (navigator.modelContext) check is important. Without it, your script throws on browsers that don't support WebMCP yet. Wrap all your registration calls in this guard, or load the MCP-B polyfill first.

WebMCP is still early, but it's not experimental in the way most browser APIs are early. Google and Microsoft are co-authoring the spec, Chrome Canary already ships it, and the W3C is moving toward formal standardization. Sites that add WebMCP support now will be ready when browser-based agents go mainstream. Sites that wait will be stuck with screen scraping as their only integration path.

The code isn't complicated. The hard part is deciding which tools to expose and writing descriptions good enough that an agent picks the right one. Start with your most-used feature, register one tool, and test it. You can always add more later.

Frequently Asked Questions

Do I need to change my backend to add WebMCP?
No. WebMCP tools run entirely in client-side JavaScript. Your tool handlers call your existing API endpoints using fetch — the same endpoints your frontend already uses. You don't need new routes, new controllers, or any backend changes. If your site already has a REST or GraphQL API that your frontend consumes, your WebMCP handlers just call those same endpoints.
How does navigator.modelContext differ from a regular REST API?
A REST API requires the agent to know your endpoint URLs, handle authentication separately, and parse your response format. With navigator.modelContext, the agent discovers available tools automatically by visiting your page. Authentication is inherited from the browser session — no API keys needed. The JSON Schema on each tool tells the agent exactly what parameters to send. It's like a self-documenting API that lives inside the page itself.
Can I use WebMCP with React, Vue, or other frameworks?
Yes. Call navigator.modelContext.registerTool() after your app mounts — in a useEffect hook in React, in onMounted in Vue, or in ngOnInit in Angular. The key requirement is that your tool handler logic should be separate from your component rendering. Extract your data-fetching and business logic into standalone functions or services, then call those from your WebMCP handlers. This keeps your tools working even when your component tree changes.
How do I get my WebMCP site listed on webmcplist.com?
Visit webmcplist.com and submit your site with its URL, a description of your tools, and the tool names you've registered. Submissions are reviewed before going live — reviewers check that your tools actually work, return structured data, and have clear descriptions. Make sure your tools function on the live deployed site (not just localhost) before submitting. The directory is the primary way agents and developers discover WebMCP-enabled sites.

Related Articles & Tools