useWebTool
Registers a tool that lives in a component and closes over its state, while the component is mounted.
Registers a tool that lives in a component and closes over its state, while the component is mounted. Pass the definition directly, as you would to defineWebTool, or a tool that is already defined. Nothing is added to the JSX for the cursor: it finds the control by role and accessible name (the tool’s title, or its id read as words) and types each parameter into the field with that name or label.
Import
import { useWebTool } from "@webmcp-agents/react";
import type { UseWebToolOptions, WebToolState } from "@webmcp-agents/react";
Example
The action is one function with two callers. When an agent calls clear_cart, the cursor finds the button named “Clear cart”, parks on it, and clear runs only after the person’s own click.
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>
);
}
In Next.js the file starts with "use client". A button of your own that runs the tool the way an agent would:
const clearCart = useWebTool({ id: "clear_cart", /* … */ });
<Button onClick={() => clearCart.invoke()}>Try it</Button>;
A tool already made with defineWebTool in a file of its own is passed as it is, and is
on while the component is mounted (and enabled). Its execute reads only its input and module state, never the
component’s:
import { useWebTool } from "@webmcp-agents/react";
import { Button } from "@/components/ui/button";
import { cart } from "@/lib/cart";
import { clearCart } from "@/tools/clear-cart";
export function CartPanel({ open }: { open: boolean }) {
// An agent calls this: the tool from tools/clear-cart.ts,
// on while this panel is mounted and open.
useWebTool(clearCart, { enabled: open });
if (!open) return null;
// A person clicks this.
return (
<aside className="flex flex-col gap-3">
<p>Total: ${cart.total().toFixed(2)}</p>
<Button variant="destructive" onClick={() => cart.clear()}>
Clear cart
</Button>
</aside>
);
}
Options
useWebTool(tool, options?). The first argument is the definition; the second holds the hook’s own options.
| Name | Type | Default | Description |
|---|---|---|---|
tool |
WebToolConfig | WebTool |
required | The definition, with every option of defineWebTool: id, title, description, inputSchema, execute, and optionally target, fields, fill, requireConfirmation and the flags. One difference: match is ignored here, because the tool is on while the component is. A tool already made with defineWebTool is used as it is. |
enabled |
boolean |
true |
Register only while true, to switch the tool by state: useWebTool(tool, { enabled: cart.items.length > 0 }). While false the tool is released like an unmount releases it. |
Returns
A WebToolState:
| Name | Type | Description |
|---|---|---|
invoke |
WebTool["invoke"] |
Call the tool the way an agent would: the cursor moves, fields are typed, confirmation applies. Resolves to what execute returned; rejects when the call fails or the tool is not registered. The same function on every render, so it is safe in dependency arrays. |
registered |
boolean |
The tool is registered with WebMCP. False until the effect runs, while enabled is false, and on pages without WebMCP. |
supported |
boolean |
The page has document.modelContext (native, or the polyfill once it has loaded). False during server rendering. |
Notes
- Calls always reach the latest render’s
execute,target,fieldsandfill, so new state never needs a re-registration (atargetthat closes overcartsees the current cart). - It re-registers only when what the agent sees changes: id, title, description, input schema, flags and annotations,
requireConfirmation, a non-functiontarget(thewebToolSignature). A new definition replaces the old one at once, since Chrome would cancel its calls anyway. - Disabling or unmounting releases the tool like the catalog does: a macrotask later (StrictMode’s remount or a quick re-enable cancels it), and only after in-flight calls settle, plus 500 ms for the answer to reach the agent. A tool that disables itself (
clear_cartemptying the cart) still answers. - The signature is computed during render, so a bad schema throws at the component, not inside an effect.
- Works without a provider. Inside a
<WebAgentsProvider>it installs the provider’s cursor before registering, so the tool is animated, and passes the provider’s earlier-formcontextasctx.context. Without one, nothing installs the cursor. - Registering an id that is already registered replaces that registration (the newest wins). Do not give a component’s tool the same id as a catalog tool.
- Inside a
<WebAgentsProvider>in a browser without WebMCP, the tool waits for the polyfill the provider loads and registers once it is there;registeredturns true then. Without WebMCP and with no polyfill coming (no provider, orpolyfill={false}), registration warns once (document.modelContext is unavailable) and does nothing, so the app still renders. - A form is a tool with an
inputSchema: React Hook Form and TanStack Form inputs hear the cursor’s typing, andfill: (values) => form.reset(values)shows values in widgets it cannot type into. See Tools with input. - With
requireConfirmation, a trusted click on the tool’s control runs the tool instead of the control’s ownonClick, so the action happens once. That includes a React AriaButton’sonPressand aLink’s navigation: the click React Aria re-fires is swallowed too. It needs exactly one matching control: give the tool atitlethat names one button, or atarget. - Icon-only buttons need an
aria-label; a translated UI uses a functiontargetreturning the translated name.
See also
- Define a tool, the page that teaches it
defineWebTool, every option of the definitionlocate, how the control and the fields are found<WebAgentsProvider>, the cursor and the catalogdeferRelease, why unmounting never cancels a call- Tools with input and Call a tool