# Errors

## Tool errors

Tool errors return `isError: true` with a JSON body in the text content:

```json
{
  "error": {
    "code": "NOT_FOUND",
    "message": "No report found for run abc-123. The run may still be in progress.",
    "details": { "run_id": "abc-123" }
  }
}
```

`details` is present only when the handler supplies it.

| Code | Meaning | Retry? |
|---|---|---|
| `NOT_FOUND` | No matching record | Not without changing the input |
| `VALIDATION_FAILED` | Handler-level validation failed | No — fix the input |
| `RATE_LIMITED` | Bucket empty | Yes, after `details.retry_after_seconds` |
| `UPSTREAM_ERROR` | Downstream or configuration failure | Sometimes — check the message |
| `INTERNAL_ERROR` | Uncaught exception | Yes, with backoff |

Schema-level violations are rejected by the MCP SDK before the handler runs, so they
surface as protocol errors rather than as `VALIDATION_FAILED`.

## Successful responses

```json
{
  "content": [{ "type": "text", "text": "<pretty-printed JSON>" }],
  "structuredContent": { }
}
```

Prefer `structuredContent` — the text block is the same data formatted for display.

## Transport and auth errors

| Status | Code | Meaning |
|---|---|---|
| `401` | `-32001` | Missing or invalid bearer key |
| `503` | `-32002` | Authentication is not configured on the server |
| `500` | `-32603` | Internal server error |

## `NOT_FOUND` is frequently a timing problem

`get_report` returns `NOT_FOUND` for a run that exists but has not finished. The message
says so. Poll [`get_audit_status`](tools/get-audit-status.md) until `status` is
`complete` rather than retrying `get_report` blindly.

## Audit logging

Every call writes one audit row: timestamp, tool, tier, truncated caller-key hash,
redacted arguments, status, duration, and any error. Argument redaction truncates
strings at 200 characters, caps arrays at 20 items, and replaces any key matching
`token|key|secret|password|authorization|credential` with `[redacted]`.
