Define a tool
A tool is the function that does the work, plus an optional hint about which control it belongs to. Nothing is added to your JSX.
A tool is something an agent can do on your page. You write one as execute, the function that does the work,
plus an optional target when the cursor needs to be told which control the tool belongs to. Nothing is added to
your JSX for the cursor: no attribute, no ref, no id.
A complete example
One function, two callers. Write the tool where its work lives, and pick the tab that fits:
useWebTool, in a component: the tool is on while the component is on the page, andexecutereads the component’s own state.defineWebTool, in a module of its own: the tool belongs to the whole app, reads a store, and imports no React, so a server file can import it too. The provider registers it.
Both take the same options.
src/components/clear-cart.tsx
import { useWebTool } from "@webmcp-agents/react";
import { useState } from "react";
import { Button } from "@/components/ui/button";
export function ClearCart() {
const [items, setItems] = useState(["Ocean Tee", "Canvas Tote"]);
const clear = () => {
setItems([]);
return "The cart is empty.";
};
// An agent calls this.
useWebTool({
id: "clear_cart",
title: "Clear cart",
description:
"Empty the cart. Use when the user asks to start over. " +
"The user confirms on the page.",
destructive: true,
requireConfirmation: true,
execute: clear,
});
// A person clicks this.
return (
<div className="flex flex-col items-start gap-3">
<p>
{items.length > 0 ? items.join(", ") : "The cart is empty."}
</p>
<Button variant="destructive" onClick={clear}>
Clear cart
</Button>
</div>
);
}src/tools/clear-cart.ts
import { defineWebTool } from "@webmcp-agents/core";
import { cart } from "../lib/cart";
// An agent calls this.
export const clearCart = defineWebTool({
id: "clear_cart",
title: "Clear cart",
description:
"Empty the cart. Use when the user asks to start over. " +
"The user confirms on the page.",
destructive: true,
requireConfirmation: true,
execute: () => {
cart.clear();
return "The cart is empty.";
},
});src/components/clear-cart-button.tsx
import { Button } from "@/components/ui/button";
import { cart } from "@/lib/cart";
// A person clicks this.
export function ClearCartButton() {
return (
<Button variant="destructive" onClick={() => cart.clear()}>
Clear cart
</Button>
);
}components/clear-cart.tsx
"use client";
import { useWebTool } from "@webmcp-agents/react";
import { useState } from "react";
import { Button } from "@/components/ui/button";
export function ClearCart() {
const [items, setItems] = useState(["Ocean Tee", "Canvas Tote"]);
const clear = () => {
setItems([]);
return "The cart is empty.";
};
// An agent calls this.
useWebTool({
id: "clear_cart",
title: "Clear cart",
description:
"Empty the cart. Use when the user asks to start over. " +
"The user confirms on the page.",
destructive: true,
requireConfirmation: true,
execute: clear,
});
// A person clicks this.
return (
<div className="flex flex-col items-start gap-3">
<p>
{items.length > 0 ? items.join(", ") : "The cart is empty."}
</p>
<Button variant="destructive" onClick={clear}>
Clear cart
</Button>
</div>
);
}tools/clear-cart.ts
import { defineWebTool } from "@webmcp-agents/core";
import { cart } from "../lib/cart";
// An agent calls this.
export const clearCart = defineWebTool({
id: "clear_cart",
title: "Clear cart",
description:
"Empty the cart. Use when the user asks to start over. " +
"The user confirms on the page.",
destructive: true,
requireConfirmation: true,
execute: () => {
cart.clear();
return "The cart is empty.";
},
});components/clear-cart-button.tsx
"use client";
import { Button } from "@/components/ui/button";
import { cart } from "@/lib/cart";
// A person clicks this.
export function ClearCartButton() {
return (
<Button variant="destructive" onClick={() => cart.clear()}>
Clear cart
</Button>
);
}src/components/clear-cart.tsx
import { useWebTool } from "@webmcp-agents/react";
import { useState } from "react";
import { Button } from "@/components/ui/button";
export function ClearCart() {
const [items, setItems] = useState(["Ocean Tee", "Canvas Tote"]);
const clear = () => {
setItems([]);
return "The cart is empty.";
};
// An agent calls this.
useWebTool({
id: "clear_cart",
title: "Clear cart",
description:
"Empty the cart. Use when the user asks to start over. " +
"The user confirms on the page.",
destructive: true,
requireConfirmation: true,
execute: clear,
});
// A person clicks this.
return (
<div className="flex flex-col items-start gap-3">
<p>
{items.length > 0 ? items.join(", ") : "The cart is empty."}
</p>
<Button variant="destructive" onClick={clear}>
Clear cart
</Button>
</div>
);
}src/tools/clear-cart.ts
import { defineWebTool } from "@webmcp-agents/core";
import { cart } from "../lib/cart";
// An agent calls this.
export const clearCart = defineWebTool({
id: "clear_cart",
title: "Clear cart",
description:
"Empty the cart. Use when the user asks to start over. " +
"The user confirms on the page.",
destructive: true,
requireConfirmation: true,
execute: () => {
cart.clear();
return "The cart is empty.";
},
});src/components/clear-cart-button.tsx
import { Button } from "@/components/ui/button";
import { cart } from "@/lib/cart";
// A person clicks this.
export function ClearCartButton() {
return (
<Button variant="destructive" onClick={() => cart.clear()}>
Clear cart
</Button>
);
}src/components/clear-cart.tsx
import { useWebTool } from "@webmcp-agents/react";
import { useState } from "react";
import { Button } from "@/components/ui/button";
export function ClearCart() {
const [items, setItems] = useState(["Ocean Tee", "Canvas Tote"]);
const clear = () => {
setItems([]);
return "The cart is empty.";
};
// An agent calls this.
useWebTool({
id: "clear_cart",
title: "Clear cart",
description:
"Empty the cart. Use when the user asks to start over. " +
"The user confirms on the page.",
destructive: true,
requireConfirmation: true,
execute: clear,
});
// A person clicks this.
return (
<div className="flex flex-col items-start gap-3">
<p>
{items.length > 0 ? items.join(", ") : "The cart is empty."}
</p>
<Button variant="destructive" onClick={clear}>
Clear cart
</Button>
</div>
);
}src/tools/clear-cart.ts
import { defineWebTool } from "@webmcp-agents/core";
import { cart } from "../lib/cart";
// An agent calls this.
export const clearCart = defineWebTool({
id: "clear_cart",
title: "Clear cart",
description:
"Empty the cart. Use when the user asks to start over. " +
"The user confirms on the page.",
destructive: true,
requireConfirmation: true,
execute: () => {
cart.clear();
return "The cart is empty.";
},
});src/components/clear-cart-button.tsx
import { Button } from "@/components/ui/button";
import { cart } from "@/lib/cart";
// A person clicks this.
export function ClearCartButton() {
return (
<Button variant="destructive" onClick={() => cart.clear()}>
Clear cart
</Button>
);
}Either way, when an agent calls clear_cart, the cursor finds the button named “Clear cart”, parks on it because of
requireConfirmation, and the work runs after the person’s own click. What execute returns is the agent’s answer.
A defineWebTool tool is registered by handing it to the provider (tools={{ clearCart }}), which is the subject of
Tools across your app. A useWebTool tool registers itself. The two also meet:
useWebTool(clearCart) turns a defined tool on only while a component is mounted
(A defined tool, on with a component).
How the cursor finds the control
The cursor needs one thing from a tool: which control on the page it belongs to. It looks for it by role and name, the way a screen reader names things.
- The name it looks for is the tool’s
title, or itsidread as words (clear_cartis “clear cart”), among buttons, links, tabs, menu items and switches. - A control’s name is its accessible name:
aria-labelledby,aria-label, its<label>, itsplaceholder, then its visible text,altandtitle. Case and extra spaces do not matter, and an exact match beats one that only starts with the name (“Place order” finds “Place order · $46.00”). - Icon-only buttons need an
aria-label. It is their name for the cursor, and for screen readers. - No control, no cursor. A tool with nothing to point at runs at once, unseen, which is right for a read like
get_cart. In development, the console says so once for any other tool.
When the control depends on the call
Say which control with target: a locator, a CSS selector, or a function of the call’s input that returns either.
Here each row has its own button, so the tool picks one per call.
import { useWebTool } from "@webmcp-agents/react";
import { Trash2 } from "lucide-react";
import { useState } from "react";
import { z } from "zod";
import { Button } from "@/components/ui/button";
const LINES = [
{ id: "tee", name: "Ocean Tee" },
{ id: "tote", name: "Canvas Tote" },
];
export function CartLines() {
const [lines, setLines] = useState(LINES);
const remove = (lineId: string) => {
const line = lines.find((line) => line.id === lineId);
if (!line) throw new Error(`No line "${lineId}" in the cart.`);
setLines(lines.filter((line) => line.id !== lineId));
return `Removed ${line.name}.`;
};
// An agent calls this.
useWebTool({
id: "remove_from_cart",
title: "Remove from cart",
description:
"Remove one line from the cart. " +
`Lines: ${lines.map((line) => line.id).join(", ") || "none"}.`,
inputSchema: z.object({
lineId: z
.string()
.describe("The id of the line to remove, e.g. tee"),
}),
// Which button? The one a screen reader calls "Remove Ocean Tee".
target: ({ lineId }) => ({
role: "button",
name: `Remove ${lines.find((line) => line.id === lineId)?.name}`,
}),
execute: ({ lineId }) => remove(lineId),
});
// A person clicks these.
return (
<ul className="flex flex-col gap-2">
{lines.map((line) => (
<li key={line.id} className="flex items-center gap-2">
{line.name}
<Button
variant="ghost"
size="icon"
aria-label={`Remove ${line.name}`}
onClick={() => remove(line.id)}
>
<Trash2 />
</Button>
</li>
))}
</ul>
);
}"use client";
import { useWebTool } from "@webmcp-agents/react";
import { Trash2 } from "lucide-react";
import { useState } from "react";
import { z } from "zod";
import { Button } from "@/components/ui/button";
const LINES = [
{ id: "tee", name: "Ocean Tee" },
{ id: "tote", name: "Canvas Tote" },
];
export function CartLines() {
const [lines, setLines] = useState(LINES);
const remove = (lineId: string) => {
const line = lines.find((line) => line.id === lineId);
if (!line) throw new Error(`No line "${lineId}" in the cart.`);
setLines(lines.filter((line) => line.id !== lineId));
return `Removed ${line.name}.`;
};
// An agent calls this.
useWebTool({
id: "remove_from_cart",
title: "Remove from cart",
description:
"Remove one line from the cart. " +
`Lines: ${lines.map((line) => line.id).join(", ") || "none"}.`,
inputSchema: z.object({
lineId: z
.string()
.describe("The id of the line to remove, e.g. tee"),
}),
// Which button? The one a screen reader calls "Remove Ocean Tee".
target: ({ lineId }) => ({
role: "button",
name: `Remove ${lines.find((line) => line.id === lineId)?.name}`,
}),
execute: ({ lineId }) => remove(lineId),
});
// A person clicks these.
return (
<ul className="flex flex-col gap-2">
{lines.map((line) => (
<li key={line.id} className="flex items-center gap-2">
{line.name}
<Button
variant="ghost"
size="icon"
aria-label={`Remove ${line.name}`}
onClick={() => remove(line.id)}
>
<Trash2 />
</Button>
</li>
))}
</ul>
);
}import { useWebTool } from "@webmcp-agents/react";
import { Trash2 } from "lucide-react";
import { useState } from "react";
import { z } from "zod";
import { Button } from "@/components/ui/button";
const LINES = [
{ id: "tee", name: "Ocean Tee" },
{ id: "tote", name: "Canvas Tote" },
];
export function CartLines() {
const [lines, setLines] = useState(LINES);
const remove = (lineId: string) => {
const line = lines.find((line) => line.id === lineId);
if (!line) throw new Error(`No line "${lineId}" in the cart.`);
setLines(lines.filter((line) => line.id !== lineId));
return `Removed ${line.name}.`;
};
// An agent calls this.
useWebTool({
id: "remove_from_cart",
title: "Remove from cart",
description:
"Remove one line from the cart. " +
`Lines: ${lines.map((line) => line.id).join(", ") || "none"}.`,
inputSchema: z.object({
lineId: z
.string()
.describe("The id of the line to remove, e.g. tee"),
}),
// Which button? The one a screen reader calls "Remove Ocean Tee".
target: ({ lineId }) => ({
role: "button",
name: `Remove ${lines.find((line) => line.id === lineId)?.name}`,
}),
execute: ({ lineId }) => remove(lineId),
});
// A person clicks these.
return (
<ul className="flex flex-col gap-2">
{lines.map((line) => (
<li key={line.id} className="flex items-center gap-2">
{line.name}
<Button
variant="ghost"
size="icon"
aria-label={`Remove ${line.name}`}
onClick={() => remove(line.id)}
>
<Trash2 />
</Button>
</li>
))}
</ul>
);
}import { useWebTool } from "@webmcp-agents/react";
import { Trash2 } from "lucide-react";
import { useState } from "react";
import { z } from "zod";
import { Button } from "@/components/ui/button";
const LINES = [
{ id: "tee", name: "Ocean Tee" },
{ id: "tote", name: "Canvas Tote" },
];
export function CartLines() {
const [lines, setLines] = useState(LINES);
const remove = (lineId: string) => {
const line = lines.find((line) => line.id === lineId);
if (!line) throw new Error(`No line "${lineId}" in the cart.`);
setLines(lines.filter((line) => line.id !== lineId));
return `Removed ${line.name}.`;
};
// An agent calls this.
useWebTool({
id: "remove_from_cart",
title: "Remove from cart",
description:
"Remove one line from the cart. " +
`Lines: ${lines.map((line) => line.id).join(", ") || "none"}.`,
inputSchema: z.object({
lineId: z
.string()
.describe("The id of the line to remove, e.g. tee"),
}),
// Which button? The one a screen reader calls "Remove Ocean Tee".
target: ({ lineId }) => ({
role: "button",
name: `Remove ${lines.find((line) => line.id === lineId)?.name}`,
}),
execute: ({ lineId }) => remove(lineId),
});
// A person clicks these.
return (
<ul className="flex flex-col gap-2">
{lines.map((line) => (
<li key={line.id} className="flex items-center gap-2">
{line.name}
<Button
variant="ghost"
size="icon"
aria-label={`Remove ${line.name}`}
onClick={() => remove(line.id)}
>
<Trash2 />
</Button>
</li>
))}
</ul>
);
}target |
Example | Use it when |
|---|---|---|
A locator { role?, name?, within? } |
{ role: "button", name: "Empty the cart" } |
The button’s name differs from the tool’s title. name takes a string or a RegExp; within narrows to a part of the page. |
| A CSS selector | "#empty-cart" |
The control has an id and no good name. |
| A function of the input | ({ lineId }) => ({ role: "button", name: `Remove ${lines[lineId]}` }) |
The control depends on the call, as above, or on the UI’s language: () => ({ role: "button", name: t("cart.clear") }). |
What the agent reads
Two things reach the agent: the description, before it calls, and what execute returns, after.
descriptionis how the agent decides. Say what the tool does, when to use it, what it needs first and what comes next. Writing tool descriptions has the four things to say.- The return value is the answer. A sentence (“The cart is empty.”), or data. Throw when the tool cannot do its job: the agent reads the message as an error result with a code, and the cursor never moves for input that fails the schema.
Try it
Open the devtools panel (bottom right, or Alt+Shift+A), expand clear_cart and press Run. Or call the tool
from the browser console, the way an agent would. agent is there in development, put there by the devtools.
await agent.invoke("clear_cart");
Either way the cursor parks on Clear cart until you click it. Scroll or click while it travels: it gets out of the way, and the tool still finishes.
Options you’ll use most
| Option | What it is for |
|---|---|
id |
The tool’s name, unique on the page. Snake case reads best to agents: clear_cart. |
title |
A human name, and the name the cursor looks for. |
description |
What the agent reads to decide when to call it. |
inputSchema |
The tool’s parameters, as a zod (or any Standard Schema) object or JSON Schema. Tools with input. |
execute |
The work. Its return value is the agent’s answer. |
target |
Which control, when the name is not enough. |
requireConfirmation |
Wait for the person’s own click. Confirm before it runs. |
readOnly, destructive, repeatable, external |
Say what kind of tool it is, in plain words. Agents read them as WebMCP annotations. |
Every option, with its type and default: useWebTool and
defineWebTool.