Define an agent
One definition for what the assistant knows, which tools it may use and which helpers it hands work to. The page and your server read the same file.
Your tools say what can be done on the page. An agent says how: the order the tools go in, what to ask the
person, what to avoid, and which tools are its own. defineWebAgent is the one place you write that, the way
defineWebTool is the one place you write a tool. It is plain data, so the chat in the browser and your /api/chat
route read the same file.
A complete example
Two agents: shopper on every page, and orders, a helper it hands questions about past orders to.
import { defineWebAgent } from "@webmcp-agents/chat/agent";
import { tools } from "./tools";
// Plain data: no DOM, no secrets. The page and your /api/chat route
// both import this file.
export const orders = defineWebAgent({
id: "orders",
description:
"Answers questions about past orders: what was bought, and " +
"where an order is now.",
instructions: [
"list_orders and track_order are on the orders page: " +
"go_to orders first.",
"Answer with the order's id, its status and when it arrives.",
],
tools: [tools.goTo, tools.listOrders, tools.trackOrder],
// A faster model is enough for this narrow job.
model: "anthropic/claude-sonnet-5-5",
});
export const shopper = defineWebAgent({
id: "shopper",
instructions: [
"You help people shop at Field Store.",
"Read the cart with get_cart before you change it.",
"add_to_cart is on a product's page only.",
"Hand questions about past orders to the orders agent.",
],
tools: [
tools.getTime,
tools.getPage,
tools.goTo,
tools.getCart,
// A tool a component owns (useWebTool) goes in by its id.
"add_to_cart",
"place_order",
],
agents: [orders],
});
// What your /api/chat route serves: the top-level agents. A
// sub-agent is found through its parent.
export const agents = [shopper];import { defineWebAgent } from "@webmcp-agents/chat/agent";
import { tools } from "./tools";
// Plain data: no DOM, no secrets. The page and your /api/chat route
// both import this file.
export const orders = defineWebAgent({
id: "orders",
description:
"Answers questions about past orders: what was bought, and " +
"where an order is now.",
instructions: [
"list_orders and track_order are on the orders page: " +
"go_to orders first.",
"Answer with the order's id, its status and when it arrives.",
],
tools: [tools.goTo, tools.listOrders, tools.trackOrder],
// A faster model is enough for this narrow job.
model: "anthropic/claude-sonnet-5-5",
});
export const shopper = defineWebAgent({
id: "shopper",
instructions: [
"You help people shop at Field Store.",
"Read the cart with get_cart before you change it.",
"add_to_cart is on a product's page only.",
"Hand questions about past orders to the orders agent.",
],
tools: [
tools.getTime,
tools.getPage,
tools.goTo,
tools.getCart,
// A tool a component owns (useWebTool) goes in by its id.
"add_to_cart",
"place_order",
],
agents: [orders],
});
// What your /api/chat route serves: the top-level agents. A
// sub-agent is found through its parent.
export const agents = [shopper];import { defineWebAgent } from "@webmcp-agents/chat/agent";
import { tools } from "./tools";
// Plain data: no DOM, no secrets. The page and your /api/chat route
// both import this file.
export const orders = defineWebAgent({
id: "orders",
description:
"Answers questions about past orders: what was bought, and " +
"where an order is now.",
instructions: [
"list_orders and track_order are on the orders page: " +
"go_to orders first.",
"Answer with the order's id, its status and when it arrives.",
],
tools: [tools.goTo, tools.listOrders, tools.trackOrder],
// A faster model is enough for this narrow job.
model: "anthropic/claude-sonnet-5-5",
});
export const shopper = defineWebAgent({
id: "shopper",
instructions: [
"You help people shop at Field Store.",
"Read the cart with get_cart before you change it.",
"add_to_cart is on a product's page only.",
"Hand questions about past orders to the orders agent.",
],
tools: [
tools.getTime,
tools.getPage,
tools.goTo,
tools.getCart,
// A tool a component owns (useWebTool) goes in by its id.
"add_to_cart",
"place_order",
],
agents: [orders],
});
// What your /api/chat route serves: the top-level agents. A
// sub-agent is found through its parent.
export const agents = [shopper];import { defineWebAgent } from "@webmcp-agents/chat/agent";
import { tools } from "./tools";
// Plain data: no DOM, no secrets. The page and your /api/chat route
// both import this file.
export const orders = defineWebAgent({
id: "orders",
description:
"Answers questions about past orders: what was bought, and " +
"where an order is now.",
instructions: [
"list_orders and track_order are on the orders page: " +
"go_to orders first.",
"Answer with the order's id, its status and when it arrives.",
],
tools: [tools.goTo, tools.listOrders, tools.trackOrder],
// A faster model is enough for this narrow job.
model: "anthropic/claude-sonnet-5-5",
});
export const shopper = defineWebAgent({
id: "shopper",
instructions: [
"You help people shop at Field Store.",
"Read the cart with get_cart before you change it.",
"add_to_cart is on a product's page only.",
"Hand questions about past orders to the orders agent.",
],
tools: [
tools.getTime,
tools.getPage,
tools.goTo,
tools.getCart,
// A tool a component owns (useWebTool) goes in by its id.
"add_to_cart",
"place_order",
],
agents: [orders],
});
// What your /api/chat route serves: the top-level agents. A
// sub-agent is found through its parent.
export const agents = [shopper];Import defineWebAgent from @webmcp-agents/chat/agent: that entry has no React and no DOM, so a server file can
import it. Install the package if you have not yet:
npm install @webmcp-agents/chatpnpm add @webmcp-agents/chatyarn add @webmcp-agents/chatbun add @webmcp-agents/chatnub add @webmcp-agents/chataube add @webmcp-agents/chatInstructions
instructions is a string or a list of lines, added to the system prompt under “About this site”. Write what the
model cannot read from the tools: the order of a task (“list_orders before track_order”), which page a tool is on, what to
ask the person, and what never to do. Tool descriptions say what each tool does; instructions say how they fit.
The browser sends only the agent’s id. The server reads the instructions from its own copy of this file, so page text never writes your system prompt.
The tools it may use
Left out, tools is every tool on the page. A list narrows it, and takes both kinds of tool, mixed:
- A catalog tool, by reference:
tools.getCart. The object stands for its id. - A component’s tool, by its id:
"add_to_cart". A tool written withuseWebToolhas no object to import.
The list is an allow-list on both sides: the browser offers the model only these tools, and the server refuses a call
to any other. Listing a catalog tool by reference makes this file import your catalog, which is why catalog files use
defineWebTool from @webmcp-agents/core and import no React.
A function over the page’s tool descriptors works too, for rules a list cannot say:
tools: (tools) => tools.filter((tool) => tool.name !== "place_order"),
Sub-agents
agents gives an agent helpers. Each one becomes a tool named delegate_to_<id> (here delegate_to_orders) that
takes a task. When the parent calls it, the chat runs the helper’s own loop, with its own instructions, tools and
model, and hands its final answer back as the tool’s result.
- Write a
descriptionon a sub-agent. It becomes the delegate tool’s description, which is how the parent knows when to call it. - A sub-agent sees only the task, not the parent’s conversation, so the parent has to hand over a complete one.
- It still works the page. The helper’s tools run in the browser under the cursor, and anything confirm-gated still waits for the person’s click. In the chat, the delegate call shows as a card with the helper’s own calls inside it.
Register only the top-level agents on the server (the agents export above): a sub-agent is found through its parent.
Ids must be unique across the whole tree.
A model per agent
model names the agent’s model as "<provider>/<model>". Without it, the agent uses the server’s default
(WEB_AGENTS_MODEL, else anthropic/claude-opus-5-5). Give a narrow helper a faster model, as orders does.
It is a string, never a key: the server resolves it with the provider’s key from its own environment. A provider other than the default needs its key and its SDK on the server (Add the chat).
Use it
The definition is the same wherever the agent runs. The chat takes it as a prop (<AgentChat agent={shopper} />,
the next page). To run it with no chat on the page, pick the tab that fits:
In a component, with live status:
const agent = useWebAgent(shopper);
<Button onClick={() => agent.invoke("Where is my last order?")}>
{agent.status === "running" ? "Working…" : "Track my last order"}
</Button>Anywhere else, on the agent itself:
const { text } = await shopper.invoke("Where is my last order?");Call an agent has both in full.
Try it
An agent needs a model to run, and the next page adds one: the chat and its /api/chat route. Then ask the chat
“Where is my last order?”. shopper calls delegate_to_orders, and the card for it shows the helper going to
/orders, calling list_orders and then track_order.
Options you’ll use most
| Option | What it is for |
|---|---|
id |
The agent’s name, 1–64 of a-z A-Z 0-9 _ . -. The browser sends it; the server looks it up. |
instructions |
How the tools fit together: a string or lines. |
tools |
The tools it may use: catalog tools and ids, mixed, or a function. Default: every tool on the page. |
agents |
Helpers it hands work to, each as a delegate_to_<id> tool. |
description |
For a sub-agent: what it does, in one sentence. |
model |
"<provider>/<model>". Default: the server’s. |
maxSteps |
Model steps per message for this agent. |
Every option: defineWebAgent and
useWebAgent.