> WebMCP in practice: declarative form attributes, document.modelContext.registerTool, tool annotations, security gates, local testing and a checklist.
>
> Web page: https://balazscsorba.com/blog/webmcp-agent-ready-website-guide · Language: English · Also available in: [Deutsch](https://balazscsorba.com/de/blog/webmcp-agent-ready-website-guide.md) · [Magyar](https://balazscsorba.com/hu/blog/webmcp-agent-ready-website-guide.md)
> Author: Balázs Csorba · Published: 2026-05-28 · Updated: 2026-10-07 · Keywords: WebMCP, document.modelContext, registerTool, agent-ready website, WebMCP declarative API, WebMCP origin trial, WebMCP tools, tools Permissions Policy, how to expose website actions to AI agents, Chrome 149 WebMCP

[Blog](https://balazscsorba.com/blog)/[Web engineering](https://balazscsorba.com/blog/category/web)

# WebMCP in practice: making a website agent-ready with declared tools

WebMCP in practice: declarative form attributes, document.modelContext.registerTool, tool annotations, security gates, local testing and a checklist.

[Balázs Csorba](https://balazscsorba.com/about)·May 28, 2026·updated October 7, 2026·10 min read

-   WebMCP
-   Chrome
-   [AI agents](https://balazscsorba.com/blog/tag/ai-agents)
-   Origin trial
-   Permissions Policy
-   JSON Schema

![WebMCP for agent-ready websites: a robot reads a clear menu board in front of an open shop door.](https://balazscsorba.com/images/blog/webmcp-agent-ready-website-guide/cover.webp?v=72114b8a9c)

## Key takeaways

-   WebMCP is a proposed web standard, an Intent to Experiment in Chrome from version 149, that lets a page register tools with an AI agent instead of leaving the agent to read the DOM.
-   The declarative API turns an ordinary form into a tool with the toolname and tooldescription attributes, plus toolparamdescription per field and toolautosubmit for agent-driven submission.
-   The imperative API is one call: document.modelContext.registerTool() with a name, description, JSON Schema inputSchema and an async execute function.
-   All four tool annotations default to false: readOnlyHint, untrustedContentHint, consequentialHint and, from Chrome 156, debugging.
-   WebMCP only works in origin-isolated documents and is gated by the tools Permissions Policy, which defaults to self and needs allow=tools on a cross-origin iframe.

On this page

1.  [What is WebMCP?](https://balazscsorba.com/#what-webmcp-is)
2.  [Why scraping is a bad interface for agents](https://balazscsorba.com/#why-scraping-is-a-bad-interface)
3.  [How does the declarative API work?](https://balazscsorba.com/#declarative-api)
4.  [How does document.modelContext.registerTool() work?](https://balazscsorba.com/#imperative-api)
5.  [What do tool annotations tell the agent?](https://balazscsorba.com/#tool-annotations)
6.  [What security preconditions does WebMCP have?](https://balazscsorba.com/#security-and-permissions)
7.  [How do you test WebMCP locally?](https://balazscsorba.com/#testing-and-limits)
8.  [WebMCP checklist](https://balazscsorba.com/#checklist)
9.  [Sources](https://balazscsorba.com/#sources)

Listen to this article

0:00

**WebMCP** is a proposed web standard that lets a page declare its own tools to AI agents running in the browser, instead of leaving them to read the DOM and guess what a button is for. Chrome ships it as an origin trial from Chrome 149, and Google announced it at I/O on 19 May 2026. As of September 2026 it is an Intent to Experiment incubated in the W3C Web Machine Learning Community Group, not a Recommendation, so the details below are labelled by the Chrome version they were checked against.

This article works through both WebMCP APIs in code: the declarative attributes you put on a form, the imperative `document.modelContext.registerTool()` call, tool annotations, the two preconditions that gate the whole thing (origin isolation and the `tools` Permissions Policy), how to test it locally, and a checklist. The worked example is the plugin on this site, which registers three page tools: `get_page_content`, `get_contact_details` and `open_page`.

## What is WebMCP?

WebMCP lets a web page register named tools, each with a JSON Schema for its input and a function the browser can call. The agent running in that browser sees the tool list, decides which tool fits the user's task, fills the input and reads the result. The three things it gains are described in the Chrome documentation as **discovery** (a standard way to register tools such as `checkout` or `filter_results`), **JSON Schemas** for the inputs, and **state**, so the agent knows what the current page offers.

The deployment model matters. These tools are registered by the page and executed by the page, in that origin. That is a different thing from an MCP server, which the agent reaches over the network and which lives in your backend. The explainer in [webmachinelearning/webmcp](https://github.com/webmachinelearning/webmcp) is where the design is argued out, and [the ChromeStatus entry](https://chromestatus.com/feature/5117755740913664) shows where the implementation stands. Google's I/O post said Gemini in Chrome would "soon" support the WebMCP APIs, and showed a wall of consumer brands working with it, including Expedia, Booking.com, Shopify, Etsy and Target.

**Read the version with the code**

This API has already been renamed once: earlier drafts and blog posts used `navigator.modelContext`, the current one uses `document.modelContext`. Treat every snippet here as version-labelled, and ship feature detection, not a hard dependency.

## Why scraping is a bad interface for agents

The default way an agent uses a website is what the Chrome docs call _actuation_: "the act of an agent simulating manual mouse clicks and text input, as though it were the human user engaging with your website." That is the worst interface you can offer, because every step is an interpretation the agent has to get right by itself. Class names change, a label says "Continue" when it means "pay", and a six-field form has six chances to guess wrong.

Declared tools remove the guess. The agent does not need to work out that a button labelled "Find flights" submits a search form; you told it, in a schema it can read. The documentation makes a second point that is easy to miss: tools "execute on your webpage visibly, so users gain trust that tasks are completed as expected", and your brand and human-centred design choices stay intact instead of being replaced by a script that clicks through a stripped-down checkout.

WebMCP is also not a replacement for making your content readable. The documentation lists its own first limitation: "Clients and browsers must visit a site directly to know if it has callable tools." An agent that never loads your page still needs a Markdown copy, an `llms.txt` or a plain API, which is the subject of [llms.txt versus Accept: text/markdown](https://balazscsorba.com/blog/llms-txt-vs-markdown-content-negotiation). Tools cover the part a static document cannot: actions, state and the user's current page.

## How does the declarative API work?

The declarative API needs no JavaScript. You add attributes to an ordinary HTML `<form>` and the browser derives a tool from it. Two attributes on the form are mandatory: `toolname` and `tooldescription`. Remove either one and the tool is unregistered.

HTML

```
<form toolname="searchFlights"
      tooldescription="Search available flights between two airports for a date range."
      action="/flights">

  <label for="from">Departure airport</label>
  <input id="from" name="from" required>

  <label for="to">Arrival airport</label>
  <input id="to" name="to" required>

  <select name="cabin"
    toolparamdescription="Cabin class; economy is the default.">
    <option value="economy">Economy</option>
    <option value="premium_economy">Premium economy</option>
    <option value="business">Business</option>
  </select>

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

The form fields become tool parameters. A `<select>` turns into an `enum`, and the text of each `<option>` becomes that value's title in the generated schema, so the agent knows whether a choice means "Return my purchase" or "Where is my package"; `required` on the input ends up in the schema's `required` array. Use `toolparamdescription` whenever the field name alone is not enough. Without it the browser falls back to the text of the associated `<label>`, and then to `aria-description`, which is a thin description for an agent that has never seen your page.

Submission is your choice. Without `toolautosubmit`, the agent fills the fields, brings the form into focus and leaves the Submit button to the person. With it, the agent also submits and the page navigates. Either way the form stays visible while the agent works, and two window events tell you what happened: `toolactivated` fires once the fields are pre-filled, `toolcancel` fires when the user cancels or the form is reset. Both are non-cancelable and carry a `toolName`.

When you do want a result back, use `respondWith()` on the submit event. `SubmitEvent` gains an `agentInvoked` boolean that tells you an agent triggered the submission, so the same handler can behave differently for a person. You must call `preventDefault()` first, and the promise you pass is serialized and returned to the model as the tool's output.

TypeScript

```
form.addEventListener('submit', (event) => {
  event.preventDefault()
  if (event.agentInvoked) event.respondWith(runSearch())   // resolves to the tool output
})
```

![Declarative and imperative WebMCP side by side: HTML form attributes versus a registerTool call with a JSON Schema](https://balazscsorba.com/images/blog/webmcp-agent-ready-website-guide/diagram-17.webp?v=6d55878a12)

Declarative tools are derived from markup by the browser. Imperative tools are JavaScript objects you register, so they can do anything the page can do.

Criterion

Declarative attributes

Imperative registerTool

Where it lives

HTML attributes on a form

JavaScript, usually in a client plugin

What you write

Tool name, description, per-field descriptions

Name, description, JSON Schema, execute function

Input schema

Derived from labels, options and `required`

Yours, verbatim

Result

Submit and navigate, or `respondWith`

Whatever `execute` returns

Good for

Plain forms a person also fills in

Computed, stateful, cross-page actions

Weakness

Only what a form can express

More code, more ways to get it wrong

## How does document.modelContext.registerTool() work?

The imperative API is one call: you pass a tool object with a `name`, a `description`, an `inputSchema` in JSON Schema and an async `execute` function, and the browser makes it callable. This is the plugin on this site, trimmed. `target()` resolves the page name and language to a path and to the Markdown copy of that path.

TypeScript

```
const pageInput = {
  type: 'object',
  properties: {
    page: { type: 'string', enum: Object.keys(PAGES), description: 'home, about, references, blog, game …' },
    language: { type: 'string', enum: ['en', 'de', 'hu'], description: 'en, de or hu. Defaults to the shown language.' },
  },
  required: ['page'],
}

await document.modelContext.registerTool({
  name: 'get_page_content',
  description: 'Returns the full text of a page of this site as Markdown.',
  inputSchema: pageInput,
  annotations: { readOnlyHint: true },
  async execute(input) {
    const response = await fetch(target(input).markdown, { headers: { accept: 'text/markdown' } })
    if (!response.ok) throw new Error(`Could not load the page (${response.status}).`)
    return { content: [{ type: 'text', text: await response.text() }] }
  },
})
```

Three details in that snippet are what make this a tool rather than a `fetch` wrapper. First, the tool returns Markdown, not HTML: it asks for the page's `.md` copy with `Accept: text/markdown`, so the agent gets a few kilobytes of clean text instead of a script-heavy document. Second, `enum` in the schema keeps the agent from inventing page names, and the description tells it what each one is for. Third, `open_page` on this site uses the Nuxt router rather than `navigateTo`, because tools execute long after setup, outside the Nuxt context.

Reading tools back is symmetric. `document.modelContext.getTools()` returns an alphabetically ordered list of what the calling document is allowed to see, and `executeTool(tool, input)` runs one, returning `null` when the tool triggered a navigation instead of a result. A `toolchange` event on `document.modelContext` tells a frame that the list moved. Tools can be unregistered with an `AbortSignal`, and from Chrome 153 unregistering no longer breaks executions that are already running. Your `execute` receives that signal as a second argument, so pass it to any `fetch` it starts.

![One WebMCP tool call end to end: agent, browser, page tool, user confirmation, and the result returned to the agent](https://balazscsorba.com/images/blog/webmcp-agent-ready-website-guide/diagram-24.webp?v=7c52ee959d)

One tool call: discovery, schema, execution on the page, a visible effect the user can confirm, and a result that goes back to the agent.

What runs inside `execute` is an ordinary loop around a model call, the same loop a server-side agent would run, except that the state is the DOM. That is described in [the agent loop, explained](https://balazscsorba.com/blog/agent-loop-explained).

## What do tool annotations tell the agent?

Annotations are optional booleans in a tool's `annotations` object. All of them default to `false`, they are advisory signals rather than a security boundary, and they exist so an agent can decide what to do before it calls.

Annotation

Set it to true when

What it buys you

`readOnlyHint`

The tool only reads and changes nothing

The agent can call it freely, for example to search a catalog

`untrustedContentHint`

The output contains user-generated or fetched data

Clients treat the result as data to sanitize and delimit, not as instructions

`consequentialHint`

Running it has real-world, non-reversible effects

Agents and browsers can require a mandatory confirmation

`debugging`

The tool is developer tooling, not user-facing

General-purpose agents filter it out; available from Chrome 156

The plugin on this site sets `readOnlyHint: true` on the two tools that return text, and leaves it off `open_page`, because navigating is a state change even though it destroys nothing. That is the bar I would hold: annotate honestly, because a wrong `readOnlyHint` is a lie the agent acts on, and an unannotated consequential tool is a purchase an agent can make without asking. Anything that renders content your users wrote should carry `untrustedContentHint`; why that is a security decision rather than a label is covered in [prompt injection as an architecture problem](https://balazscsorba.com/blog/prompt-injection-lethal-trifecta-patterns).

## What security preconditions does WebMCP have?

Two, and both are checked by the browser before your code runs. The first is **origin isolation**: WebMCP is only available in origin-isolated documents, so that the document's origin stays stable for the tool's lifetime. If `document.domain` is enabled, for example because a response sends `Origin-Agent-Cluster: ?0`, the WebMCP APIs are disabled. Check your own headers before you spend an afternoon on a tool that never registers.

The second is the `tools` [Permissions Policy](https://developer.chrome.com/docs/privacy-security/permissions-policy), which defaults to `self`. Top-level and same-origin documents may register tools; cross-origin iframes may not, unless the embedding page adds `allow="tools"`. Registration is also gated separately from visibility: a tool has to be listed in `exposedTo` to be visible cross-origin, and the caller still has to ask for it in `getTools({ fromOrigins })`.

The rest is your own design. Consequential actions need a human, and the API gives you two ways to get one: the declarative path, where the form stays visible and the Submit button stays with the user, and `consequentialHint`, which lets the browser require confirmation. My own rule, and the one I apply to agents that write code, is the same here: the tool prepares it, the person commits it. Nothing that spends money, sends a message or cannot be undone should complete on a model's say-so alone.

## How do you test WebMCP locally?

With a flag. The documentation's local setup is `chrome://flags/#enable-webmcp-testing`: set it to Enabled, relaunch, and the APIs are available on your machine without enrolling. For testing on real users' machines, enroll the origin trial; the Chrome team describes it as time-limited early access with usage limits, which is the trade for shipping an experiment to live traffic.

For inspection, install the Model Context Tool Inspector extension. It shows which tools a page registered, calls them by hand, verifies that the browser can parse your input schema, and shows the structured output or the error message, which is where most schema mistakes become obvious. It is a separate thing from Gemini in Chrome.

Know the limits before you commit. The documentation says the API is "primarily designed for local browser workflows with a human in the loop", so headless runs are not the target. Complex interfaces may need refactoring, or extra JavaScript, before their state can be exposed. Discoverability needs a visit. And the documentation's own status line applies: WebMCP "is under active discussion and subject to change in the future". Frameworks are filling in: Angular has experimental support, React has a `usewebmcp` package.

Which raises the question of when to wait. If your agents are shopping agents rather than browser agents, the server-side protocols are further along, and [UCP, ACP, AP2 and WebMCP compared](https://balazscsorba.com/blog/agentic-commerce-protocols-ucp-acp-guide) works through that. WebMCP earns its place when the thing you want an agent to do needs the page you are already on.

## WebMCP checklist

1.  **Decide whether you need tools at all.** If an agent only has to read, a Markdown copy of the page is cheaper and works without a browser.
2.  **Get access first:** the local Chrome flag for development, the origin trial for real users.
3.  **Use attributes for plain forms, registerTool() for everything else.** The imperative API is the only one that can reach application state.
4.  **Describe every parameter** with `toolparamdescription` or a real `<label>`, and prefer enums to free strings.
5.  **Decide submission deliberately.** Leave Submit to the human, or add `toolautosubmit` and return a result with `respondWith()`.
6.  **Annotate honestly:** `readOnlyHint` only when nothing changes, `untrustedContentHint` on user content, `consequentialHint` on anything irreversible.
7.  **Check your headers.** Keep origin isolation, and remember the `tools` policy is the gate for iframes.
8.  **Keep the human in the loop** for money, messages and deletions. Prepare, then ask.
9.  **Feature-detect the API** and tolerate its renames, so a browser that drops it costs you nothing.

If you are building this into an application rather than a marketing site, the same shape of work comes up in [Vue and Nuxt development](https://balazscsorba.com/expertise/vue-nuxt-developer).

## Sources

1.  [Chrome for Developers: WebMCP (get started)](https://developer.chrome.com/docs/ai/webmcp)
2.  [Chrome for Developers: WebMCP Imperative API](https://developer.chrome.com/docs/ai/webmcp/imperative-api)
3.  [Chrome for Developers: WebMCP Declarative API](https://developer.chrome.com/docs/ai/webmcp/declarative-api)
4.  [Chrome for Developers: Join the WebMCP origin trial (9 Jun 2026)](https://developer.chrome.com/blog/ai-webmcp-origin-trial)
5.  [Chrome for Developers: 15 updates from Google I/O 2026 (19 May 2026)](https://developer.chrome.com/blog/chrome-at-io26)
6.  [WebMCP explainer: webmachinelearning/webmcp](https://github.com/webmachinelearning/webmcp)
7.  [ChromeStatus: WebMCP feature entry](https://chromestatus.com/feature/5117755740913664)

## Frequently asked questions

What is WebMCP?

WebMCP is a proposed web standard, incubated in the W3C Web Machine Learning Community Group, that lets a web page register its own tools with an AI agent running in the browser. Each tool has a name, a description, a JSON Schema for its input and an executable function, so the agent can discover and call it instead of scraping the DOM and guessing which element does what. Chrome runs it as an origin trial from Chrome 149.

What is the difference between navigator.modelContext and document.modelContext?

navigator.modelContext was the earlier shape, used in the first drafts and blog posts. The current imperative API lives on the document, so you call document.modelContext.registerTool(), getTools() and executeTool(). Chrome renamed the entry point while the API was still experimental, which is why production code reads both and falls back. Treating a browser-experimental API as a hard dependency is how a rename becomes an outage.

Should I use the declarative or the imperative WebMCP API?

Use the declarative attributes when a plain form already does the job: toolname and tooldescription on the form, toolparamdescription on fields whose meaning is not obvious, and toolautosubmit only if the agent should submit. Use document.modelContext.registerTool() for anything computed, stateful or cross-page, because only the imperative API can reach application state or return a result without navigating.

Is WebMCP safe to ship on a production site?

It can be, with the browser's own gates respected. WebMCP only runs in origin-isolated documents, so any header that enables document.domain disables it, and the tools Permissions Policy defaults to self, which means a cross-origin iframe needs allow=tools. Beyond that, your execute function is the boundary: mark irreversible tools with consequentialHint so a confirmation can be required, and keep the human in the loop for anything that spends money or cannot be undone.

How do I test WebMCP tools locally?

Enable chrome://flags/#enable-webmcp-testing, relaunch Chrome, and the APIs are available locally with no enrollment. Use the Model Context Tool Inspector extension to see which tools a page registered, call them by hand and check that the browser can parse your input schema. For real-user testing, enroll the origin trial, which is time-limited early access with usage limits.

Written by Balázs Csorba

Senior fullstack & AI engineer in Styria, Austria – 10+ years of Vue, Nuxt, Node.js and PHP, now building tooling for AI agents.

[AI engineering & MCP servers →](https://balazscsorba.com/expertise/ai-engineer)[About me →](https://balazscsorba.com/about)

## More articles

-   [WebMCP: publishing tools instead of pixels](https://balazscsorba.com/blog/webmcp)
-   [AI in the B2B customer portal: orders, invoices and reorders](https://balazscsorba.com/blog/b2b-customer-portal-ai)
-   [llms.txt vs Markdown content negotiation: what agents actually fetch](https://balazscsorba.com/blog/llms-txt-vs-markdown-content-negotiation)
-   [Designing tools for AI agents: names, descriptions, schemas and error messages](https://balazscsorba.com/blog/agent-tool-design-descriptions-errors)

## Sounds like what you need?

Tell me about your project or role – I’d love to hear from you.

[Get in touch](mailto:contact@balazscsorba.com) [Connect on LinkedIn](https://www.linkedin.com/in/balazs-csorba)
