---
title: Define a tool
description: 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.
sidebar:
  label: 1. Define a tool
---

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.

:::note[shadcn/ui and React Hook Form are optional]
The Build examples use [shadcn/ui](https://ui.shadcn.com) components and React Hook Form, because that is how most
React apps are built today. Each page says which components its examples use; this one uses only the button:

```package-install
npx shadcn@latest add button
```

Any component library, or plain elements, works the same: the cursor finds a control by its role and name.
:::

## 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, and `execute` reads 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.

**Vite**

**useWebTool**

**`src/components/clear-cart.tsx`**

```tsx title="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>
  );
}
```

**defineWebTool**

**`src/tools/clear-cart.ts`**

```ts title="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`**

```tsx title="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>
  );
}
```

**Next.js**

**useWebTool**

**`components/clear-cart.tsx`**

```tsx title="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>
  );
}
```

**defineWebTool**

**`tools/clear-cart.ts`**

```ts title="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`**

```tsx title="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>
  );
}
```

**TanStack Start**

**useWebTool**

**`src/components/clear-cart.tsx`**

```tsx title="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>
  );
}
```

**defineWebTool**

**`src/tools/clear-cart.ts`**

```ts title="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`**

```tsx title="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>
  );
}
```

**Astro**

**useWebTool**

**`src/components/clear-cart.tsx`**

```tsx title="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>
  );
}
```

**defineWebTool**

**`src/tools/clear-cart.ts`**

```ts title="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`**

```tsx title="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](/docs/build/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](/docs/build/tools-across-your-app#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 its `id` read as words (`clear_cart` is "clear cart"), among
  buttons, links, tabs, menu items and switches.
- **A control's name is its accessible name**: `aria-labelledby`, `aria-label`, its `<label>`, its `placeholder`,
  then its visible text, `alt` and `title`. 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.

**Vite**

```tsx title="src/components/cart-lines.tsx"
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>
  );
}
```

**Next.js**

```tsx title="components/cart-lines.tsx"
"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>
  );
}
```

**TanStack Start**

```tsx title="src/components/cart-lines.tsx"
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>
  );
}
```

**Astro**

```tsx title="src/components/cart-lines.tsx"
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.

- **`description`** is how the agent decides. Say what the tool does, when to use it, what it needs first and what
  comes next. [Writing tool descriptions](/docs/concepts/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](/docs/concepts/errors), 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.

```js
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](/docs/build/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](/docs/build/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`](/docs/reference/react/use-web-tool) and
[`defineWebTool`](/docs/reference/core/define-web-tool).
