Inspiration

People will happily let an agent research a purchase. Almost nobody lets one finish it.

The reason is not that agents shop badly. It is that "it can look but it cannot buy" has never been a mechanism, only a promise. Every guardrail on offer today is a server saying no, and a refusal is just a message. A model can retry a message, argue with it, or try again with different arguments.

What it does

Hubit is a storefront where you can hand an agent the shopping without handing it your wallet.

You write your rules in plain language: under $1,200, I already have a laptop, prioritise quality. The agent searches the store, compares specs, finds cheaper alternatives, fills the cart, and types your delivery details. You watch every move it makes on screen while you edit the same cart yourself.

Then it goes to check out, and checkout is not in its toolbox. Not refused. Not there. The agent has nothing to retry and nothing to argue with. Its only remaining move is to ask you: "$1,255 against your $1,200 budget. Raise it by $55?"

You click approve, and checkout appears in the agent's toolbox, live, in the same tab, with no reload.

Three things have to be true before that happens, and all three are visible on the page: the cart is inside your rules, your delivery details are complete, and you approved this basket. Change any of them and the approval goes with it. You approved a particular cart going to a particular address, so an agent cannot quietly redirect a parcel you already said yes to.

How we built it

A static storefront on Next.js, React and Tailwind, deployed on Vercel. Seventeen tools the agent can use. No backend, no database, no server anywhere in the project, which is the whole argument: there is no API for the agent to go around, because the authority lives in the page you are looking at.

The agent writes to the same place your keyboard does. There is no separate agent path, which is what makes it honest rather than theatre. Every field the agent typed into your delivery form is marked on screen and tagged as agent-written, and you can overwrite any of it. That single screen carries the entire idea: the agent can type your address and still cannot spend your money.

The store never just goes quiet on the agent either. It always says why checkout is being withheld, so the agent knows to come back and ask instead of retrying into a wall.

Challenges we ran into

Handing back a tool while you are using it. Checkout places the order, and placing the order is exactly what takes checkout away. The tool was pulling the rug out from under itself mid-call, so the agent saw an error on an order that had actually gone through fine.

Accomplishments that we're proud of

An uncoached model shopped the store on its own, in production. Given nothing but a plain shopping prompt, it compared desks, chairs, monitors and keyboards, built a $1,255 basket against a $1,200 budget, hit the missing checkout, went looking for cheaper alternatives, found the nearest one only saved $50, and proposed raising the budget on its own initiative. We never told it to do any of that. The full transcript is in the repo.

105 automated checks against the live site, all passing. They drive the real tools the way an agent would, and they open the gate by clicking the actual approve button on the page, not by flipping a flag in test code.

Nothing in this project is faked. No mock agent, no simulated run, no fallback path. Everything in the demo is the real thing working.

What we learned

That the interesting part of WebMCP is not that a web page can offer tools to an agent. It is that which tools exist can change while the agent is working, and that it can change because a human just made a decision.

We also learned to be precise about what we are claiming. This is a consent boundary, not a security boundary, and it sits one step earlier than a server check. A server saying no is something a model retries. A tool that is not there is something it never thinks to reach for. A real shop wants both, and WebMCP is what makes the earlier one possible at all.

What's next for Hubit

Real payments behind the gate, so both layers do their proper job: the missing tool stops the agent from ever forming the intent, the server stops anything that goes around the page.

Approvals with shape, instead of one yes or no. Approve up to an amount, approve one merchant, approve for the next ten minutes. The mechanism already handles this, since each one is just a different question asked at the same moment.

Approval that survives a refresh, tied to the exact basket so it cannot be reused on a different cart or a different address.

The pattern beyond shopping. Nothing about this is specific to retail. Sending an email, filing a pull request, booking a flight, moving money: any page where an agent should do the whole job except the last irreversible step can hold back exactly one tool, and hand it over when a human says so.

Why our use case is a strong fit for WebMCP

People will happily let an agent research a purchase. Almost nobody lets one finish it.

The reason is not that agents shop badly. It is that "it can look but it cannot buy" has never been a mechanism, only a promise. Every guardrail on offer today is a server saying no, somewhere behind an API. A refusal is a message, a message is an input, and a model can be argued into retrying an input.

WebMCP has one property that no server-side guardrail can imitate: the tool surface is a live, mutable property of the tab. registerTool takes an AbortSignal, so a capability can be absent, then appear mid-conversation because a human decided something, then disappear again when they change their mind. Hubit registers checkout only while a gate is open. When that gate is closed the agent cannot call checkout, cannot retry it, and cannot argue with it, because there is nothing there. getTools() does not list it, and a stale handle captured before the abort is rejected at browser level.

