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

createDevtoolsStore

Creates the devtools' state, independent of any UI: the page's tools, every call (run from the panel or by a real agent) and the panel's settings.

Creates the devtools’ state, independent of any UI: the page’s tools, every call (run from the panel or by a real agent) and the panel’s settings. Use it to build another UI on the same data, or to run tools and read their records from code.

Import

import { createDevtoolsStore, DEFAULT_SETTINGS } from "@webmcp-agents/devtools";
import type { DevtoolsState, DevtoolsStore } from "@webmcp-agents/devtools";

Example

A call log for your own panel, and a tool run through the store. subscribe and getSnapshot fit useSyncExternalStore.

import { createDevtoolsStore } from "@webmcp-agents/devtools";
import { useSyncExternalStore } from "react";

// A panel of your own on the devtools' data.
const store = createDevtoolsStore({ maxCalls: 50 });

export function CallLog() {
  const calls = useSyncExternalStore(
    store.subscribe,
    () => store.getSnapshot().calls,
  );

  return (
    <ul>
      {calls.map((call) => (
        <li key={call.id}>
          {call.tool}: {call.status} ({call.origin})
        </li>
      ))}
    </ul>
  );
}

// Run a tool as an agent would, and read its finished record.
export async function runLatestOrder() {
  const record = await store.runTool("track_order", { id: "1002" });
  return record.status;
}

Options

createDevtoolsStore(options?) takes DevtoolsStoreOptions.

Name Type Default Description
visuals DevtoolsVisuals | null null The agent cursor to drive from Settings (setSpotlight, setYieldToUser). With null the switches are disabled.
storageKey string "web-agents-devtools" localStorage key for the settings the person changed. Use another key to keep two stores apart.
maxCalls number 200 Calls (and markers) kept in the timeline. Older ones drop off.
initialSettings Partial<DevtoolsSettings> not said Starting settings, below whatever the person saved. undefined entries are ignored.
now () => number Date.now Clock in epoch ms, for tests.

DEFAULT_SETTINGS is the DevtoolsSettings before anything is passed or saved:

const DEFAULT_SETTINGS = {
  open: false, tab: "tools", dock: "bottom", size: 320, spotlight: false, yieldToUser: true,
};

Returns

A DevtoolsStore:

Member Type Description
getSnapshot () => DevtoolsState The current immutable snapshot. A new object per change.
subscribe (listener: () => void) => () => void Called after every change. Returns an unsubscribe function.
refreshTools () => void Re-read getTools(), the catalog tools, and each tool’s control, fields and match. Coalesced to one microtask. The store also calls it on toolchange, URL changes and (debounced) DOM changes, and only publishes a snapshot when the tool views changed.
runTool (name: string, args?: Record<string, unknown>) => Promise<CallRecord> Run a tool as the agent would (cursor and all). Resolves with the finished record; never rejects.
cancel (id: string) => void Cancel a running or waiting call. Panel runs abort their signal; agent calls need canCancel.
clearCalls () => void Drop finished calls and all markers. Open calls stay.
locate (name: string) => boolean Scroll the tool’s control (the one the cursor acts on, else its toolname element) into view and flash it. false when it isn’t on screen.
setSetting <K extends keyof DevtoolsSettings>(key: K, value: DevtoolsSettings[K]) => void Change and persist one setting. spotlight and yieldToUser also go to visuals.
resetLayout () => void Dock position and size back to defaults.
destroy () => void Stop listening and restore window.agent. Calls in flight still settle. Idempotent.

window.agent

Creating a store also sets window.agent, a console helper that plays the agent; destroy() restores the previous value. Calls made through it run through runTool (cursor and all) and appear on the timeline with origin devtools.

Method Returns Description
invoke(name, args?) Promise<ModelContextToolResult | undefined> Calls the tool the way an agent would. Resolves with the call’s result, or undefined when it failed before producing one (an unknown tool): the error is on the timeline record.
run(name, args?) Promise<ModelContextToolResult | undefined> The same function as invoke, kept under its earlier name.
list() Promise<ModelContextToolInfo[]> document.modelContext.getTools(), or [] without WebMCP.
await agent.invoke("add_to_cart", { quantity: 2 });
await agent.list();

Types

Type Shape
DevtoolsState { tools: ToolView[]; calls: CallRecord[]; markers: TimelineMarker[]; settings: DevtoolsSettings; supported: boolean; canCancel: boolean; hasVisuals: boolean }. calls and markers are newest first, capped at maxCalls. supported: document.modelContext exists. canCancel: cancelTool exists (the polyfill).
DevtoolsSettings { open: boolean; tab: DevtoolsTab; dock: "bottom" | "right"; size: number; spotlight: boolean; yieldToUser: boolean }. size is the height (bottom dock) or width (right dock) in px.
DevtoolsTab "tools" | "timeline" | "settings"
DevtoolsVisuals Pick<AgentVisuals, "setSpotlight" | "setYieldToUser">
ToolView { name; title?; description; schema: SchemaNode; annotations: Record<string, unknown>; kind: "declarative" | "imperative"; onScreen: boolean; confirms: boolean; registered: boolean; control?: ControlView; fields?: FieldView[]; match?: string; fits: boolean; flags: FlagView[] }. onScreen: the tool has a control on the page (its form, the control core resolves, or a toolname element). confirms: a declarative form without toolautosubmit, or a tool with requireConfirmation. registered: registered with document.modelContext right now (false for a catalog tool that is off here). control and fields: only for tools from defineWebTool. match: the tool’s match as written; fits: it fits the current path (true without one). flags: the tool’s flags as badges.
ControlView { by: "target" | "marker" | "name" | "call"; count: number; label: string; warn: boolean }. How the control was found, how many matched, and the words the panel shows (button · Add to cart, 3 matches, none: runs unseen, none: confirmation fails, depends on the call). warn: a confirm-gated tool with no control, or with several found by name.
FieldView { name: string; field: string | null }. A parameter and the field it is typed into (spinbutton · Quantity), null when nothing is typed.
FlagView { flag: "readOnly" | "destructive" | "repeatable" | "external" | "safeResponse"; label: string; title: string; warn?: boolean }. A flag as a badge (read-only, destructive, repeatable, external, untrusted answer) with its tooltip.
CallRecord { id; tool; origin: "devtools" | "agent"; args?; fills: CallFill[]; status: CallStatus; startedAt; endedAt?; result?: ModelContextToolResult; error?; prompt? }. Times are epoch ms. prompt is set while status is waiting.
CallStatus "running" | "waiting" | "ok" | "error" | "cancelled". waiting: the browser paused for the user’s confirmation click.
CallFill { field: string; value: string | boolean; at: number }
TimelineMarker { id: string; type: "yield" | "resume"; reason?: string; at: number }
DevtoolsStoreOptions The options table above.

Notes

  • Creating a store sets window.agent. destroy() restores the previous value.
  • Without document (SSR) the store is inert.
  • Core events carry no call id, so an event belongs to the newest unfinished call of its tool. Two concurrent calls of one tool may mix up.
  • A result counts as an error when isError is set or its text starts with Error: or Error [code]:. A call that navigates the page resolves to the text (navigated).
  • Saved settings win over initialSettings. Saved spotlight and yieldToUser are applied to visuals on the next task; unsaved ones leave the app’s own visuals options alone.
  • If localStorage is blocked, settings still apply but are not persisted.

See also

Was this page helpful?