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.
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.
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 + ".";
}
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.
// 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 }] }; } }); }
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 }] };
}
});
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.
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.