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

createWebAgents

The framework-free way to put an app's tools on a page.

The framework-free way to put an app’s tools on a page. It installs the agent cursor, registers every catalog tool that is available and whose match fits the current path, and keeps that true as the visitor navigates, following the browser’s URL itself. Where the browser has no WebMCP it loads the polyfill first. <WebAgentsProvider> is a thin wrapper over it; use it directly from Astro pages, Vue, Svelte or a plain script.

Import

import { createWebAgents } from "@webmcp-agents/core";
import type { WebAgents, WebAgentsOptions } from "@webmcp-agents/core";

Load the cursor’s stylesheet once beside it, from your entry or layout:

import "@webmcp-agents/core/cursor.css";

Example

A two-tool catalog. get_cart is on every page; list_orders only while the URL is /orders. Everything an agent does is shown by the cursor.

import { createWebAgents, defineWebTool } from "@webmcp-agents/core";

const getCart = defineWebTool({
  id: "get_cart",
  description: "Read the cart. Use before checking out.",
  readOnly: true,
  execute: () => ({ items: 2 }),
});

const listOrders = defineWebTool({
  id: "list_orders",
  description: "List past orders. Only on the orders page.",
  readOnly: true,
  match: "/orders",
  execute: () => [{ id: "1002", total: "$128.00" }],
});

const tools = { getCart, listOrders };

// The cursor installs, and each tool registers while its `match`
// fits the URL (get_cart everywhere, list_orders on /orders).
export const agents = createWebAgents({
  tools,
  visuals: { name: "Assistant" },
});

// Only some of them, decided by your own state.
agents.setAvailableTools([tools.getCart]);

// Back to every tool of the catalog.
agents.setAvailableTools(undefined);

Options

createWebAgents(options) takes a WebAgentsOptions.

Name Type Default Description
tools WebToolCatalog required Everything the app has, as an object ({ getCart, listOrders }) or an array. A tool cannot be listed twice: two with one id throw, naming both (tools.getCart and tools.cart). See WebToolCatalog.
availableTools AvailableTools every tool of tools What an agent may use right now, when that depends on your state (signed in, a plan, a feature flag). A tool’s match still applies on top. See AvailableTools.
visuals AgentVisualsOptions | false {} Options for the agent cursor, read when it installs. false installs nothing: tools still register, without animation. See installAgentVisuals.
context () => unknown not said Read on every call and passed to execute as ctx.context. For apps written before tools closed over their own state; new code does not need it.
polyfill "auto" | false "auto" Where the browser has no document.modelContext (Firefox, Safari, Chrome without WebMCP), "auto" loads the polyfill, a chunk of its own that browsers with WebMCP never download; the cursor and the tools wait for it. In production too, so every visitor gets the tools and the chat. false never loads it: tools then register only where the browser has WebMCP, and elsewhere registerWebTool warns once. Read on start().
start boolean true false: do nothing until start() is called (no listeners, no cursor, no registration). For a framework binding that creates this during a render it may throw away.

WebToolCatalog

Readonly<Record<string, WebTool>> | readonly WebTool[]. An object lets code refer to tools.getCart and lists a tool once; an array is the same without keys. A key may be used in availableTools.

AvailableTools

readonly (WebTool | string)[]. Each entry is a tool by reference (tools.getCart), by its key in an object catalog, or by its id.

Returns

A WebAgents handle:

Name Type Description
setAvailableTools(tools) (tools: AvailableTools | undefined) => void Change what an agent may use right now; undefined for every catalog tool. A tool that leaves is released once its call in flight is answered.
setTools(tools) (tools: WebToolCatalog) => void Swap the catalog. Only tools whose definition changed re-register.
destroy() () => void Stop: no more URL tracking, every tool released (each after its call in flight is answered), the cursor removed a moment later. start() before then picks up where it left, without re-registering anything.
start() () => void Start (again). Called for you unless start: false was passed. Does nothing on the server or when already started.
setPathname(pathname) (pathname: string | undefined) => void Match tools against this path instead of the browser’s URL, for a router’s own idea of the path; undefined to follow the URL again.
registry ToolRegistry The registry behind it: enable a catalog tool by hand while a part of the page is on screen (registry.enable([id])).
visuals Pick<AgentVisuals, "setSpotlight" | "setYieldToUser"> Change the cursor’s spotlight and yielding at runtime. Before the cursor installs (the polyfill still loading) a change becomes its starting option; with visuals: false it does nothing. currentAgentVisuals() returns this object to UI that has no handle on it.

Notes

  • A tool is on when it is available and its match fits the path. No match means every page. A tool that stops matching is released only after a call in flight is answered, so turning a tool off never cancels a call still answering (Tool lifecycle).
  • It follows the URL itself. The Navigation API where the browser has it, else history.pushState / replaceState patched for as long as someone watches, plus popstate and hashchange; see watchPathname. Full page loads (Astro, classic sites) simply start again. A hash route (#/orders) counts as the path /orders; a plain #anchor does not.
  • match turns a tool on as soon as the URL changes, which can be before the new route has rendered. A tool must not assume the page’s own state is there yet: do not read what the page sets in a mount effect. Write what a tool needs before you navigate (Skyline stores a search before it navigates to the results).
  • URL-change notices are delivered just after the navigation call returns, once per burst, not inside it: Next.js changes the URL inside a React insertion effect, where telling React (a store listener, a tool registering) is an error.
  • The cursor is installed before the first tool registers, so tools are animated without any ordering rule in your app.
  • Without WebMCP, start() loads the polyfill and the tools wait for it. The cursor installs and the catalog registers once document.modelContext exists, and a tool registered meanwhile (registerWebTool, useWebTool) registers then too, in the order it came; one released before then never registers. The page itself never waits. With native WebMCP nothing is loaded and everything registers at once.
  • Throws on a duplicate tool id. On the server, and until start(), it returns the same object and does nothing: no DOM access at import, safe in SSR.
  • destroy() is deferred a macrotask for the cursor, so a restart in the same tick (React StrictMode) changes nothing.
  • It does not install the devtools, unlike <WebAgentsProvider> in development: @webmcp-agents/devtools is built on this package, so this package cannot load it. A page built on createWebAgents adds one line after it in development, installWebAgentsDevtools(), whose Settings then drive this cursor.

See also

Was this page helpful?