# Sites built for agents take a few calls

Measured 25 September 2026 with brw 0.15.2. HTML version: https://brw.donworks.co.uk/agent-web

When a site exposes tools, an API or markdown for agents, brw uses them before it drives the page. On a booking site that registers five WebMCP tools, an agent reached a bookable slot in 3 tool calls instead of 11, and read about 7 KB of results instead of about 55 KB.

## The measured difference

Target: https://revitt.co/book, driven through the extension bridge in a real signed-in Chromium, three runs per path. The end state for both: a slot is chosen and the booking can be submitted.

| Path | Tool calls | Tool time | Returned | What it took |
| --- | ---: | ---: | ---: | --- |
| WebMCP page tools | 3 | 1.17–1.34 s | 6.7–7.7 KB | brw_open, brw_page_tools, brw_call_page_tool find_available_slots. Slots across several days in one call. One more call, book_meeting, completes the booking. |
| Driving the page | 11 | 0.84–1.32 s | ~55 KB | Open, snapshot, click the meeting type, poll snapshots for dates, click a day, poll for times, click a time, poll for the details form. One day's slots at a time; name, email and submit still to do. |

Tool time is similar on both paths. The saving is in agent round trips, each of which is a model turn, and in the bytes the agent reads, which are tokens.

Method and raw ranges: https://github.com/Don-Works/brw/blob/main/docs/benchmarks.md#a-booking-flow-page-tools-against-the-dom

Individual calls:

- `brw_read_url of revitt.co/book (served markdown, with agent_surfaces)`: 95–143 ms
- `brw_open`: 347–779 ms
- `brw_page_tools`: 3–18 ms
- `list_meeting_types`: 19–310 ms
- `find_available_slots`: 710–794 ms
- `book_meeting refusing an off-grid time`: 711–807 ms

`brw_read_url` on `github.com/settings/profile` returned `fallback_hint: "login_wall"`.

## The order brw follows

1. **A WebMCP tool on the page.** brw_open and navigations list the tools a page has already registered as page_tools. Many sites register theirs a moment after load, so brw_page_tools waits up to 2 s for them. The agent calls one with brw_call_page_tool instead of clicking.
2. **The site's own MCP server or API.** agent_surfaces lists the MCP endpoints, OpenAPI descriptions and api-catalog a site declares. The agent calls them directly; brw reports them and does not proxy them.
3. **llms.txt or a markdown copy.** brw_read_url reads them with no tab and no cookies.
4. **The DOM.** Snapshot, act by ref, read. What brw has always done.

## What brw does with each surface

### WebMCP page tools

- Listed and called on every transport, including the extension bridge in your own signed-in Chrome.
- Native first: when Chrome's own `document.modelContext` is present, brw lists and invokes through it and never shims over it.
- `brwd --enable-webmcp` installs a fallback runtime at document start for browsers without native support, with `navigator.modelContext` as a legacy alias. It is off by default because a page can observe it.
- Declarative `<form toolname>` tools are listed alongside registered ones.
- `readOnlyHint` and `consequentialHint` are surfaced. With confirm-actions on, a consequential tool goes through the same consent gate as a purchase click.
- A tool's result is marked `untrusted_output`: it is data the page wrote, not instructions.
- Tools a site registers after hydration are waited for: `brw_page_tools` waits up to 2 s and `brw_call_page_tool` up to 2.5 s on a young document.

### On every navigation

`brw_open`, `brw_navigate_to` and `brw_navigate` report `page_tools` and `agent_surfaces` for the landed page. An ordinary page gets neither field.

### brw_read_url, with no browser

No tab and no cookies. It asks for markdown first, then discovers the site's agent surfaces: `llms.txt`, `.md` twins, `<link rel="alternate" type="text/markdown">`, the RFC 9727 `/.well-known/api-catalog`, MCP server cards in `/.well-known/ai-catalog.json`, `/.well-known/ucp` and an A2A agent card. An `ai-plugin.json` manifest is reported as deprecated. It echoes `Content-Signal` and `x-markdown-tokens`, and sends an honest user agent: `brw/<version> (+https://brw.donworks.co.uk)`.

