---
description: "Creates a counter for a tool's calls, so its unregistration can wait for them."
---

# `createCallTracker`

Creates a counter for a tool's calls, so its unregistration can wait for them. Pair it with [`deferRelease`](/docs/reference/core/defer-release) and pass it as `calls` to [`registerWebTool`](/docs/reference/core/register-web-tool); the registry and `useWebTool` already do, so you reach for it only when you register tools yourself.

## Import

```ts
import { createCallTracker } from "@webmcp-agents/core";
import type { CallTracker } from "@webmcp-agents/core";
```

## Example

Count the calls of one registration, and ask whether it is safe to unregister it. [`deferRelease`](/docs/reference/core/defer-release) does the waiting for you.

```ts title="track-calls.ts"
import {
  createCallTracker,
  defineWebTool,
  registerWebTool,
} from "@webmcp-agents/core";

const tool = defineWebTool({
  id: "get_time",
  description: "Read the time.",
  readOnly: true,
  execute: () => new Date().toISOString(),
});

// registerWebTool counts each call of the tool on this tracker.
const calls = createCallTracker();
export const unregister = registerWebTool(tool, { calls });

// You can count your own work on it too.
export async function refresh(load: () => Promise<void>) {
  await calls.track(load);
}

// While a call runs, and for 500 ms after the last one settled.
export const canUnregister = () => !calls.busy();
```

## Options

None: `createCallTracker()` takes no arguments.

## Returns

A `CallTracker`:

| Name | Type | Description |
|---|---|---|
| `track(call)` | `<T>(call: () => T \| Promise<T>) => Promise<T>` | Run one call and count it until it settles. Resolves or rejects with the call's own outcome. |
| `busy()` | `() => boolean` | `true` while a call runs, and for [`RESPONSE_GRACE_MS`](/docs/reference/core/defer-release) (500 ms) after the last one settled. |

## Notes

- The grace after the last call gives the browser time to deliver the answer before the tool changes.
- `busy` is a plain function and can be passed as `isBusy` without binding.
- A rejected call still counts as settled: the counter drops in a `finally`.
- Each tool needs its own tracker: it counts calls, not which tool they were for.

## See also

- [Tools across your app](/docs/build/tools-across-your-app), where tools turn on and off
- [`deferRelease`](/docs/reference/core/defer-release)
- [`registerWebTool`](/docs/reference/core/register-web-tool)
- [`createToolRegistry`](/docs/reference/core/create-tool-registry)
