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

Confirm before it runs

requireConfirmation makes a tool wait for the person's own click on its control: the cursor parks on the button, and only a real click runs the tool.

Anything the person would regret (an order, a booking, a deletion) should wait for their click. Set requireConfirmation: true: the agent’s call validates its input, the cursor flies to the tool’s control and parks there, and the tool runs only when the person clicks it.

A complete example

import { useWebTool } from "@webmcp-agents/react";
import { useState } from "react";

import { Button } from "@/components/ui/button";
import { cart } from "@/lib/cart";

export function PlaceOrder() {
  const [placed, setPlaced] = useState("");
  const total = `$${cart.total().toFixed(2)}`;

  const place = () => {
    const id = `FS-${Date.now().toString(36).toUpperCase()}`;
    setPlaced(`Order ${id} placed.`);
    return `Order ${id} placed, ${total}.`;
  };

  // An agent calls this. It runs on the person's click.
  useWebTool({
    id: "place_order",
    title: "Place order",
    description:
      "Place the order for what is in the cart. Use when the user " +
      "says to buy. Read the cart with get_cart first. The user " +
      "confirms on the page.",
    requireConfirmation: true,
    external: true,
    execute: place,
  });

  // A person clicks this. Its name starts with the tool's title.
  return (
    <div className="flex flex-col items-start gap-3">
      <Button onClick={place}>Place order · {total}</Button>

      {placed && <p>{placed}</p>}
    </div>
  );
}
"use client";

import { useWebTool } from "@webmcp-agents/react";
import { useState } from "react";

import { Button } from "@/components/ui/button";
import { cart } from "@/lib/cart";

export function PlaceOrder() {
  const [placed, setPlaced] = useState("");
  const total = `$${cart.total().toFixed(2)}`;

  const place = () => {
    const id = `FS-${Date.now().toString(36).toUpperCase()}`;
    setPlaced(`Order ${id} placed.`);
    return `Order ${id} placed, ${total}.`;
  };

  // An agent calls this. It runs on the person's click.
  useWebTool({
    id: "place_order",
    title: "Place order",
    description:
      "Place the order for what is in the cart. Use when the user " +
      "says to buy. Read the cart with get_cart first. The user " +
      "confirms on the page.",
    requireConfirmation: true,
    external: true,
    execute: place,
  });

  // A person clicks this. Its name starts with the tool's title.
  return (
    <div className="flex flex-col items-start gap-3">
      <Button onClick={place}>Place order · {total}</Button>

      {placed && <p>{placed}</p>}
    </div>
  );
}
import { useWebTool } from "@webmcp-agents/react";
import { useState } from "react";

import { Button } from "@/components/ui/button";
import { cart } from "@/lib/cart";

export function PlaceOrder() {
  const [placed, setPlaced] = useState("");
  const total = `$${cart.total().toFixed(2)}`;

  const place = () => {
    const id = `FS-${Date.now().toString(36).toUpperCase()}`;
    setPlaced(`Order ${id} placed.`);
    return `Order ${id} placed, ${total}.`;
  };

  // An agent calls this. It runs on the person's click.
  useWebTool({
    id: "place_order",
    title: "Place order",
    description:
      "Place the order for what is in the cart. Use when the user " +
      "says to buy. Read the cart with get_cart first. The user " +
      "confirms on the page.",
    requireConfirmation: true,
    external: true,
    execute: place,
  });

  // A person clicks this. Its name starts with the tool's title.
  return (
    <div className="flex flex-col items-start gap-3">
      <Button onClick={place}>Place order · {total}</Button>

      {placed && <p>{placed}</p>}
    </div>
  );
}
import { useWebTool } from "@webmcp-agents/react";
import { useState } from "react";

import { Button } from "@/components/ui/button";
import { cart } from "@/lib/cart";