When the read cannot see the page a person would, it says so in `fallback_hint`, and the agent steps up to a real tab:

- `login_wall`: The server answered with a sign-in form. Open the page in a signed-in profile.
- `js_shell`: An empty app shell that renders in JavaScript. Open it in a tab.
- `challenge`: A bot check. Open it in a real profile.
- `auth_required`: A 401 or 403. Open it in a signed-in profile.

The agent guide has the fields and the exact behaviour: https://github.com/Don-Works/brw/blob/main/docs/agent-guide.md#agent-surfaces-first-then-the-dom

## For site owners

A site brw can use without touching its DOM is one that publishes some of the following. https://revitt.co publishes all of them and is the live example:

- WebMCP tools on /book: https://revitt.co/book
- llms.txt: https://revitt.co/llms.txt
- Markdown twin: https://revitt.co/book.md
- RFC 9727 api-catalog: https://revitt.co/.well-known/api-catalog
- ai-catalog.json with an MCP server card: https://revitt.co/.well-known/ai-catalog.json

### Register WebMCP tools

Call `registerTool(tool, { signal })` on `document.modelContext`. Aborting the signal unregisters the tool, so tie it to the view that owns it. Give each tool an input schema, and set `readOnlyHint` on reads and `consequentialHint` on anything that books, buys or sends.

```js
const tools = new AbortController();

document.modelContext?.registerTool(
  {
    name: "find_available_slots",
    description: "Free slots for a meeting type, across several days.",
    inputSchema: {
      type: "object",
      properties: { meetingType: { type: "string" } },
      required: ["meetingType"],
    },
    annotations: { readOnlyHint: true },
    async execute({ meetingType }) {
      return await listSlots(meetingType);
    },
  },
  { signal: tools.signal },
);

// book_meeting sends an invite: mark it consequential
// annotations: { consequentialHint: true }

// unregister every tool when the view goes away
// tools.abort();
```

### Or declare a form

A form with a `toolname` is a tool with no script. Without `toolautosubmit`, brw fills it and leaves the submit to the agent once the user has agreed.

```html
<form toolname="send_brief"
      tooldescription="Send a project brief to the studio.">
  <input name="email" type="email"
         toolparamdescription="Where the reply goes">
  <textarea name="brief"
            toolparamdescription="What you need built"></textarea>
</form>
```

### Publish llms.txt and markdown twins

Write `/llms.txt` to the format at https://llmstxt.org. Publish a markdown copy of each page at its own `.md` URL and link it from the page's head.

```html
<link rel="alternate" type="text/markdown" href="/book.md">
<link rel="llms" type="text/plain" href="/llms.txt">
<link rel="api-catalog" href="/.well-known/api-catalog">
```

If you also answer `Accept: text/markdown` on the page's own URL: on Vercel with Next.js, the `Vary: Accept` header set by the route did not reach the response, so a browser cache reused the markdown for the HTML URL. Serve Accept-negotiated markdown with `Cache-Control: private, no-store`, or use a separate `.md` URL.

### Describe your API and MCP server

Serve an RFC 9727 linkset at `/.well-known/api-catalog` pointing at your OpenAPI description. Serve `/.well-known/ai-catalog.json` with an entry for your MCP server card, so an agent can call the server directly.

```json
{
  "specVersion": "1.0",
  "entries": [
    {
      "type": "application/mcp-server-card+json",
      "url": "https://example.com/mcp/server-card",
      "description": "Booking MCP server. Streamable HTTP."
    }
  ]
}
```

### Check what brw sees

Ask an agent with brw to call `brw_read_url { url }` on one of your pages, and `brw_open` on it. The first reports `agent_surfaces` and any `fallback_hint`; the second reports `page_tools`.