To be exact about what we are claiming: this is not a security boundary, and we have never presented it as one. It is a consent boundary, and it operates one layer earlier than a server check. A server refusal is a runtime "no" that a model retries, reasons around and argues with. A capability that is absent from the surface means the planner never forms the intent, so there is no retry loop to lose. In production both exist the server check is the security boundary, and this is what stops an agent from ever reaching it without you. WebMCP is what makes that earlier layer possible at all, because tools are registered per session in the page and can be revoked mid-conversation.

How it creates a better user experience

The tedious half of a purchase gets delegated. The half that needs judgment does not.

The agent searches, compares specs side by side, finds cheaper alternatives, fills the cart, and types your delivery details name, email, address, postcode, country, delivery speed, courier notes. It writes to the same store your keyboard writes to. There is no separate agent path, and that is exactly what makes the provenance honest: every field the agent typed is outlined in the accent colour and tagged agent, and you can overwrite any of them. That one screen carries the whole thesis the agent can type your address and still cannot spend your money.

You also watch it work instead of reading a transcript of it. Two tools move your screen filter_catalog changes the shelf you are looking at, focus_product opens a quick view and every call streams into a rail on the right with its real arguments. The rail renders from the tool host's own registry rather than from a hardcoded list, so if a tool ever failed to register it shows that instead of claiming it is live.

And the store always tells you why checkout is withheld: over budget, no delivery address, or this basket is not approved. The agent's only remaining move is to ask you. propose_constraint_change queues a proposal and changes nothing by itself.

What people and agents can do together that was difficult or impossible before

Grant a capability mid-conversation and take it back.

Approve the basket and checkout appears live, in the same tab, with no reload. Then change the cart, move the budget, edit the delivery address, switch to express delivery, or click "Withdraw approval", and it leaves the surface again. Consent is per basket, not per session: you approved a particular cart going to a particular place, so any change to either withdraws it. Emptying a required delivery field withholds checkout, and restoring that field does not silently re-open it, because the approval left with the change.

The surface does not merely shrink, it swaps. Placing an order removes checkout and adds get_order in one state change the two conditional tools are conditional in opposite directions.

None of that is expressible against a static tool manifest, and it is not what the field is doing. Every public WebMCP storefront demo we could find, including the ones in GoogleChromeLabs/webmcp-tools/demos, registers checkout unconditionally alongside search_products. The agent can always complete the purchase. Not one of them models the human as an authority the agent has to come back to.

How we implemented WebMCP

Seventeen tools. Fifteen always registered, two conditional in opposite directions.

One seam. src/lib/webmcp.ts is the only file in the project that touches document.modelContext. Everything else talks to a ToolHost interface, so the runtime's quirks are contained in one reviewable place. Registration is the literal call, with an AbortController per tool:

document.modelContext.registerTool({
  name: def.name,
  description: def.description,
  inputSchema: def.inputSchema,
  execute: async (input) => def.execute(input),
}, { signal: controller.signal });

The tool surface is derived state. One effect rebuilds the entire surface as a function of the gate boolean, rather than registering the base tools once and toggling checkout in a second effect. We built that version first and it was subtly wrong: checkout ended up callable-but-rejecting in the window between the two effects. The UI subscribes to the host registry through useSyncExternalStore rather than reading it during render, because registration happens after the render caused by the state change that opened the gate.

We probed the runtime in both browsers instead of trusting the documentation. Six findings are written down with dates in docs/webmcp-findings.md, each cited from the code that depends on it. Three changed the build: toolchange is not usable, because modelContext is not an EventTarget in ChatGPT's in-app browser at all, so we keep our own registry and never render from it. inputSchema does not validate, so all seventeen tools hand-validate missing arguments, wrong types, out-of-range values, unknown ids, string coercion. And a tool must not unregister itself synchronously inside its own call: checkout places the order, which closes the gate, which aborts checkout, while checkout is still executing. That one state change is deferred 350 ms behind a placing flag so a second call cannot double-order; revocation for every other reason stays immediate.

Both headers WebMCP asks for are set in next.config.ts and are live on the deployment: Origin-Agent-Cluster: ?1 and Permissions-Policy: tools=(self).

Evidence. A 105-assertion capability audit drives the real document.modelContext.executeTool path in flagged Chrome 152 not unit tests, and not a mocked tool host. Screen-moving tools are asserted against the DOM rather than against their own return value, and the gate is opened by clicking the actual approve button. 105 passed, 0 failed, both locally and against the deployed URL. A separate run through ChatGPT's in-app browser confirmed the same tools register and execute there, with the gate working in both directions. A production in-app-browser shopping run then had a model select actions from an uncoached shopping prompt, compare candidates, propose the needed budget change, and complete the approved order. It also surfaced an honest test-plan wrinkle: because changing delivery details revokes consent, the prompt's order required a second final approval.

No backend, no database, no API routes. Next.js 16, React 19, Tailwind v4, TypeScript, deployed on Vercel.

Built With

Share this project:

Updates

Submission history