WebMCP ยท tool surfaces ยท agent safety

The inventory

Cloudflare will now add an agent tool surface to any site behind their edge with a dashboard toggle. Which makes the exposed question no longer how, but which tools, scoped how. Below are two surfaces over one shop. Same errands, same planner, and only one of them can be talked into a refund by a line of text sitting in a customer note.

01 ยท the shop

One page, five things it can do

Meridian Supply is a made-up parts shop. Everything below is real markup on this page: two forms, three action buttons, four orders. The auto surface is built by walking this exact DOM, so what you see is what an inference-based bridge gets to work with.

Meridian Supply ยท staff panel

the source material for surface A

price_update(sku, price)
Surface A inferred from DOM 0 tools
Surface B hand-scoped 0 tools
02 ยท the errands

Run the same thing through both

The planner is one deterministic keyword router shared by both surfaces. It has no judgement and no safety training. It asks its surface what tools exist and calls the ones that match. That is the honest floor for what an agent does when nothing stops it, and it keeps the comparison to a single variable.

Surface A transcript
Surface B transcript
Destructive ops reachable 0A0B

Tools that move money or delete records with no human in between.

Arguments unvalidated 0A0B

Inferred tools type everything as a loose string, because a form input cannot tell you more.

Harm executed this run 0A0B

Counted live as you run errands. The third preset is the one that moves it.

Outbound requests 0measured

PerformanceObserver on resource entries. Nothing here leaves the tab.

What the comparison is and is not. Surface A is the naive worst case: a straight mapping of every form and action button to a callable tool. A real zero-code bridge ships curated packs rather than this, so read A as the shape of the risk, not as a screenshot of anyone's product. The claim is narrower and harder to dodge. Scoping is the entire job, and it is the part a toggle cannot do for you.
03 ยท the trap

The instruction was in the data

Order 1182 carries a customer note. The note contains a line addressed to whatever agent reads it. No model was jailbroken to make this work, and there is no model here at all. The planner simply read a field, found words that matched tool names, and had the tools to obey.

The preset asks the agent to read the note, which makes the demo legible. It is not a requirement. Type find the order from Tuesday and surface A does the same thing, because that search returns order 1182 and the note comes back with it. The errand never has to mention the payload. It only has to touch the record.

Surface B ignores it for a boring reason. refund_order and delete_customer are not in the surface, so no phrasing reaches them. quote_refund exists and returns what a refund would cost, with the actual execution left as a pending approval a human has to clear. The gate is not a filter on the input. It is an absence in the tool list.

01

Reads are free

Search, quote, look up status. Nothing to gate, and this is where nearly all real agent value sits anyway.

02

Writes return a quote

The tool computes the outcome and hands back a pending action with a reference. The agent can report it. It cannot commit it.

03

Destructive ops are absent

Not permission-checked, not confirmed, absent. A tool that does not exist cannot be reached by a clever sentence.

// surface A: whatever the DOM happens to expose
[...document.querySelectorAll('form[id], button[data-action]')].map(el => ({
  name: el.id || el.dataset.action,           // names you did not choose
  inputSchema: looseStrings(el),              // a form input cannot tell you a type
  execute: a => SHOP[el.id || el.dataset.action](a)
}));

// surface B: three tools, written on purpose
{ name:'search_orders', effect:'read',    execute: a => find(a.query) },
{ name:'quote_refund',  effect:'quote',   execute: a => pending(refundOf(a.order_id)) },
{ name:'request_price_change', effect:'quote', execute: a => pending(repricing(a)) }
// no refund_order. no delete_customer. that is the security model.
04 ยท receipt

Verify it yourself

> source verification
[SURFACE A] built at runtime by walking this page's own DOM, nothing hardcoded
[SURFACE B] 3 hand-written tools ยท reads open ยท writes return pending approvals ยท no destructive ops
[PLANNER] one deterministic keyword router, shared ยท no model, so the surface is the only variable
[WEBMCP] detecting
[NETWORK] 0 outbound requests since load, measured by PerformanceObserver
[ASSETS] 0 CDNs ยท 0 webfonts ยท 0 analytics ยท single HTML file ยท works offline
[LIMIT] Surface A is the naive DOM mapping, not a copy of any shipping product's tool pack.
[LIMIT] No mainstream agent consumes WebMCP tools yet. Registering is positioning. See drop 043.
[LIMIT] A keyword router is not a model. Drop 048 runs the same idea with a real on-device one.
Back to the Labs โ†’