Inspiration

Most agent demos on the web just act. The agent clicks buy, submits the form, or empties the cart, and you find out afterward. That gives up two things the open web needs as agents arrive: control, meaning you decide what actually changes, and access, meaning anyone can operate the page, not only people who can see it and use a mouse. WebMCP's own guidance also warns about prompt injection, where a malicious page or a hijacked agent issues tool calls a person never asked for. We wanted a design that answered both at once.

What it does

Baton is a small grocery store. It works normally for a person, who can browse, add items, review the cart, and check out. It also registers five WebMCP tools, so an agent can do the same things from a request like "add oat milk, then check out."

The read tools, search_products and review_cart, run right away. The tools that change state, add_to_cart, remove_from_cart, and checkout, never mutate anything directly. They create a proposal on screen and return text saying the action is awaiting confirmation, so a well-behaved agent waits. Every action is narrated through an ARIA-live region and a plain-language card, and confirmed or rejected actions land in a visible timeline. Checkout is a mock. No payment is taken and no order is sent anywhere.

How we built it

Vanilla JavaScript, HTML, and CSS, with no framework, no build step, and no backend, deployed as static files to Cloudflare Pages. The tools live in a local registry the page's own interface calls directly, and the same tools are separately registered with document.modelContext for the agent, so the human interface never depends on the agent-side API being present.

The confirmation step is enforced inside the tool, not the interface. A tool that changes state does not change it in execute(). It sets one pending proposal and returns text that tells the agent to wait. State only changes when the person clicks Confirm, so there is no code path from a tool call to a state change that skips the person.

We also shipped a "Real agent" console in the page itself: paste any OpenAI-compatible LLM key, and a real model plans the tool calls, which run through the same WebMCP interface and the same confirmation step. We used it to test the flow with a real third-party model (gpt-4o-mini via a model gateway) driving the tools end to end on the live deployed page.

Challenges we ran into

Testing against a real WebMCP client was harder than expected. ChatGPT's web app (chatgpt.com) turned out not to be a WebMCP client at all: it web-searches the page server-side and told us directly that it "does not expose the page's document.modelContext WebMCP tool registry." Chrome's WebMCP flag needs Chrome 149+, which isn't available on our dev machine. That's what led to building the in-page "Real agent" console: it let us prove a real third-party model driving the tools end to end, without depending on a specific browser.

Accomplishments that we're proud of

The confirmation step is genuinely enforced in the tool layer, not just the UI, so there is no way for any agent, well-behaved or not, to skip it. And the same mechanism does two jobs at once: it is an accessibility story, because the agent operates the page's own tools instead of a scraped DOM, and it is a prompt-injection defense, because an injected instruction can call checkout, but it only ever produces a proposal, never an order.

What we learned

How new WebMCP still is, down to which real clients actually support it today. And that a confirmation step designed for accessibility turns out to double as a security boundary, without any extra code, because both problems have the same shape: an agent should be able to act, but a person should always get the last word.

What's next for Baton

A larger catalog and a second task, like a form or a booking, to show the design works beyond shopping. A per-tool policy, where some proposals are approved automatically and high-stakes ones always ask. And a shared session, where a helper and the person they assist both see the same page.

Built With

Share this project:

Updates

Submission history