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

Call an agent

agent.invoke runs an agent from your own code, with no chat on the page; useWebAgent adds live status in a component.

An agent has the same verb as a tool: invoke. Give it a sentence, and it decides which tools to call: the model asks for a tool, the tool runs on the page under the cursor, the result goes back, until the model answers. It is the chat’s loop without the chat, for a button that does a whole task (“Track my last order”), a command palette, or an onboarding step.

A complete example

One task, “Track my last order”, run two ways. Pick the tab that fits where the call is made:

In a component: the hook runs the agent and keeps its status live.

src/components/track-last-order.tsx

import { useWebAgent } from "@webmcp-agents/chat";

import { shopper } from "@/agents";
import { Button } from "@/components/ui/button";

export function TrackLastOrder() {
  const agent = useWebAgent(shopper);
  const busy = agent.status === "running" || agent.status === "waiting";

  return (
    <div className="flex flex-col items-start gap-3">
      <div className="flex gap-2">
        <Button
          disabled={busy}
          onClick={() => agent.invoke("Where is my last order?")}
        >
          {busy ? "Working…" : "Track my last order"}
        </Button>

        {busy && (
          <Button variant="ghost" onClick={agent.stop}>
            Stop
          </Button>
        )}
      </div>

      {/* Each tool call, live: running, waiting for you, done. */}
      <ul className="text-sm text-muted-foreground">
        {agent.steps.map((step) => (
          <li key={step.id}>
            {step.tool}: {step.state}
          </li>
        ))}
      </ul>

      {agent.text && <p>{agent.text}</p>}

      {agent.error && <p role="alert">{agent.error.message}</p>}
    </div>
  );
}

Anywhere else (an event handler in plain code, a keyboard shortcut, a test): the agent you defined has invoke on it.

import { shopper } from "@/agents";

const { text, steps } = await shopper.invoke("Where is my last order?", {
  onStep: (step) => console.log(step.tool, step.state),
});

In a component: the hook runs the agent and keeps its status live.

components/track-last-order.tsx

"use client";

import { useWebAgent } from "@webmcp-agents/chat";

import { shopper } from "@/agents";
import { Button } from "@/components/ui/button";

export function TrackLastOrder() {
  const agent = useWebAgent(shopper);
  const busy = agent.status === "running" || agent.status === "waiting";

  return (
    <div className="flex flex-col items-start gap-3">
      <div className="flex gap-2">
        <Button
          disabled={busy}
          onClick={() => agent.invoke("Where is my last order?")}
        >
          {busy ? "Working…" : "Track my last order"}
        </Button>

        {busy && (
          <Button variant="ghost" onClick={agent.stop}>
            Stop
          </Button>
        )}
      </div>

      {/* Each tool call, live: running, waiting for you, done. */}
      <ul className="text-sm text-muted-foreground">
        {agent.steps.map((step) => (
          <li key={step.id}>
            {step.tool}: {step.state}
          </li>
        ))}
      </ul>

      {agent.text && <p>{agent.text}</p>}

      {agent.error && <p role="alert">{agent.error.message}</p>}
    </div>
  );
}

Anywhere else (an event handler in plain code, a keyboard shortcut, a test): the agent you defined has invoke on it.

import { shopper } from "@/agents";

const { text, steps } = await shopper.invoke("Where is my last order?", {
  onStep: (step) => console.log(step.tool, step.state),
});

In a component: the hook runs the agent and keeps its status live.

src/components/track-last-order.tsx

import { useWebAgent } from "@webmcp-agents/chat";

import { shopper } from "@/agents";
import { Button } from "@/components/ui/button";

export function TrackLastOrder() {
  const agent = useWebAgent(shopper);
  const busy = agent.status === "running" || agent.status === "waiting";

  return (
    <div className="flex flex-col items-start gap-3">
      <div className="flex gap-2">
        <Button
          disabled={busy}
          onClick={() => agent.invoke("Where is my last order?")}
        >
          {busy ? "Working…" : "Track my last order"}
        </Button>

        {busy && (
          <Button variant="ghost" onClick={agent.stop}>
            Stop
          </Button>
        )}
      </div>

      {/* Each tool call, live: running, waiting for you, done. */}
      <ul className="text-sm text-muted-foreground">
        {agent.steps.map((step) => (
          <li key={step.id}>
            {step.tool}: {step.state}
          </li>
        ))}
      </ul>

      {agent.text && <p>{agent.text}</p>}

      {agent.error && <p role="alert">{agent.error.message}</p>}
    </div>
  );
}

