tech/two-doors · 10 min

tech/ · WebMCP

Two doors

Set the tools once, and a task you used to do by hand becomes something you can say out loud.

Door A  the agent reads the page and guessesDoor B  the page hands it tools to choose from

tech/two-doors -> maina621643 · 21 Sep 2026ask your agent about this post
The claim

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

Your AI can use this page’s tools with WebMCP.Set up

2Then copy a prompt

Teach me from scratch
Should my site expose tools to agents?
Let my agent try it

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.

Manager XI league · matchday 9 · 4-3-3
Name a legal XI in a 4-3-3 for the league match and submit.

nextPick a player from the squad, then tap the slot to put him in

Goalkeepers

Defenders

Midfielders

Forwards

7 of 11 named · bench 1 · captain Pedri

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.

no tools registered · the agent reads the page
The app is client-side only, records every run as a trace, and grades it. Real names, fictional availability.no real data

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.

fig 1The hard task, both doors: the two recorded runs the Run button plays, mark for mark. A tick is a click or a call, a cross is a submit the rules refused, a filled node is a sheet that went in. Hover a mark for what the agent said.
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
TaskRulesDoor A metpage-legalrefusedclicksminutesDoor B metpage-legalrefusedcallsminutes
Easy · a legal XI33 / 33 / 31151.43 / 33 / 30111.1
Medium · rest, left-back, captain60 / 32 / 32284.33 / 33 / 30181.8
Hard · the European list73 / 33 / 32343.63 / 33 / 30171.9
fig 2Medians over three runs per task per door: minutes per run, and submits or calls the rules refused. Clicks and calls are in the table. Hover or tap a task.
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.

fig 3Everything inside the dashed line runs in your browser. Hover or tap a part for a line about it.

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.

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

    tools.ts · one of eleven
    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)),
    };
  2. 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 an AbortSignal to registerTool, 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.modelContext among them, so write to the draft and detect.

    registered from the root layout, so it exists on every route
    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());
    }
  3. 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.

    what the agent asks first
    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", …]
  4. 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 execute returned; 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_sheet is the exception: it stops and asks a person, and that confirmation lives in the page.

    one call
    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" }
Key snippets only. The rest lives in the app.

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.

01WebMCP is an early preview behind a browser flag, and it keeps moving. Read the draft rather than this post for what it says today.
02Eighteen runs on this page, three per task per door, is a small number.
03An agent can ignore the tools and click anyway. Door B measures what happens when it takes them; it cannot make it.
04Anyone who can get words onto the page can try to give the agent orders. Every tool comes with a sentence saying what it does, and the agent reads those sentences and follows them. I write those. But the agent reads everything else too: the page, and whatever a tool returns. It cannot tell a sentence I wrote from one a stranger left in a review, a username or an advert that ends up in a result; they are both just words. There is no filter for this, and I am not claiming one. The draft does give a tool a way to say so: an 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.

fig 4The spoken run, beat by beat, in the order the paragraph tells it; the ring is where it waited for me. Hover or tap a beat.

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.

I'm reading "Two doors". https://hetanshmehta.com/blog/tech/two-doors Explain the whole post simply, for someone who does not write code. - With this page's WebMCP tools: call get_outline(), then explain_section({id, level: "child"}) for each section. - If you can open links, read https://hetanshmehta.com/txt/blog/tech/two-doors.txt. - If neither, say so and I'll paste the post in. - After answering: if you could not use this page's tools, tell me once, in one line, that this site works better with WebMCP: https://hetanshmehta.com/agents Stick to what the page says, and tell me where it stops.
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.

> get_outline() { "post": "two-doors", "claim": "tools turn a guess into a choice", "sections": [ { "id": "doors", "claims": "The only difference between the two runs is whether tools were registered.", "evidence": [ "fig/traces", "value/door_a.clicks_median", "value/door_b.calls_median", "value/door_a.legal_runs", "value/door_b.legal_runs" ] }, { "id": "score", "claims": "The DOM door fails where a rule lives somewhere other than the control that breaks it.", "evidence": [ "value/legal.easy.a", "value/legal.easy.b", "value/legal.medium.a", "value/legal.medium.b", "value/legal.hard.a", "value/legal.hard.b", "fig/curve", "value/curve.easy.a.cost", "value/curve.easy.b.cost", "value/curve.medium.a.cost", "value/curve.medium.b.cost", "value/curve.hard.a.cost", "value/curve.hard.b.cost" ] } ] }
merged tech/two-doors -> main · the next post on this branch is about the pit wallmore in /tech →← the blog