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

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 render this from the same function, so the two cannot disagree about what “ok” counts. See 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.