Anywhere else (an event handler in plain code, a keyboard shortcut, a test): the agent you defined has invoke on it.

import { shopper } from "@/agents";

const { text, steps } = await shopper.invoke("Where is my last order?", {
  onStep: (step) => console.log(step.tool, step.state),
});

In a component: the hook runs the agent and keeps its status live.

src/components/track-last-order.tsx

import { useWebAgent } from "@webmcp-agents/chat";

import { shopper } from "@/agents";
import { Button } from "@/components/ui/button";

export function TrackLastOrder() {
  const agent = useWebAgent(shopper);
  const busy = agent.status === "running" || agent.status === "waiting";

  return (
    <div className="flex flex-col items-start gap-3">
      <div className="flex gap-2">
        <Button
          disabled={busy}
          onClick={() => agent.invoke("Where is my last order?")}
        >
          {busy ? "Working…" : "Track my last order"}
        </Button>

        {busy && (
          <Button variant="ghost" onClick={agent.stop}>
            Stop
          </Button>
        )}
      </div>

      {/* Each tool call, live: running, waiting for you, done. */}
      <ul className="text-sm text-muted-foreground">
        {agent.steps.map((step) => (
          <li key={step.id}>
            {step.tool}: {step.state}
          </li>
        ))}
      </ul>

      {agent.text && <p>{agent.text}</p>}

      {agent.error && <p role="alert">{agent.error.message}</p>}
    </div>
  );
}

Anywhere else (an event handler in plain code, a keyboard shortcut, a test): the agent you defined has invoke on it.

import { shopper } from "@/agents";

const { text, steps } = await shopper.invoke("Where is my last order?", {
  onStep: (step) => console.log(step.tool, step.state),
});

Both need the /api/chat route from Add the chat, but not the widget: by default an agent runs on the same shared adapter, which talks to that route. Both run in the browser, on a page inside your provider, so the agent sees the page’s tools.

What useWebAgent gives you

useWebAgent(agent, { adapter? }) returns:

invoke(text) Start a run. A second call stops the first. It never rejects: it resolves undefined when the run was stopped or failed, so onClick={() => agent.invoke("…")} needs no catch.
status idle, running, waiting (a tool is parked on the page until the person clicks), or error.
steps The tool calls of the current or last run, live, each with its tool, input and state. A sub-agent’s calls are included, with agent set.
text The agent’s answer so far, streamed.
error Why the last run failed, with a code. Cleared by the next invoke.
stop() Cancel the run in flight.

Each invoke is a fresh conversation: the agent remembers nothing from the last one.

What agent.invoke gives you

agent.invoke(text, { adapter?, signal?, onStep? }) is the same run with no component around it. It resolves to { text, steps, messages }: the answer, every tool call it made, and the whole conversation. Unlike the hook, it rejects: with the adapter’s message and code when the run fails, with an AbortError when its signal aborts, for an empty message, and on the server. invoke runs in the browser, because the agent’s tools are the page’s; on the server, register the agent with createChatHandler({ agents }) instead.

The agent file stays plain data that a server can import: invoke loads the browser loop on its first call.

Another model for one call

Both take an adapter. Pass createLocalAdapter() to run the same agent on a model in the browser, with no server and no key, or a createCloudAdapter({ … }) of your own. Create an adapter once, at module scope, not on every render.

Try it

Render <TrackLastOrder /> inside your provider, start the app with the key in .env.local, and click Track my last order. The button reads “Working…”, the list fills with delegate_to_orders, then the helper’s go_to, list_orders and track_order, the cursor moves to /orders, and the answer appears below. Keep a link to /orders in your layout: go_to clicks it, so your router navigates without reloading the page (a reload would end the run).

Options you’ll use most

Option Where What it is for
adapter both What runs the model. Default: one shared cloud adapter that talks to /api/chat.
signal agent.invoke Stop the run: the model, the tool in flight, any sub-agent.
onStep(step) agent.invoke Called when a tool call starts, parks on the person’s confirmation, or ends.

Reference: useWebAgent and defineWebAgent (the agent’s invoke).

Was this page helpful?