---
title: Devtools
description: See which tools are on for the current page and what the cursor will do for each, run them from a form made from their schema, and watch a timeline of every call, yours or a real agent's.
sidebar:
  label: 10. Devtools
---

`@webmcp-agents/devtools` is a panel for your page in development. It lists every tool that is on and what the cursor
will do for it, lets you play the agent with a form made from each tool's schema, and records every call, yours or a
real agent's. Open it with the button in the bottom-right corner, or **Alt+Shift+A**.

## A complete example

There is nothing to add: the highlighted provider installs the panel whenever the app runs in development (`vite dev`,
`next dev`, `astro dev`), wired to its cursor. It comes with `@webmcp-agents/react`, and a production build has no
devtools code at all: the provider's `import()` of it sits behind `process.env.NODE_ENV === "development"`, which your
bundler replaces.

**Vite**

```tsx title="src/main.tsx" {15}
import { AgentChat } from "@webmcp-agents/chat";
import { WebAgentsProvider } from "@webmcp-agents/react";
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";

import "./index.css";
import "@webmcp-agents/core/cursor.css";
import "@webmcp-agents/chat/chat.css";
import { shopper } from "@/agents";
import { tools } from "@/tools";
import { App } from "./App";

createRoot(document.getElementById("root")!).render(
  <StrictMode>
    <WebAgentsProvider tools={tools}>
      <App />
      <AgentChat
        agent={shopper}
        // In development the provider's devtools launcher has
        // the other corner.
        position={import.meta.env.DEV ? "bottom-left" : "bottom-right"}
      />
    </WebAgentsProvider>
  </StrictMode>,
);
```

**Next.js**

```tsx title="app/providers.tsx" {15}
"use client";

import { AgentChat } from "@webmcp-agents/chat";
import { WebAgentsProvider } from "@webmcp-agents/react";
import type { ReactNode } from "react";

import "@webmcp-agents/chat/chat.css";
import { shopper } from "@/agents";
import { tools } from "@/tools";

const DEV = process.env.NODE_ENV === "development";

export function Providers({ children }: { children: ReactNode }) {
  return (
    <WebAgentsProvider tools={tools}>
      {children}
      <AgentChat
        agent={shopper}
        // In development the provider's devtools launcher has
        // the other corner.
        position={DEV ? "bottom-left" : "bottom-right"}
      />
    </WebAgentsProvider>
  );
}
```

**TanStack Start**

```tsx title="src/components/providers.tsx" {13}
import { AgentChat } from "@webmcp-agents/chat";
import { WebAgentsProvider } from "@webmcp-agents/react";
import type { ReactNode } from "react";

import "@webmcp-agents/chat/chat.css";
import { shopper } from "@/agents";
import { tools } from "@/tools";

const DEV = import.meta.env.DEV;

export function Providers({ children }: { children: ReactNode }) {
  return (
    <WebAgentsProvider tools={tools}>
      {children}
      <AgentChat
        agent={shopper}
        // In development the provider's devtools launcher has
        // the other corner.
        position={DEV ? "bottom-left" : "bottom-right"}
      />
    </WebAgentsProvider>
  );
}
```

**Astro**

```tsx title="src/components/providers.tsx" {13}
import { AgentChat } from "@webmcp-agents/chat";
import { WebAgentsProvider } from "@webmcp-agents/react";
import type { ReactNode } from "react";

import "@webmcp-agents/chat/chat.css";
import { shopper } from "@/agents";
import { tools } from "@/tools";

const DEV = import.meta.env.DEV;

export function Providers({ children }: { children: ReactNode }) {
  return (
    <WebAgentsProvider tools={tools}>
      {children}
      <AgentChat
        agent={shopper}
        // In development the provider's devtools launcher has
        // the other corner.
        position={DEV ? "bottom-left" : "bottom-right"}
      />
    </WebAgentsProvider>
  );
}
```

The panel lives in a shadow root, so no CSS leaks either way, and its host carries `data-web-agents-ui`, so using it
never makes the cursor get out of the way. Tests get no panel: under `NODE_ENV=test` (Vitest, Jest) the provider
leaves it out, so it never shows up among your queries.

## What the Tools tab shows

Every tool that is registered, refreshed as tools, the URL and the page change. Under each tool's name, what the cursor
will do for it, so nobody has to guess:

| Line | Shows | For example |
|---|---|---|
| **Control** | The control the cursor goes to, as a screen reader names it. | `button · Add to cart`, `3 matches`, `none: runs unseen`, `none: confirmation fails`, `depends on the call` |
| **Fields** | Each parameter, and the field it will be typed into. | `quantity → spinbutton · Quantity`, `cabin → not typed` |
| **Match** | Where a catalog tool is on, and whether the current path fits. | `/orders, /orders/:id` |
| **Flags** | A badge per flag, and one for a tool that needs confirmation. | read-only, destructive, external |

