labs | 03 | byo tools
lab 03 | ~12 min | the hands-on hour

Three of your tools. One product page. Agent-callable.

The store below already works for humans. Search filters the list. Add-to-cart increments. Save-for-later toggles. Your job is to make all three actions callable by an agent. You register three tools against the functions that already exist: add_to_cart, search_products, save_item. The scaffold below has TODO comments on every blank. Fill them, paste into your own page, done.

Checking for WebMCP...
step 1

Meet the page you are instrumenting.

Here is the human-only version. A product catalog, three functions, three event handlers. Nothing agent-specific. This is the shape of a page you already ship. The functions return values because your tools will hand those returns back to the agent.

the existing human page (already works)
const PRODUCTS = [
  { id: "tote",  name: "Frontier Tote",   price: 24, tags: "bag canvas carry" },
  { id: "mug",   name: "Builder Mug",     price: 14, tags: "ceramic coffee kitchen" },
  { id: "cap",   name: "Two-Caller Cap",  price: 28, tags: "hat cotton wear" },
  { id: "socks", name: "Semantic Socks",  price: 12, tags: "wool warm wear" }
];
let cart = 0;
const saved = new Set();

function addToCart(productId, qty) {
  const p = PRODUCTS.find(x => x.id === productId);
  if (!p) return "No product with id " + productId + ".";
  const n = Math.max(1, parseInt(qty, 10) || 1);
  cart += n;
  render();
  return "Added " + n + " x " + p.name + ". Cart total: " + cart + ".";
}

function searchProducts(query) {
  const q = (query || "").toLowerCase().trim();
  const hits = PRODUCTS.filter(p =>
    (p.name + " " + p.tags).toLowerCase().includes(q));
  render(q);
  return hits.map(p => p.name + " ($" + p.price + ", id: " + p.id + ")");
}

function saveItem(productId) {
  const p = PRODUCTS.find(x => x.id === productId);
  if (!p) return "No product with id " + productId + ".";
  saved.add(productId);
  render();
  return "Saved " + p.name + " for later. Saved items: " + saved.size + ".";
}
step 2

Fill in the scaffold. Three tools, one guard.

This is the exercise. One feature-detect wraps all three registrations. search_products is done for you as the worked example. Fill the TODOs for add_to_cart and save_item. Each tool needs a name, a description written for a small model, a inputSchema schema, and an execute that calls the matching function and returns its string as MCP content.

your exercise | fill the TODOs
// One guard for all three. Never register without it.
if (navigator.modelContext && typeof navigator.modelContext.registerTool === "function") {

  // --- WORKED EXAMPLE: search_products (leave this as your template) ---
  navigator.modelContext.registerTool({
    name: "search_products",
    description: "Search the catalog by name or keyword. "
               + "Returns matching product names, prices, and ids.",
    inputSchema: {
      type: "object",
      properties: { query: { type: "string", description: "Search text, e.g. 'bag' or 'coffee'." } },
      required: ["query"]
    },
    execute: async (args) => {
      const hits = searchProducts(args && args.query);
      return { content: [{ type: "text", text: hits.length
        ? "Found: " + hits.join("; ") : "No matches." }] };
    }
  });

  // --- TOOL 2: add_to_cart ---
  navigator.modelContext.registerTool({
    name: "add_to_cart",
    // TODO: describe it for a small model. What does it do, what does it return?
    description: "",
    inputSchema: {
      type: "object",
      properties: {
        // TODO: which product, and how many? product_id is required, quantity is not.
      },
      required: [ /* TODO */ ]
    },
    execute: async (args) => {
      // TODO: call addToCart(...) with the args, wrap the return in MCP content.
      const msg = addToCart(args && args.product_id, args && args.quantity);
      return { content: [{ type: "text", text: msg }] };
    }
  });

  // --- TOOL 3: save_item ---
  navigator.modelContext.registerTool({
    name: "save_item",
    // TODO: describe save-for-later. One product id in, a confirmation out.
    description: "",
    inputSchema: {
      type: "object",
      properties: {
        // TODO: product_id, a string, required.
      },
      required: [ /* TODO */ ]
    },
    execute: async (args) => {
      // TODO: call saveItem(...) and return its string as content.
      const msg = saveItem(args && args.product_id);
      return { content: [{ type: "text", text: msg }] };
    }
  });
}
reveal the filled solution
solution | the two TODO tools completed
navigator.modelContext.registerTool({
  name: "add_to_cart",
  description: "Add a product to the shopping cart by its id. "
             + "Optionally set how many units. Returns the new cart total.",
  inputSchema: {
    type: "object",
    properties: {
      product_id: { type: "string", description: "The product id, e.g. 'tote' or 'mug'." },
      quantity:   { type: "integer", minimum: 1, description: "Units to add. Defaults to 1." }
    },
    required: ["product_id"]
  },
  execute: async (args) => {
    const msg = addToCart(args && args.product_id, args && args.quantity);
    return { content: [{ type: "text", text: msg }] };
  }
});

navigator.modelContext.registerTool({
  name: "save_item",
  description: "Save a product to the shopper's saved-for-later list by its id. "
             + "Returns a confirmation and the saved count.",
  inputSchema: {
    type: "object",
    properties: {
      product_id: { type: "string", description: "The product id to save, e.g. 'cap'." }
    },
    required: ["product_id"]
  },
  execute: async (args) => {
    const msg = saveItem(args && args.product_id);
    return { content: [{ type: "text", text: msg }] };
  }
});
step 3

Drive the running page.

The store below has all three tools registered and working, so you can see the target behavior. Use it as a human, then open the Tool Inspector and invoke each tool: search_products {"query":"wear"}, add_to_cart {"product_id":"cap","quantity":2}, save_item {"product_id":"mug"}. Every call, human or agent, lands in the log with its source.

live store | three tools, two callerstools: not registered
cart
0
saved
0

activity log

  • No calls yet. Search, add, or save, or invoke a tool from the Inspector.

the lesson under the exercise

Three tools took the same shape: a name an agent would search for, a description that says what it does and what it returns, a typed inputSchema schema, and an execute that reuses a function you already had. The write action (add_to_cart), the read action (search_products), and the state toggle (save_item) all fit it. That is most of a product surface. What does not fit this shape is anything destructive. That is the next lab.