webmcpagentsengineering

What is WebMCP, and why should your web app register tools?

WebMCP lets a web page hand tools to the AI agent driving the browser. What the proposal is, why it beats letting an agent click around, and how SeggWat shipped it in a Rust app.

Hauke Jung
|September 02, 2026|
7 min read

MCP gives an agent that runs somewhere else a way into your product: a server, an API key, a network hop. WebMCP is the same idea pointed the other way. The agent is already inside the browser tab, looking at the page your user is looking at, and the page hands it tools.

This post covers what the proposal is, why registering tools beats letting an agent guess at your buttons, and what it looked like to ship it in a real product.

What WebMCP is

WebMCP is a proposal from Google and Microsoft engineers in the W3C Web Machine Learning community group, first published in August 2025. It went into a Chrome origin trial with version 149. Edge ships it behind a flag.

The API is small. A page calls document.modelContext.registerTool() with a name, a description, a JSON Schema for the arguments, and an execute function:

js
await document.modelContext.registerTool({
  name: "add_todo",
  description: "Add an item to the user's todo list.",
  inputSchema: {
    type: "object",
    properties: {
      text: { type: "string", description: "The item, in the user's words." },
    },
    required: ["text"],
  },
  async execute({ text }) {
    await addTodo(text);
    return { content: [{ type: "text", text: `Added ${text}` }] };
  },
});

An agent driving that browser calls getTools() to see what the page offers, executeTool() to call one, and listens for toolchange when the set moves. That is the whole surface.

It borrows its vocabulary from MCP on purpose: tools, inputSchema, content blocks, isError. If you have written an MCP server, you already know the shape. There is also a declarative half, where a plain HTML form becomes a tool through attributes, so a page with no JavaScript can still take part.

Who calls the tools is whatever agent is in the browser. Today that is the browser's own assistant or an extension, since the API is exposed by the browser to code running inside it. Agents that live outside the tab, Claude Code, Claude Desktop, Cursor, reach the same tools through a bridge: an extension or a small local server that reads getTools() from the open tab and republishes it as an ordinary MCP server on your machine. The page does not know or care which it is.

The spec is not finished. provideContext() was removed in March 2026 and unregisterTool() in April, replaced by an abort signal on the registration. Expect it to move again before it ships on by default.

Why register tools instead of letting the agent click

An agent can already drive a page by reading the DOM and clicking. Four things change when the page registers tools instead.

The session is the auth. The tools run in the tab the user is signed into, through the same calls the buttons make. There is no key to mint and nothing to revoke when the tab closes. A hosted MCP server needs its own authorization path; a WebMCP tool reuses the one you already have.

The agent sees what the user sees. Same permissions, same project, same rows. If the user cannot delete something, neither can the agent, because the tool calls the endpoint that enforces that.

A schema is a contract, a DOM is a guess. "Which button is upvote" becomes vote_idea({ idea_id }). The description tells the model when to call it, the schema tells it what to pass, and both survive a redesign of the page.

Two directions, two audiences. A hosted /mcp endpoint serves your team's agents running elsewhere, overnight, with a key. WebMCP serves whoever has the page open, right now. Most products want both, and the tool definitions are close enough that the second is cheap once you have the first.

When it does not earn its place: a static page with nothing behind a login has little to hand over, and an agent can read HTML. And with support still in an origin trial, ship it as an enhancement. A page that registers nothing must still work.

An example: SeggWat's feedback board

SeggWat is a feedback tool. Its dashboard and public boards are Rust compiled to WebAssembly, and as of today both publish WebMCP tools.

Opening a project in the dashboard registers eight tools for as long as the project is open: list, read, create and update feedback, project stats, the project list, a navigation tool, and one that tells the agent which project it is looking at. Leave the project and the tools unregister with it. On the public board, visitors get ten tools for the things a visitor can already do: search ideas, read one, vote, submit, comment, browse the changelog, move between pages.

Four choices in that integration are worth copying:

  • Scope tools to what is on screen. A tool registered by a component that has since unmounted still answers calls, and acts on state nobody is looking at. That is the failure mode that got provideContext() removed from the spec. Register on mount, unregister on unmount, every time.
  • Side effects stay off by default. Resolving a feedback item through a tool does not email the submitter unless the agent passes notify_submitter: true. A bulk triage pass should not mail a batch of customers because a status flipped.
  • Give the agent an orientation tool. seggwat_current_context answers "which project am I in", and its description says to call it first.
  • Prefix the names. An agent may hold tools from several pages and servers at once. seggwat_list_feedback cannot be confused with anyone else's list_feedback.

Here is one of those tools, trimmed. The argument struct is the schema: register_typed derives the JSON Schema from the type, and the doc comments become the field descriptions the model reads.

rust
use webmcp::{Tool, ToolResult};

#[derive(serde::Deserialize, schemars::JsonSchema)]
struct ListFeedbackArgs {
    /// Which triage bucket to list. Defaults to `open`.
    status: Option<StatusBucket>,
    /// Free-text search across message bodies.
    search: Option<String>,
    /// How many items to return, 1-50. Defaults to 20.
    limit: Option<u64>,
}

webmcp::use_tool(move || {
    Tool::new("seggwat_list_feedback")
        .description(
            "List feedback items in the project the user currently has open. \
             Defaults to the open triage queue. Returns excerpts; call \
             seggwat_get_feedback for the full item.",
        )
        .register_typed(move |args: ListFeedbackArgs| {
            let scope = scope.borrow().clone();
            async move { list_feedback(scope, args).await }
        })
});

use_tool parks the registration in the component's state, so unmounting is enough to unregister. The scope.borrow() is there because the browser invokes handlers from a plain callback, outside the framework's runtime, so state reaches them through a snapshot rather than a reactive signal.

That crate is webmcp-rs, MIT licensed, with a live demo you can call tools on by hand. Five lines of eval will register a tool; the crate exists for the two things those five lines get wrong in practice, schema drift between the advertised schema and the handler's struct, and ghost tools that outlive their component.

Try it

  • Chrome 149 or newer: enable chrome://flags/#enable-webmcp-testing, install the Model Context Tool Inspector extension, and open a page that registers tools. The extension lists them and lets you call them.
  • From the console, on such a page:
js
(await document.modelContext.getTools()).map(t => t.name);
  • Any browser: the demo loads Google's polyfill, so it works without the flag.
  • From Claude Code, Claude Desktop, or Cursor: install a bridge such as the WebMCP Bridge extension or webmcp-cdp-bridge. Either one exposes the open tab's tools as a local MCP server, and the agent calls them like any other server's, inside your logged-in session.

Where this meets the directory

This directory records how remote MCP servers' contracts change over time: a tool removed, an argument newly required, an enum narrowed. A WebMCP page carries the same contract, in the same shape. A field that became required is a breaking change whether it lives behind /mcp or on a page, and the same classification applies.

So as of today the directory lists WebMCP pages next to the servers. The difference is who observed the contract. A server is probed from here every six hours. A page's tools live in the browser, where no probe can see them, so the page's owner proves control of the host, pastes what getTools() returns, and every entry is marked owner-reported. Re-paste after a change and the diff is classified like any other. If you run a page that registers tools, list it.

Blog

Tags

Recent Posts