---
description: "Decides which catalog tools are registered with WebMCP right now."
---

# `createToolRegistry`

Decides which catalog tools are registered with WebMCP right now. Every part of the app that wants a tool on (a route, a mounted component, the app shell) enables it and gets a disposer; a tool stays registered while anyone holds it. [`createWebAgents`](/docs/reference/core/create-web-agents) (and so `<WebAgentsProvider>`) builds on it and exposes it as `registry`: most apps never create one, they give tools a `match` instead.

## Import

```ts
import { createToolRegistry } from "@webmcp-agents/core";
import type { ToolRegistry, ToolRegistryOptions } from "@webmcp-agents/core";
```

## Example

A registry over two tools: one held by the app shell, one by a checkout screen that releases it when it leaves. The cursor is installed in `beforeRegister`, so every tool is animated.

```ts title="tool-registry.ts"
import {
  createToolRegistry,
  defineWebTool,
  installAgentVisuals,
} from "@webmcp-agents/core";

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

const placeOrder = defineWebTool({
  id: "place_order",
  description: "Place the order. Only on the checkout page.",
  requireConfirmation: true,
  execute: () => "Order placed.",
});

let installed = false;

export const registry = createToolRegistry([getCart, placeOrder], {
  // The cursor must be installed before a tool registers.
  beforeRegister: () => {
    if (installed) return;
    installed = true;
    installAgentVisuals();
  },
});

// Held by the app shell for as long as it is mounted.
export const releaseCart = registry.enable(["get_cart"]);

// Held by the checkout screen: call the disposer when it leaves.
export const releaseCheckout = registry.enable(["place_order"]);

registry.subscribe(() => console.debug(registry.enabled()));
```

## Options

`createToolRegistry(catalog, options?)`. The first argument is every tool the app may register; the second is a `ToolRegistryOptions`.

| Name | Type | Default | Description |
|---|---|---|---|
| `catalog` | `readonly WebTool[]` | required | Every tool the app may register, made with [`defineWebTool`](/docs/reference/core/define-web-tool). Ids must be unique: a duplicate throws. Nothing is registered until something calls `enable`. |
| `beforeRegister` | `() => void` | not said | Called before each WebMCP registration. Install the cursor here ([`installAgentVisuals`](/docs/reference/core/install-agent-visuals)), so it can hold and animate a tool from its first call. |
| `context` | `() => unknown` | not said | Read on every call and passed to `execute` as `ctx.context`. Only for apps written before tools closed over their own state; new code does not need it. |

## Returns

A `ToolRegistry`:

| Name | Type | Description |
|---|---|---|
| `enable(names)` | `(names: readonly string[]) => () => void` | Enable tools by id (ref-counted). Returns a disposer that releases them once every holder has, after in-flight calls settle. The disposer is idempotent. |
| `update(catalog)` | `(catalog: readonly WebTool[]) => void` | Swap in a new catalog. Only tools whose definition changed re-register; tools that left are released. |
| `enabled()` | `() => readonly string[]` | Ids registered with WebMCP right now, sorted. The same array until it changes. |
| `subscribe(listener)` | `(listener: () => void) => () => void` | Called whenever `enabled()` changes. Returns an unsubscribe function. |
| `destroy()` | `() => void` | Unregister everything now and stop. Later `enable` and `update` calls do nothing. |

## Notes

- Releases are deferred a macrotask, so a release plus a re-enable in the same tick (React StrictMode, a route swap that keeps a tool) never touches WebMCP.
- A released tool with a call in flight stays registered until the call settles plus [`RESPONSE_GRACE_MS`](/docs/reference/core/defer-release), because Chrome cancels a call whose tool is unregistered.
- `enable` with an id that is not in the catalog warns in development and keeps holding it: a later `update` that adds the id registers it.
- A duplicate id in a catalog throws `Duplicate WebTool id "<id>" in catalog`.
- Calls go to whatever definition the catalog holds now, so a new `execute` from `update` needs no re-registration. A change to what WebMCP sees (see [`webToolSignature`](/docs/reference/core/web-tool-signature)) swaps the registration at once, since Chrome would cancel its calls anyway.
- A tool leaves `enabled()` when it is actually unregistered, which is after the deferred release, not when its last holder lets go.

## See also

- [Tools across your app](/docs/build/tools-across-your-app), the page that teaches `match` and availability
- [`createWebAgents`](/docs/reference/core/create-web-agents), which owns one for you
- [`registerWebTool`](/docs/reference/core/register-web-tool)
- [`deferRelease`](/docs/reference/core/defer-release) and [`createCallTracker`](/docs/reference/core/create-call-tracker)
- [`matchPath`](/docs/reference/core/match-path)
- [`webToolSignature`](/docs/reference/core/web-tool-signature)