A warning marks a confirm-gated tool that would fail: nothing to click, or several controls with its name
([Confirm before it runs](/docs/build/confirm-before-it-runs#it-needs-exactly-one-control)). A catalog tool whose `match`
does not fit is listed **off here**, with its Run disabled. **Locate** scrolls to a tool's control and flashes it.

![The Tools tab: every tool on the page with a form generated from its schema and a Run button](/blume-assets/content/_/9886d3d64af77cf0.png)

## Runner, Timeline, Settings

- **Runner.** A form made from the tool's JSON Schema (strings, enums, dates, numbers, booleans, nested objects and
  arrays), with a raw JSON fallback. **Run** with `⌘/Ctrl+Enter`, and copy the call as `agent.invoke(…)` code.
- **Timeline.** Newest first: the arguments, the fields the cursor typed, "waiting for you" while a tool is parked, the
  result or error with its code, a duration bar, a marker where you took over from the cursor, and **Re-run**.
- **Settings.** The dock's position and size, and the cursor's spotlight and yield-to-user switches. Saved in
  `localStorage`.

![The Timeline tab: each call with its arguments, result and timing](/blume-assets/content/_/5607c08afa1f6333.png)

The cursor switches drive the provider's cursor. A page without the provider (built on `createWebAgents`, as in
[No framework](/docs/installation/no-framework)) installs the panel itself with `installWebAgentsDevtools()`, after
`createWebAgents`; the switches then drive that cursor.

## Place it yourself

To choose where and when the panel shows (in production too, or as a tab in TanStack Devtools), turn the provider's
off and render the component inside it:

```tsx
import { WebAgentsDevtools } from "@webmcp-agents/devtools/react";

<WebAgentsProvider tools={tools} devtools={false}>
  {children}
  <WebAgentsDevtools />
</WebAgentsProvider>
```

Install `@webmcp-agents/devtools` yourself for this, and keep it behind your own development check with a lazy
`import()` unless you want it in production. With TanStack Devtools, `webAgentsDevtoolsPlugin()` from the same entry
adds the panel as a "Web Agents" tab. Either way its Settings drive the provider's cursor.

## From the console

The devtools put `agent` on `window`:

```js
await agent.list();
await agent.invoke("add_to_cart", { size: "L", quantity: 3 });
```

`agent.invoke(name, args)` calls a tool by name, the way an agent would, and resolves to its answer. Calls made this way
show on the Timeline too. End-to-end tests drive tools the same way: the example apps' Playwright tests do.

## Limits

- Two calls of the same tool at once can mix their fills on the Timeline: agent events carry no call id.
- For a real agent's call, the Timeline shows the fields the cursor typed, not the agent's arguments.
- **Cancel** stops a run started from the panel everywhere, but another agent's call only with the polyfill: native
  Chrome lets only the caller cancel.
- The hotkey is real keyboard input, so pressing it makes the cursor get out of the way for a moment. In a scripted
  check, call tools about a second after it.

## Try it

Open `/orders`, press **Alt+Shift+A**, and find `list_orders`: **Match** shows `/orders` and that it fits. Expand it,
set `limit` to 1 and press **Run**: the answer appears under the form and on the Timeline. Go back to `/`: the tool is
listed **off here**.

## Options you'll use most

The provider's `devtools` prop: `false` turns the panel off, an object configures it (still in development only).

```tsx
<WebAgentsProvider tools={tools} devtools={{ initialOpen: true }}>
```

| `devtools` option | What it is for |
|---|---|
| `initialOpen` | Open the panel on load. Default `false`. |
| `hotkey` | The shortcut that toggles it. Default `"Alt+Shift+A"`; `false` for none. |
| `initialSettings` | Settings to start from, below what the person saved: `{ dock: "right" }`, `{ size: 420 }`. |
| `eventClient` | Post to TanStack Devtools' event bus. Default `true`. |

Every option: [`<WebAgentsProvider devtools>`](/docs/reference/react/web-agents-provider),
[`installWebAgentsDevtools`](/docs/reference/devtools/install-web-agents-devtools) and
[`<WebAgentsDevtools>`](/docs/reference/devtools/web-agents-devtools).

## Next

That is the whole path. From here, learn why it works and what else the packages do:

**[Concepts](/docs/concepts/how-it-works)**

How it works, the agent cursor, the tool lifecycle, errors, and how to write a good tool description.

**[More](/docs/more/on-device-agent)**

An on-device model, shadcn chat blocks, declarative forms, the polyfill and deploying.
