Development

WebMCP Fundamentals: A Hands-On Guide to Building Agent-Ready Web Pages

Introduction

You’ve probably watched a browser agent try to use a website. It takes a screenshot, guesses which button is “Add to cart,” clicks the wrong thing, scrolls, and tries again. Each step is a guess, and one redesign or confusing UX might break the whole flow.

This guide covers the WebMCP fundamentals that fix that problem. Instead of making the agent guess, your page tells it exactly what it can do: “here’s an add_task tool, here’s the input it needs, call it.” No pixel-hunting.

By the end, you’ll have a small task-tracker app that exposes three tools to AI agents. You’ll test them in Chrome DevTools and then hand them to a real agent. It’s all plain HTML and JavaScript, with no framework and no backend.

What is WebMCP?

WebMCP is a proposed web standard that lets a website expose structured tools (named functions with descriptions and JSON Schema inputs) to AI agents running in the browser, so agents call site features directly instead of clicking through the UI.

Here’s the analogy I find most useful. Think of your website as a restaurant. Without WebMCP, an agent is a customer who can’t read the menu and has to point at other people’s plates. With WebMCP, you hand the agent a printed menu: dish names, what each one contains, and how to order it.

In code, that “menu” is a set of tools you register in JavaScript with document.modelContext.registerTool(). Each tool has a name, a description, an inputSchema, and an execute function that runs your existing app logic. There’s also a declarative flavour where you annotate a normal HTML <form> and the browser turns it into a tool for you (Chrome’s WebMCP overview).

If you already know MCP, the WebMCP basics will feel familiar: tools, descriptions, JSON Schema. The big difference is where it runs. A classic MCP server is a separate process your agent connects to. WebMCP tools live inside the browser tab, run as the signed-in user, and update the UI the user is looking at. (New to MCP itself? Start with my MCP for Beginners guide, then come back.)

Why WebMCP Matters in the AI Era

So why should you care, beyond “agents click badly”? Because the cost of a wrong guess goes up as agents start doing real tasks for users. Here are three places where WebMCP earns its keep.

1. Complex forms that humans barely understand. Think insurance quotes, visa applications, or multi-passenger flight bookings. A submit_application tool with a strict schema means the agent maps “my wife and two kids” into the right fields instead of guessing whether “Name” means full name or first name only. Google lists exactly this kind of structured form-filling as a primary use case (WebMCP origin trial announcement).

2. UI widgets designed for eyes, not models. Date-range pickers, drag-and-drop schedulers, and map selectors are miserable for vision-based agents. A pick_dates tool that takes { start: "2026-11-03", end: "2026-11-07" } skips the widget and still updates it visibly.

3. Power features buried in menus. Your SaaS probably has a settings page with a “re-sync data” or “run diagnostics” action three levels deep. Expose it as a tool, and a support agent in the browser can trigger it in one call, with the user watching.

Here’s a question worth pausing on: which three actions on your own product would you most want an agent to get right every time? Keep those in mind. They’re your first tools.

Prerequisites

You don’t need much:

  • Chrome 150 or later (desktop). Chrome Canary is the safest bet for the newest behaviour.
  • The “WebMCP for testing” flag enabled at chrome://flags/#enable-webmcp-testing (or an origin trial token for a deployed site; more on that later).
  • A way to serve files on localhost: Node.js 18+ (for npx serve) or Python 3.
  • Comfort with basic JavaScript (modules, async/await) and a rough idea of what JSON Schema is.
  • Optional: a Gemini API key, if you want to test with a real agent in Step 6.

Step-by-Step Tutorial: WebMCP Fundamentals in Practice

We’ll build this in layers: a normal app first, then WebMCP on top. That order matters, because WebMCP is meant to be a progressive enhancement. Your app must still work for humans in browsers that don’t support it.

Step 1: Enable WebMCP in Chrome

  1. Open chrome://flags/#enable-webmcp-testing.
  2. Set WebMCP for testing to Enabled.
  3. Click Relaunch.

To confirm it worked, open DevTools on any page and run this in the Console:

// Should print "object" when WebMCP is enabled
typeof document.modelContext;

If you see "undefined", jump to the Troubleshooting section before going further.

Step 2: Create the project files

Create a folder with three files:

mkdir webmcp-tasks && cd webmcp-tasks
touch index.html app.js tools.js

