labs | 02 | cart
lab 02 | ~8 min | the hero demo

One button. One counter. The agent presses it.

This is the demo the whole night is built around. A page has a real <button id="add-to-cart"> and a counter that ticks when you click it. Ten lines register an add_to_cart tool. Then you open the Tool Inspector, invoke the tool from outside the page, and the counter moves without anyone touching the button. The most legible "agent did that" moment in modern web. The counter below is live.

Checking for WebMCP...
step 1

Start with a page that already works for humans.

Semantic HTML first. A real <button>, not a <div onClick>, so it surfaces in the accessibility tree and the click handler owns the state. The agent layer comes second and reuses this exact handler.

the human page
<button id="add-to-cart">Add to cart</button>
<span>Cart: <strong id="cart-count">0</strong></span>

<script>
  let cart = 0;

  // The one function both callers use.
  function addToCart(qty) {
    const n = Math.max(1, parseInt(qty, 10) || 1);
    cart += n;
    document.getElementById("cart-count").textContent = cart;
    return cart;
  }

  document.getElementById("add-to-cart")
    .addEventListener("click", () => addToCart(1));
</script>
step 2

Register add_to_cart. Ten lines.

Feature-detect, then register. The execute calls the same addToCart the button calls, and returns the new cart total as an MCP text result so the agent knows it worked. That return value is the agent's confirmation.

the agent layer
if (navigator.modelContext && typeof navigator.modelContext.registerTool === "function") {
  navigator.modelContext.registerTool({
    name: "add_to_cart",
    description: "Add the current product to the shopping cart. "
               + "Optionally pass how many units to add.",
    inputSchema: {
      type: "object",
      properties: {
        quantity: { type: "integer", minimum: 1, description: "Units to add. Defaults to 1." }
      }
    },
    execute: async (args) => {
      const total = addToCart(args && args.quantity);
      return { content: [{ type: "text", text: "Added to cart. New total: " + total + "." }] };
    }
  });
}
step 3

Click it as a human first.

Press Add to cart below. The counter ticks. The log records a human call. Nothing surprising yet. This is just a store.

live store | agents and humans share this buttonadd_to_cart: not registered
product
Frontier Tote
$24.00
0in cart

activity log

  • Empty cart. Click Add to cart, or invoke add_to_cart from the Tool Inspector.
step 4

Now let the agent press it.

Open the WebMCP Tool Inspector extension on this page. You will see add_to_cart in the tool list, with the schema you wrote. Invoke it with { "quantity": 3 }. Watch the counter jump by three and a new line appear tagged agent. Nobody touched the button. That is the whole demo, and it lands every time.

No Canary set up yet? The Simulate agent call button runs the tool's own execute locally so you can see the agent path first, then wire up the real inspector.

what the agent reads back
{ "content": [ { "type": "text", "text": "Added to cart. New total: 3." } ] }

why this is the demo

An agent scraping this page would have to find the button in the DOM, guess that it adds to a cart, click it, and re-read the page to confirm. That is the browser tax: latency, guesswork, and a retry when the guess is wrong. With add_to_cart registered, the agent reads a named tool with a typed schema and a return value that confirms the result. No scraping, no guessing, no retry. Ten lines bought that.

the trap you just dodged

The <div onClick> button. If your add-to-cart is a styled div with a click handler, it never surfaces in the accessibility tree, and a scraping agent cannot even find it. WebMCP does not fix that page. Register the tool on a real <button> whose handler owns the state, and both callers get a clean path. Semantic HTML is still the floor. WebMCP is the ceiling on top of it.