Skip to content
WebMCP Agents
Esc
↑↓navigate↵open⌘Jpreview
On this page

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, 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.

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 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.

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.

  • 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 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.

Was this page helpful?