Skip to main content
brwbrw← Back to brw

agent surfaces

Sites built for agents take a few calls

Measured 25 September 2026 with brw 0.15.2 · this page as markdown

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: 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.

Tool calls, tool time and bytes returned for the WebMCP path and the DOM path on the same booking flow
PathTool callsTool timeReturnedWhat it took
WebMCP page tools31.17–1.34 s6.7–7.7 KBbrw_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 page110.84–1.32 s~55 KBOpen, 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.

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.

For site owners

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

  • WebMCP tools on /book: revitt.co/book
  • llms.txt: revitt.co/llms.txt
  • Markdown twin: revitt.co/book.md
  • RFC 9727 api-catalog: revitt.co/.well-known/api-catalog
  • ai-catalog.json with an MCP server card: 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.

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.

<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 llmstxt.org. Publish a markdown copy of each page at its own .md URL and link it from the page's head.

<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.

{
  "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.

brw is open source under AGPL-3.0. Install brw · Source