export function PlaceOrder() {
  const [placed, setPlaced] = useState("");
  const total = `$${cart.total().toFixed(2)}`;

  const place = () => {
    const id = `FS-${Date.now().toString(36).toUpperCase()}`;
    setPlaced(`Order ${id} placed.`);
    return `Order ${id} placed, ${total}.`;
  };

  // An agent calls this. It runs on the person's click.
  useWebTool({
    id: "place_order",
    title: "Place order",
    description:
      "Place the order for what is in the cart. Use when the user " +
      "says to buy. Read the cart with get_cart first. The user " +
      "confirms on the page.",
    requireConfirmation: true,
    external: true,
    execute: place,
  });

  // A person clicks this. Its name starts with the tool's title.
  return (
    <div className="flex flex-col items-start gap-3">
      <Button onClick={place}>Place order · {total}</Button>

      {placed && <p>{placed}</p>}
    </div>
  );
}

The button reads “Place order · $28.00”; the cursor finds it by the tool’s title, because a name that starts with the title matches too.

What the person sees

  • The cursor parks on the button, with a label that says what to do: Click "Place order · $28.00" to confirm. The button gets a dashed outline ([data-agent-active] in cursor.css).
  • Their click runs the tool instead of the button’s onClick, so the order is placed once, by execute, and its return value is the agent’s answer. Only a trusted click counts: a script calling button.click() cannot confirm for them.
  • Until they click, the agent waits. The call does not time out while it is parked, and the chat shows it as waiting for the person. Moving the mouse or typing in a field does not cancel it; clicking or scrolling elsewhere does.

The input is validated before the cursor moves, so the person never approves a call that then fails its schema.

How long it waits

The call waits 10 seconds for the click. After that it fails with Error [needs_user], which tells the agent to ask the person and call again, and the prompt leaves the page. It also ends early, with Error [cancelled], when the person moves on: they click anywhere but the control, scroll the page (wheel, touch, Page Down or Space), or start another tool call themself (a button or chip that runs a tool, the devtools). Input inside the chat or the devtools does not count, nor does a trackpad scroll still coasting from before the prompt appeared. An agent’s own next call never cancels it.

It needs exactly one control

A confirm-gated tool fails closed. When there is no control to click (not on the page, disabled, or gone while it waits), or when several controls match its name, the call fails with a ConfirmationUnavailableError: the agent reads Error [unavailable] and a sentence that says why, and nothing runs unconfirmed.

Three product cards, each with an “Add to cart” button, all match the name “Add to cart”. Give each button its own accessible name, and the tool the same one:

useWebTool({
  id: `add_to_cart_${product.sku}`,
  title: `Add to cart: ${product.name}`,
  requireConfirmation: true,
  // …
});

<Button type="submit" aria-label={`Add to cart: ${product.name}`}>
  Add to cart
</Button>

The visible text stays “Add to cart”, and the name starts with it, so the cursor and a screen reader agree. A target that picks one control per call works too. The devtools show “3 matches” with a warning on a tool that would fail this way.

Say what kind of tool it is

requireConfirmation changes what happens on the page. Flags say what the tool is, in plain words, for the agent to read (they become WebMCP annotations); they change nothing on the page. A confirm-gated tool usually carries one:

Flag Meaning Becomes
readOnly Only reads. Safe to call at any time. readOnlyHint
destructive Deletes or overwrites; hard to undo. clear_cart sets it. destructiveHint
repeatable Calling twice has the same effect as once. idempotentHint
external Talks to something outside the page: the network, another system. place_order sets it. openWorldHint
safeResponse false: the answer may hold text the site did not write (reviews, messages), so the agent must not follow instructions in it. untrustedContentHint, inverted

Leave a flag out when it does not apply: “not said” is not the same as false.

For a tool that needs the person in another way (“which size?”), call waitingForUser(toolName, prompt) inside execute: the chat shows the prompt, and the agent’s tool time limit stops while the person answers.

Try it

Render <PlaceOrder /> on a page, then in the browser console:

await agent.invoke("place_order");

The cursor parks on Place order and the promise waits. Click the button: the call resolves with “Order FS-… placed, $28.00.” Scroll or click while the cursor travels: it gets out of the way, and the call still waits for your click.

Options you’ll use most

Option What it is for
requireConfirmation Wait for the person’s click on the tool’s control. Default false.
title The name of the one control that confirms.
target Which control, when the title is not enough or several share it.
destructive, external Tell the agent what kind of action it is about to ask for.

Every option, with flags and annotations: defineWebTool.

Was this page helpful?