Put this in index.html:

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>WebMCP Task Tracker</title>
  <style>
    body { font-family: system-ui, sans-serif; max-width: 32rem; margin: 2rem auto; padding: 0 1rem; }
    li { cursor: pointer; padding: 0.25rem 0; }
    li.done { text-decoration: line-through; color: #888; }
    #status { font-size: 0.9rem; color: #555; }
  </style>
</head>
<body>
  <h1>Task Tracker</h1>
  <p id="status">Checking for WebMCP…</p>

  <form id="add-form">
    <input name="title" placeholder="New task" required>
    <button type="submit">Add</button>
  </form>

  <ul id="task-list"></ul>

  <!-- Both files are ES modules; tools.js imports from app.js -->
  <script type="module" src="app.js"></script>
  <script type="module" src="tools.js"></script>
</body>
</html>

Step 3: Write the app logic (no WebMCP yet)

This is a plain task list. Notice that addTask and completeTask are exported, because our tools will reuse them. That’s the key design idea: WebMCP tools call the same functions your UI calls.

// app.js — a normal, human-usable task list
export const tasks = [];
let nextId = 1;

const list = document.querySelector('#task-list');

function render() {
  list.innerHTML = '';
  for (const task of tasks) {
    const li = document.createElement('li');
    li.textContent = `#${task.id} ${task.title}`;
    if (task.done) li.classList.add('done');
    li.addEventListener('click', () => completeTask(task.id));
    list.append(li);
  }
}

export function addTask(title) {
  const task = { id: nextId++, title: title.trim(), done: false };
  tasks.push(task);
  render(); // the user sees agent-made changes immediately
  return task;
}

export function completeTask(id) {
  const task = tasks.find((t) => t.id === id);
  if (!task) return null;
  task.done = true;
  render();
  return task;
}

document.querySelector('#add-form').addEventListener('submit', (event) => {
  event.preventDefault();
  const input = event.target.elements.title;
  addTask(input.value);
  input.value = '';
});

render();

Step 4: Register your WebMCP tools

Now the fun part. Put this in tools.js:

// tools.js — exposes the app's features to AI agents via WebMCP
import { tasks, addTask, completeTask } from './app.js';

const statusEl = document.querySelector('#status');

// Prefer the current spec location; fall back to the older, deprecated one
const modelContext = document.modelContext ?? navigator.modelContext;

if (!modelContext) {
  // Progressive enhancement: humans can still use the app
  statusEl.textContent = 'WebMCP is not available in this browser. The app still works normally.';
} else {
  try {
    await registerTools(modelContext);
    statusEl.textContent = 'WebMCP tools registered: add_task, list_tasks, complete_task';
  } catch (error) {
    statusEl.textContent = `WebMCP registration failed: ${error.message}`;
    console.error(error);
  }
}

async function registerTools(mc) {
  await mc.registerTool({
    name: 'add_task',
    description: "Add a new task to the user's task list. Returns the new task's id and title.",
    inputSchema: {
      type: 'object',
      properties: {
        title: { type: 'string', description: 'Short task title, for example "Buy milk"' },
      },
      required: ['title'],
    },
    execute: async ({ title }) => {
      // Validate anyway: never trust model-generated input blindly
      if (typeof title !== 'string' || !title.trim()) {
        return 'Error: title must be a non-empty string.';
      }
      const task = addTask(title);
      return `Added task #${task.id}: ${task.title}`;
    },
  });

  await mc.registerTool({
    name: 'list_tasks',
    description: 'List every task with its id, title, and whether it is done.',
    inputSchema: { type: 'object', properties: {} },
    annotations: { readOnlyHint: true }, // tells agents this tool has no side effects
    execute: async () => JSON.stringify(tasks),
  });

  await mc.registerTool({
    name: 'complete_task',
    description: 'Mark a task as done using its numeric id. Call list_tasks first if you do not know the id.',
    inputSchema: {
      type: 'object',
      properties: {
        id: { type: 'integer', description: 'The task id, shown as #id in the list' },
      },
      required: ['id'],
    },
    execute: async ({ id }) => {
      const task = completeTask(Number(id)); // coerce in case the model sends "3"
      return task
        ? `Completed task #${task.id}: ${task.title}`
        : `Error: no task with id ${id}. Call list_tasks to see valid ids.`;
    },
  });
}

A few things to notice. The annotations object is optional metadata: readOnlyHint marks safe reads, and consequentialHint flags irreversible actions so agents can ask the user to confirm first (Imperative API docs). The return values are short strings an LLM can read easily. And errors are returned as helpful text instead of thrown, so the agent can recover.

Pro Tip: Write tool descriptions for the model, not for your teammates. “Call list_tasks first if you do not know the id” looks redundant to a human, but it’s exactly the hint that stops an agent from inventing task ids.

Step 5: Serve it and test in DevTools

WebMCP needs a secure context, and localhost counts. Start a server from the project folder with whichever you have:

# Option A: Node.js
npx serve .
# → Serving at http://localhost:3000

# Option B: Python 3
python3 -m http.server 8000
# → Serving HTTP on :: port 8000 ...

Open the URL in Chrome. You should see “WebMCP tools registered: add_task, list_tasks, complete_task” under the heading.

Now open DevTools, go to the Application tab, and select WebMCP in the sidebar. You’ll see your three tools under Available tools. Click add_task, enter a title like Write WebMCP post, and click Run tool. The task appears in the list on the page, and the Invoked tools log records the input and output (DevTools WebMCP panel docs).

You can do the same from the Console, which is handy for quick checks:

// List what an agent would discover (sorted alphabetically)
const tools = await document.modelContext.getTools();
tools.map((t) => t.name);
// → ['add_task', 'complete_task', 'list_tasks']

// Run a tool by hand, exactly as an agent would
const addTool = tools.find((t) => t.name === 'add_task');
await document.modelContext.executeTool(addTool, { title: 'Ship the demo' });
// → 'Added task #2: Ship the demo'

Congrats 🎉 You now have a page that any WebMCP-aware agent can operate through a proper contract instead of screen-scraping.

Step 6: Let a real agent drive it

DevTools proves the plumbing works. To see an actual model choose your tools, install Google’s Model Context Tool Inspector extension, add a Gemini API key in its UI, and open your localhost page.

Try a prompt like: “Add ‘call the dentist’ and ‘renew passport’ to my list, then mark the dentist one as done.” A well-described toolset should produce something like add_task → add_task → list_tasks → complete_task, and you’ll watch the list update in real time.

Pro Tip: Keep the DevTools WebMCP panel open while you prompt the agent. If a tool’s invocation counter stays at zero, the agent never considered it, and that’s almost always a description problem rather than a code problem.

Step 7 (bonus): The declarative way with a plain HTML form

Not every tool needs JavaScript. You can turn an existing form into a tool with two attributes, toolname and tooldescription (Declarative API docs). Replace the form in index.html with this:

<form id="add-form"
      toolname="add_task_form"
      tooldescription="Fill in a new task for the user to review and submit.">
  <input name="title" placeholder="New task" required
         toolparamdescription="Short task title, for example Buy milk">
  <button type="submit">Add</button>
</form>

Then update the submit handler in app.js so the agent gets a result back:

document.querySelector('#add-form').addEventListener('submit', (event) => {
  event.preventDefault(); // required before respondWith()
  const input = event.target.elements.title;
  const task = addTask(input.value);
  input.value = '';
  // agentInvoked is true only when an AI agent triggered this submission
  if (event.agentInvoked) {
    event.respondWith(Promise.resolve(`Added task #${task.id}: ${task.title}`));
  }
});

When an agent calls add_task_form, Chrome focuses the form and fills the field, but the user still clicks Add. That’s human-in-the-loop for free. (Add the toolautosubmit attribute if you want the agent to submit on its own.) If you keep both versions, delete the imperative add_task tool so the agent isn’t choosing between two tools that do the same job.

Troubleshooting & Common Mistakes

These are the issues you’re most likely to hit, roughly in the order you’ll hit them.

1. TypeError: Cannot read properties of undefined (reading 'registerTool') document.modelContext doesn’t exist. Either the flag is off, you didn’t relaunch, or your Chrome is too old. Fix the browser, and always feature-detect like Step 4 does so other browsers degrade gracefully.

2. TypeError: navigator.modelContext.provideContext is not a function You’re following an early-2026 tutorial. provideContext() and clearContext() were removed during the preview, and the entry point moved from navigator.modelContext to document.modelContext. Use registerTool() per tool on document.modelContext.

3. Registration fails the second time (duplicate tool name) Registering a name that’s already registered is rejected, which bites you with hot-reload or with components that mount twice. Pass an AbortSignal and abort it on teardown:

const controller = new AbortController();
await document.modelContext.registerTool(myTool, { signal: controller.signal });
// later, when the component unmounts or the route changes:
controller.abort(); // unregisters the tool

4. Tools never appear, even with the flag on Check how you opened the page. Opening index.html straight from disk (file://) is unreliable, so use a localhost server. Tools inside a cross-origin iframe also need allow="tools" on the <iframe>, because the tools Permissions Policy defaults to same-origin only (WebMCP overview).

5. The agent calls the tool with the wrong argument types Models sometimes send "3" instead of 3. The DevTools WebMCP panel flags schema violations in the output pane. Tighten the schema with enum and clear descriptions, and coerce defensively in execute like we did with Number(id).

6. executeTool complains about its arguments in the Console Older Chrome builds expected the arguments as a JSON string ('{"title":"Buy milk"}'), and the string form is now deprecated in favour of a plain object. If one form fails, try the other, then update Chrome.

Pro Tip: Return errors as plain-language strings that tell the agent what to do next (“Call list_tasks to see valid ids”). A thrown exception just tells the agent “something broke.” A good error message lets it fix its own mistake.

Limitations & When Not to Use It

I like WebMCP a lot, but let’s be honest about where it stands.

It’s still experimental. Chrome shipped an early preview behind a flag in Chrome 146 in February 2026, and opened an origin trial starting in Chrome 149. The spec is a Community Group draft, not a W3C Standard, and the API surface has already changed more than once. Expect to update your code.

It needs a browser tab. WebMCP is designed for a user in the browser with an agent helping them. If your agent runs headless on a server with no human present, a regular API or an MCP server is the better fit. Google has a WebMCP vs MCP comparison that’s worth reading, and many teams will ship both.

Discovery is page-by-page. An agent only learns about your tools after it loads your page. There’s no site-wide manifest yet.

Agent support is still thin. Exposing tools is easy; having mainstream agents call them is the slower part. Treat WebMCP today as a low-cost bet, not a traffic channel.

Security is your job. A WebMCP tool runs with the user’s session, so treat every tool like a public API endpoint: authorize on the server, validate inputs, mark irreversible actions with consequentialHint, and set untrustedContentHint when returning user-generated content that could carry prompt injection (WebMCP security guidance).

So here’s a second question for you: if an agent called your most powerful tool with the worst possible arguments, what would happen? If the answer scares you, fix that before you register anything.

FAQ

Is WebMCP the same as MCP?

No. They share ideas (tools, descriptions, JSON Schema inputs), but MCP connects agents to servers running outside the browser. WebMCP tools are registered by a web page and run inside the browser tab as the signed-in user. They complement each other rather than compete.

Which browsers support WebMCP?

Right now it’s Chrome: behind the chrome://flags/#enable-webmcp-testing flag for local development, and through an origin trial for deployed sites. Other browsers haven’t shipped it, so always feature-detect and keep your UI fully usable without it.

Do I need a backend to use WebMCP?

No. As this tutorial shows, WebMCP tools are plain JavaScript functions in your page. If a tool reads or changes server data, it calls your existing API like the rest of your frontend does, with the same authentication and authorisation.

Should I use the imperative or declarative WebMCP API?

Use the declarative API when a tool maps cleanly to an existing HTML form; it’s a two-attribute change and keeps the user in control of submitting. Use the imperative registerTool() API for anything involving app state, multiple steps, or data that isn’t a form.

Can I use WebMCP in production today?

Only through Chrome’s origin trial, which lets you enable the API for real users on a registered origin for a limited time. Because the API is still changing, put it behind feature detection and budget time for updates.

Next Steps

Once your first tools work, here’s where I’d go next:

  1. Read the official best practices. Google’s WebMCP best practices and building effective tools guides cover naming, granularity, and schema design in depth.
  2. Study the demos. The GoogleChromeLabs webmcp-tools repo includes a React flight-search demo (imperative) and a restaurant booking demo (declarative).
  3. Bring it into your framework. React has experimental support through the usewebmcp package, and Angular has built-in experimental support.
  4. Follow the spec. Watch the WebMCP explainer on GitHub and Chrome Status so API changes don’t surprise you.

Conclusion

That’s the core of WebMCP fundamentals: register tools with a clear name, a model-friendly description, and a strict schema, then let them call the same functions your UI already uses. You built a working agent-ready app, tested it in DevTools, handed it to a real agent, and saw the declarative shortcut along the way.

Now take it to your own project. Pick one action your users do all the time, expose it as a single well-described tool, and watch how an agent handles it. Then tell me in the comments which tool you built first. I’d love to see what you make. 🚀

Rana Ahsan

Rana Ahsan is a seasoned software engineer and technology leader specialized in distributed systems and software architecture. With a Master’s in Software Engineering from Concordia University, his experience spans leading scalable architecture at Coursera and TopHat, contributing to open-source projects. This blog, CodeSamplez.com, showcases his passion for sharing practical insights on programming and distributed systems concepts and help educate others. Github | X | LinkedIn

Recent Posts

Server Side Request Forgery (SSRF) Explained for Developers

Server Side Request Forgery (SSRF) tricks your own server into fetching malicious URLs — leaking cloud credentials, exposing internal services, and bypassing firewalls entirely. This…

3 months ago

Git Worktree Explained: Work on Multiple Branches at Once

Git Worktree is Git's built-in superpower for checking out multiple branches into separate directories — simultaneously. No stashing, no duplicate clones. This tutorial walks you…

6 months ago

JSON vs JSONC vs JSON5: The Complete Guide for Devs

JSON, JSONC, and JSON5 look almost identical but break in completely different ways. This guide covers the real grammar constraints, parser compatibility traps, trailing-comma gotchas,…

6 months ago

This website uses cookies.