We will be undergoing planned maintenance on Oct 7th 6:00AM UTC / Oct 7th 2:00AM ET

Inspiration

There are only two ways to let an AI act for you on a website today. Hand it your browser, and it inherits your entire logged-in session — every page, every saved card. Or issue it a token, which outlives the session and can be replayed somewhere you'll never see. There is no third option that says "just a little bit."

WebMCP is that third option, and we wanted to build the smallest thing that makes the difference obvious. Meal planning turned out to be perfect: it's a dependency graph pretending to be a list, and a grocery account holds exactly the things an agent has no business touching — a card, an address, an order history, a spend limit.

What it does

Tell CookTree what you want for dinner. It plans the week, works out what's already in your kitchen, dedupes ingredients across dishes, rounds up to real pack sizes, and gives you a list of things to actually buy. Twenty-one ingredient requests collapse into sixteen ingredients and eight purchases.

Say "drop the mapo tofu" and it returns the diff — three lines gone, −$14.70 — not a page you have to re-read. Say "I used up the garlic" and the buy list grows to match. Describe a dish it has never heard of, in English or Chinese, and it generates a real recipe with quantities, cookware, and a photograph, then plans it through the same engine as the built-in dishes.

Then ask it to change your delivery address. It can't. Not blocked, not password-protected — the site never registered that function. Thirteen tools exist; that isn't one of them.

How we built it

One static HTML file, no build step. Thirteen tools defined once in §4 TOOLS[] and registered individually with document.modelContext.registerTool(), falling back to navigator.modelContext and to provideContext({tools}) where registerTool is absent.

Every tool body — and every button in the UI — routes through a single invoke(name, args) function, so there is no separate agent path that can drift out of sync with what people see. Clicking an ingredient fires explain_shortage; clicking a dish fires plan_week. It all shows up in the on-page agent console as tool calls, because it is all the same layer.

Two Vercel Functions sit behind it so provider keys never reach the browser or the repo: one turns free text into a plannable dish (DeepSeek for the recipe, Nano Banana 2 Lite for the photo, in parallel), the other turns shortages into live Shopify Global Catalog listings and real merchant carts.

We chose the tool set with one question per candidate: if this fired a hundred times by mistake, what happens? Nothing → expose. A draft gets messy → expose. A retailer handoff is created → expose, but gate it on a human. Money moves → keep the final action on the retailer's page. The account becomes someone else's → never expose. That last answer is why update_delivery_address, update_payment_method, read_order_history and update_spend_limit have no tool.

Challenges we ran into

The spec is still moving. The context object has lived on both document and navigator during the draft, and both spellings are still in the wild, so we probe for both. registerTool returns a promise, which a synchronous try/catch will not catch — a registration failure could leave the status pill claiming success.

We trusted the model's numbers once, and shouldn't have. A generated recipe asked for 3 g of onion. An onion is a 200 g pack. Now the site validates every quantity against its own pack sizes and says in the console when it corrects one. The model is a source, not an authority.

A hardcoded string nearly lied to the judges. The console header shipped with 8 tools registered, which JavaScript overwrites at load. When we opened the live site in ChatGPT's plain browsing — which reads rendered text without executing the page — it reported eight tools. Anything that doesn't run the script saw the wrong number.

Deployment split in half. GitHub Pages auto-deployed but has no /api routes; Vercel had the routes but kept serving a stale build, because the Vercel GitHub App was installed on the account without being authorized for this repository. Diagnosing that took a cache-busting fetch and reading response headers, not guessing.

Accomplishments that we're proud of

We verified the argument in someone else's agent. ChatGPT's Cloud Browser opened the live site, enumerated all thirteen tools by name, and called get_kitchen successfully. Then we asked it to change the delivery address, and it came back with:

→ update_delivery_address {"address":"42 Mission St"} ✕ no such tool — 13 registered, this is not one of them the site never exposed it · a click-agent would open Settings and change it

That refusal is not our demo script talking to itself. A real third-party agent tried, and the site had nothing to give it.

We're also proud that checkout stops where it does. CookTree sends product names and quantities to Shopify, gets back real listings and real merchant carts, and then stops — the person pays on the merchant's own page. That boundary is the design, not an unfinished edge: a version of this that took payment would be a version that had to hold a card.

What we learned

Splitting is what buys the site choice. The interesting granularity isn't categories, it's scope. get_order_status is registered — it covers handoffs this agent created in this tab. Reading the retailer account's order history is a different scope and has no tool. The same noun splits into an exposed half and a withheld half, and you only get that by registering functions instead of handing over a session.

And an honest boundary: this is least privilege, not a security perimeter. An agent that already has DOM access can go around all of it. The point isn't that the site is armored. It's that a cooperating agent never needs that access in the first place, and the site — for the first time — gets to define the surface it's willing to support.

Cooking is just the version you can understand in ninety seconds. The mechanism matters most in banking, health portals, and internal admin consoles, where the list of things a site must not hand over is long.

What's next for Cooktree

Make the Agent/Manual switch real. Right now it only disables the chat box. The spec's registerTool(..., { signal }) means a site can revoke its tool layer with an AbortController — so the toggle could genuinely withdraw all thirteen tools and drop the pill to zero. A site that can choose what to expose should also be able to choose when.

Surface the withheld set as a first-class thing. Today the four unregistered functions are visible in the account panel and in the docs. They should be part of the protocol conversation: a site should be able to tell an agent "this exists and I'm not giving it to you," so the agent can say so to the user instead of failing blind.

Take it somewhere with real stakes. The same shape — a small set of typed functions, a human gate on the one that reaches outside, and a withheld list you can point at — is what a bank or a patient portal would need. Groceries were the version we could ship in three days.

Built With

Share this project:

Updates

Submission history