00 read this with your agent
Read this with your agent first.
Copy one of these, paste it into whatever AI you use, and it will teach you from this page and only this page. An agent in a browser with WebMCP can use the page’s tools directly, and here it can run the demo itself.
1Set up your AI optional, two minutes, once
2Then copy a prompt
01 the claim
Most of the web’s visitors are about to be agents, and the page has to decide how to receive them.
An agent reading a page has to guess what to click. An agent given a tool only has to choose one.
An agent, an AI acting in your browser, uses a site the way a stranger would: read the page, guess which control does what, click, read again. Every step is a guess, and a guess can invent a button that is not there.
Hand it a short list of things it can do instead, each with a name, a description it reads, and a schema, a precise description of the input it needs, and the guess collapses into a choice. Get the choice right and the action is almost guaranteed.
That is the promise of WebMCP: a site registers tools, small named actions such as “place a player”, for whatever agent is driving the browser. The docs tell you how to register one. They do not tell you what it changes, so this post asks a narrower question: on a real task, with a real agent, how much does it matter?
If there is a complicated task I do by hand, I can set the right tools once, and then I can say it out loud and the agent gets it done.the sentence this post has to earn
02 play with it
Pick the eleven for Saturday.
A small football app with fiddly rules, and a switch that decides whether the agent gets tools.
27 players, a formation to fill, injuries, suspensions, and a European list that differs from the league one. One commitment at the end: submit the team sheet. Try it by hand first. Then pick a door, press Run, and watch a recorded agent do it with what it could see.
nextPick a player from the squad, then tap the slot to put him in
Goalkeepers
Defenders
Midfielders
Forwards
The UEFA list submitted under Article 15, as registered.
Door A is selected: no tools registered, the agent can only read the page. Press run to watch a recorded one, or play it yourself.
Names, numbers, positions and nationalities are the real 2026-27 first team. Everything about availability — fitness, suspensions, minutes, registration lists and the club-trained flag — is made up by me for this app. None of it is on this page, and neither is nationality: they live in another system, which the tools can ask and the form only reports at submit.
03 two doors
Same task, same agent, one switch.
Both runs were recorded step by step, so you can see exactly what each door cost.
Through Door A nothing is registered: the agent reads and clicks like a person. Through Door B the page registers 11 tools and the agent gets the list. The page records every click, every call and every refusal as a trace, a saved record of the run.
- Door A · clicks
- 28median
- Door B · calls
- 17median
- Door A · task met
- 6of 9 runs
- Door B · task met
- 9of 9 runs
The details: the two earlier app versions, and how the runs were recorded
These runs were made three times, on three versions of the app, and the numbers above are the third. The first page printed the rulebook and the compliance counters beside the pitch, and every DOM run met its task with nothing refused: 9 of 9, at a median of 17 clicks against 13 calls. The second put the rules in a legalese drawer and left a fitness chip and the minutes on every squad row, and the DOM door still met 9 of 9 at 20 clicks, because nothing it needed was off the page. So the third page hides what real apps hide: the squad list is a name and a position, the medical report is a tool and a refusal at submit, and nationality and the club-trained mark live in the tools too. Same agent, same harness, same tasks.
Claude Opus 5, a fresh agent per run ran it, on Playwright-driven Chromium with document.modelContext from the page's WebMCP polyfill, on a local build of 89733f5, 3 runs per task per door, 1 run excluded: medium-B: three attempts before run 14 were killed by the API (one HTTP 529 after five calls, then a 500 and a 529 before any call); none was the agent's doing; set aside under tmp/aborted, not in this folder, not counted.. Run 17 of rules-in-drawer (hard, Door B): a person pressed Confirm after the runner's own permission system stopped it at the human step; its calls stand. The figures draw the as-shipped condition; 18 earlier runs as rules-in-view and 19 earlier runs as rules-in-drawer are kept and reported in 04.
04 the score
Where the DOM door gives out.
Through the page the agent handed in wrong sheets and was never told; through the tools it never did, and it was faster on every task.
The numbers here are minutes and wrong sheets, not clicks. Medium through the DOM: 0 of 3 met. 2 runs confirmed a sheet the page accepted with four over-cap starters on it, and the third declined because it could not know. Through the tools, 3 of 3, and no wrong action in nine runs.
From first action to confirm: 1.4, 4.3 and 3.6 minutes through the DOM against 1.1, 1.8 and 1.9 through the tools. The difference is the refusal loop: submit, be told, fix. The DOM agent only learns at submit, and only if the app refuses; the tool door made no wrong action because the report is one call and every runner made it first.
A team sheet is a small thing to get wrong. The failure is not. A DOM agent that submits a wrong sheet and is never told is the same agent that will confirm a wrong order, a wrong transfer or a wrong booking: whatever the page accepts without objecting. The page accepted it, so the agent reported success, and nobody was told. An agent acting on someone’s behalf has to act on reliable information, and a control on a page is not information about the rules behind it. Well-defined tools are: the rules as data before the first move, the availability report as one call, a refusal that names the article. That is what WebMCP registers on the page, and it is why the tool door was right 9 times in 9 with nothing refused, in less time on every task.the point of the post
That is how agentic browsing should work: a site that registers its tools is telling an agent what it can rely on, and the agent that asks before it acts is the one you would let act for you.
The harder the task, the wider the gap.
Rule density counts how many of the nine rules a task can get wrong: easy 3, medium 6, hard 7.
the same numbers as a table
| Task | Rules | Door A met | page-legal | refused | clicks | minutes | Door B met | page-legal | refused | calls | minutes |
|---|---|---|---|---|---|---|---|---|---|---|---|
| Easy · a legal XI | 3 | 3 / 3 | 3 / 3 | 1 | 15 | 1.4 | 3 / 3 | 3 / 3 | 0 | 11 | 1.1 |
| Medium · rest, left-back, captain | 6 | 0 / 3 | 2 / 3 | 2 | 28 | 4.3 | 3 / 3 | 3 / 3 | 0 | 18 | 1.8 |
| Hard · the European list | 7 | 3 / 3 | 3 / 3 | 2 | 34 | 3.6 | 3 / 3 | 3 / 3 | 0 | 17 | 1.9 |
The details: how the minutes were clocked, the refusal medians, and what nine runs can say
The minutes run from the first recorded action to the confirm or the decline, the agent’s thinking included, the same model and harness on both doors.
Even where the DOM door reached a legal sheet it took a refusal to get there: a median of 1 per run on easy, 2 on medium, 2 on hard.
Rule density is a count, not a feeling, worked out from the rulebook and the board the task hands you rather than typed under the axis. This post cannot say more than its runs do, nine through each door, three per task. On those runs the tool door made no wrong action and the DOM door made 2 silent ones.
05 the tools
Three tiers, and a human at the last one.
Sorting tools by what they can do is what makes it safe to let an agent use them.
Answerread-only · readOnlyHint
- get_squad
- get_availability
- get_rules
- get_team_sheet
- validate_team_sheet
Call them freely; nothing changes. validate hands back the rules’ verdict before submit.
Actionreversible · drives the page
- set_formation
- place_player
- bench_player
- set_captain
- clear_team_sheet
Each moves the sheet, and another can undo it. One verb each; a giant edit_sheet would have been worse.
Sensitivecommitment · consequentialHint
- submit_team_sheet
It does not finish until a person confirms in the page. The agent proposes; the human commits.
The details: where the three-tier sorting comes from (the WebMCP directory)
This is the same sorting the public WebMCP directory uses for the sites it lists, so the reader learns it from the build rather than from a glossary.
06 how it is wired
Five parts, no server.
A tool is code on the page: create it, register it, and the agent can find it and call it.
There is nothing to upload to and no key anywhere. Each trace is a JSON file the reader downloads, and the post’s figures are drawn from those files.
WebMCP, in four moves.
The page declares what it can do and the agent asks the list, not the pixels. The draft is moving; 07 says where.
01Create
A tool is four things: a name the agent matches on, a description the model actually reads and reasons about, a JSON Schema for the input, and an execute that runs in the page with the page’s own state and the user’s own session. No endpoint, no key. The description is the part people underwrite, and it is the part doing the work.
const placePlayer = { name: "place_player", // the model reads this. Write it for a reader, not for a linter. description: "Put a squad player into an open slot of the current formation.", inputSchema: { type: "object", properties: { player: { type: "string", description: "Squad player's surname." }, slot: { type: "string", enum: SLOTS }, }, required: ["player", "slot"], }, // hints let a client sort read-only from consequential annotations: { readOnlyHint: false }, // every execute is wrapped, so the recorder sees the call and the diff execute: async ({ player, slot }) => record("place_player", sheet.place(player, slot)), };
02Register
Feature-detect before you touch it: in stable Chrome it sits behind a flag or an origin trial, and a page that assumes it exists breaks for every reader who does not have it. The September draft has no
unregisterTool: you pass anAbortSignaltoregisterTool, and aborting it is how a tool goes away, which is exactly what you want on a route change. Chrome’s early preview and the polyfill still carry older names,navigator.modelContextamong them, so write to the draft and detect.if (document.modelContext) { const teardown = new AbortController(); await document.modelContext.registerTool(placePlayer, { signal: teardown.signal }); // no unregisterTool. The signal is the lifetime. onRouteChange(() => teardown.abort()); }
03Discover
This is the first thing the agent does, and the reason Door B is short: instead of guessing which control does what, it asks for the list and gets names, descriptions and schemas. Call it yourself from the console; it is the fastest way to prove your catalog is really registered rather than merely written. This page switched the panel to Door B when you got here, so the eleven are registered; through Door A the list below is the site’s own tools.
const tools = await document.modelContext.getTools(); tools.map(t => t.name); // 28 tools: the site's 17 (list_pages, search, list_posts, …) // and the app's 11: ["get_squad", "get_availability", "get_rules", "get_team_sheet", …]
04Call
The call is the tool you found in the list plus an object that satisfies its schema. What comes back is a string, whatever your
executereturned; parse it and it is structured, so the agent does not have to scrape the page to find out whether it worked. A refusal is a returned error, not a silent no-op, which is what makes the whole run recoverable.submit_team_sheetis the exception: it stops and asks a person, and that confirmation lives in the page.const tools = await document.modelContext.getTools(); const place = tools.find(t => t.name === "place_player"); const raw = await document.modelContext.executeTool(place, { player: "Gordon", slot: "LW", }); const result = JSON.parse(raw); // executeTool resolves to a string // { ok: true, slot: "LW", sheet: { filled: …, open: [ … ] } } // and on a rule violation, an error the agent can read and act on: // { ok: false, error: "not_on_european_list", player: "Raphinha" }
To try it today, run the polyfill, @mcp-b/webmcp-polyfill, or turn on chrome://flags/#enable-webmcp-testing. Tools are bound to the tab: close it and the catalog is gone, which is the property that makes this safe to put on a personal site.
07 where it breaks
What this does not prove.
The runs are few and the API is young, but the real risk is words on a page giving the agent orders.
untrustedContentHint annotation that marks a result as carrying text from someone other than the site. A label, like the one in 09, not a filter.What the three kinds do is limit the damage rather than stop the trick. The first kind only looks things up, so it can change nothing. Everything in the second kind can be undone by something else in the second kind. And the only tool that commits anything waits for a human. So the worst a hijacked agent can do here is make a mess you can tidy up; it cannot send the team sheet. That is only true while I sort the tools honestly: had I quietly put
submit_team_sheet in the second kind, none of it would hold.The details: how the API has changed, and how narrow the medium finding is
The API moved from navigator to document this year and shed provideContext on the way. Expect it to move again.
Agents are not deterministic, so the post reports the spread and shows every trace, not one lucky run. And the medium finding is narrower than it looks: it is about a rule the app only enforces in Europe and minutes that live off the page. An app that refused tired men on a league night would have taught the DOM agent at submit, the way the suspension did.
08 say it out loud
“Pick the eleven for Wednesday.”
With the tools set once, the hard task is a sentence said out loud.
The last run is the one the post was for. The hard task, said out loud to the agent in the browser, with Door B open. It asked for the rules, asked who was available, filled the sheet, validated it, and stopped at submit to ask me. I said yes.
09 for your site
Tools that look things up. Then tools that can be undone. Then the one that waits for you.
Ship the read-only tools first; they are safe today and they are most of what an agent needs.
Tools that only look things up cannot break anything. Then add the ones that change the page, one job each, so another tool can always put it back. The last one is worth being careful about: spending money, sending a message in your name, submitting the team sheet. So that tool is built not to finish on its own. When the agent asks for it, the request appears on your site as something you have to press. The agent can propose. Only you can commit.
Two things decide whether that is real protection or just for show. The question has to appear on your page, not in the agent’s own chat window. If the agent is the one being asked, the agent can answer, and a permission something gives itself is not permission. And the label for these tools, consequentialHint, is only a warning. It tells the agent’s software that this one is serious; it does not stop it. A polite agent slows down and a rude one carries on, so the actual stop has to be code on your own site. The label announces the lock. It is not the lock.
Use your agent with this page.
Every section and figure has an ask agent button. Pick how deep to go (Explain it simply, Go deeper or How do we know?) and it copies a prompt about that part. Select a passage for a prompt about it. Paste it into any AI; a browser agent uses this page’s tools, and any other reads the plain-text version.
The details: this page’s own tools, the plain-text URL, and a sample get_outline answer
get_outline lists the sections, the claim each makes and the figures. explain_section and explain_figure explain one part at a level you choose. get_evidence names the recorded runs behind a number. start_guided_reading hands an agent a plan to teach, quiz or answer from this page, or to run the demo with you. Door B adds the app’s eleven. The plain text lives at /txt/blog/tech/two-doors.txt.