> ## Documentation Index
> Fetch the complete documentation index at: https://docs.getmcpulse.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Outcomes

> How calls ended: ok, empty, bad_args, tool_error and crashed — and why empty is its own slice.

Every call ends as exactly one of four outcomes, recorded by the SDK. The breakdown splits them into **five** slices, because `ok` hides one.

| Slice        | Meaning                                                     | What it usually means                            |
| ------------ | ----------------------------------------------------------- | ------------------------------------------------ |
| `ok`         | Ran and returned something useful                           | Working                                          |
| `empty`      | Ran, succeeded, returned nothing useful                     | A filter matched nothing, or an id did not exist |
| `bad_args`   | Arguments failed schema validation — your handler never ran | The model does not understand your input schema  |
| `tool_error` | Ran and returned `isError: true`                            | Your tool refused, on purpose                    |
| `crashed`    | Threw                                                       | A bug in your tool                               |

## Why empty is split out

A call that succeeded and returned nothing is the failure the outcome field hides. Burying it inside the green bar is exactly how it stays hidden — so `ok` is drawn as `ok − empties` and `empty` gets its own slice.

Both the overview and the [tool page](/tools/detail) render this from the same function, so the two cannot disagree about what "ok" counts. See [Empty results](/metrics/empty-results).

## crashed versus tool\_error

These look identical from outside `McpServer`, which catches everything a tool does and converts it into `{ isError: true }`. The SDK wraps your tool callbacks as well as the request handler so the difference survives.

It matters because the two have opposite fixes:

* `tool_error` — your tool decided to refuse. Often correct behaviour. Worth checking the model understands *why* it was refused.
* `crashed` — your tool has a bug. Nothing about the model's behaviour will fix it.

## bad\_args is a description problem

`bad_args` means the model constructed arguments your schema rejected, and your handler never ran. That is not a bug in your code; it is a gap between what your schema requires and what your description implies.

A tool with a high `bad_args` share almost always has an under-described required field, an enum whose values are not listed, or a name that suggests something other than what it takes.

## The error rate

The **error rate** on the overview is `bad_args + tool_error + crashed` as a share of calls. `empty` is deliberately **not** in it — an empty result is a different failure with a different rate beside it.

## Counted by name, in both tables

The tool table and the clients panel both carry **`Bad args`**, **`Tool errors`**, **`Crashes`** and **`Empty`** as their own columns, from one shared definition — so a row means the same thing on both.

They were one column called *Failed*, and pooling is what made it useless. 13.4% failed says open the tool and nothing about what to change inside it. 389 bad arguments is a schema the model cannot fill in; 42 tool errors is a handler refusing work it was given; 3 crashes is a bug. One number, three fixes.

**Counts, not rates.** Both tables already carry `calls`, so the denominator is on the row — and a crash is an event rather than a proportion. Three of them reads as something to go and fix, where the same three as 0.1% is the figure your eye skips. A zero from real traffic is muted; a row with no calls dashes all four, because zero failures out of zero calls is not a measurement.

The one pooled figure left is the line above the outcome breakdown — *"13.4% failed · 0.4% empty"* — which is a summary sitting on top of a panel that lists all five by name underneath it.

## Filtering

Outcomes scope cleanly to **both** filters: `tool_hours` carries per-outcome counters per tool, per client, per hour. So does the error rate, and so does everything derived from them.

## Related

* [Empty results](/metrics/empty-results)
* [First-call success](/metrics/first-call-success) — which counts `ok` and non-empty and un-retried
* [Tool detail](/tools/detail) — the same breakdown for one tool
