---
title: Errors
description: The one shape of a failed tool call — the code in structuredContent and the sentence in the text — the codes that each tool can send, and what to do next.
---

A tool call that fails does not fail over HTTP. The tool runs, refuses, and returns a normal tool result with `isError` set. That result has one shape for each tool. [Errors](/api/errors) in the API reference describes the REST API, which uses the same codes.

## The shape

```json
{
  "content": [
    {
      "type": "text",
      "text": "Brand \"acme-running\" was changed since you read it (current revision 7). Re-fetch with brand_get and retry with the latest base_revision."
    }
  ],
  "isError": true,
  "structuredContent": {
    "error": "revision_mismatch",
    "message": "Brand \"acme-running\" was changed since you read it (current revision 7). Re-fetch with brand_get and retry with the latest base_revision.",
    "currentRevision": 7
  }
}
```

- **`isError`** is `true`. The tool ran and refused.
- **The text block** holds one sentence for a person. The agent reads it, as it reads a successful result.
- **`structuredContent`** holds the same failure as data, in the shape of each AdCrunch API:

| Field | What it is | Branch on it? |
| --- | --- | --- |
| `error` | A stable code, lower snake_case. | **Yes. Only this.** |
| `message` | The sentence of the text block. | Never. |
| Other fields | Facts that make the failure actionable, such as `currentRevision` above. | Yes, for the code that sends them. |

`message` carries no contract. AdCrunch rewrites a sentence when it can say it better. That is why the code is a separate field. A client that reads only the text still gets the whole failure.

:::tip[Always write a default branch]

A code that you have never seen is what a new failure looks like. AdCrunch can add a code to a tool at any time. A code that changes its name or goes away breaks callers, so that does not happen without notice.

:::

## The codes that each tool can send

Each tool can send these three codes:

| `error` | Other fields | What it means | Do this |
| --- | --- | --- | --- |
| `forbidden` | — | The token of your client does not hold the scope of this tool. | Authorize your client again, and grant the scope. [Auth & scopes](/mcp/auth#scopes) lists each scope. |
| `invalid_request` | `issues` | The arguments do not match the input schema of the tool, or they break a rule over several arguments. `issues` names each argument. Nothing changed. | Correct the arguments. The same call fails again. |
| `internal_error` | — | AdCrunch could not answer, or its answer did not match the output schema of the tool. The sentence never holds the details of the fault. | Try again later. |

Each tool also sends codes of its own, such as `not_found` or `revision_mismatch`. The page of each tool lists all the codes that it can send, under its reference part. The tool list gives the same codes in the `_meta` of each tool, under `dev.adcrunch/errors`.

One failure has one code on MCP and on the REST API. When a REST operation sends the same failure, the tool sends the same code, with the same other fields.

### An argument that does not match

`issues` has one entry for each argument that does not match the schema:

```json
"issues": [
  {
    "path": "/base_revision",
    "message": "Invalid input: expected number, received undefined"
  }
]
```

- **`path`** is a JSON Pointer into the arguments. An empty string is the whole set of arguments, for a rule over several arguments such as "at most one of `dailyBudget` and `lifetimeBudget`".
- **`message`** is for a person. Never branch on it.

A tool call has one part, its arguments. So an entry has no `in` field, which the REST API uses to name a part of the HTTP request.

## An edit that collides

`revision_mismatch` is the one failure that the agent handles, and does not only report. It means that the object changed after the agent read it, and `currentRevision` gives the revision of now. Nothing changed.

The agent reads the object again, checks that its edit still applies to the new version, and sends the edit again with the new revision. It does not read the object and start again from zero, because that erases what the author wrote. [What to expect](/mcp/what-to-expect#an-edit-can-collide-with-another-edit) gives the full rule.

## A missing thing and a thing that is not yours

Each tool reads your organization only. So an id that does not exist and an id of another organization get the same answer: `not_found`, or an empty list.

This is deliberate. An answer that told the two apart would let a caller find which ids exist.

Two results follow:

- **`not_found` does not prove that the thing was deleted.** It can exist in an organization that you are not in.
- **An empty result from [`query_insights`](/mcp/tools/query-insights) has two meanings.** Either your organization does not own that advertiser, or the advertiser has no data for that period. The result cannot tell you which.

The agent usually recovers by itself. It reads the list again, and it retries with a real id.

## Two failures that are not tool results

Two failures stop each tool at once, before a tool runs. They are HTTP answers of the endpoint, and your client shows them as a connection problem:

- **`401`**: the token is missing, has expired, or does not verify.
- **`403`** with `insufficient_scope`: the token holds no AdCrunch scope at all.

Both answers carry a `WWW-Authenticate` header that tells your client how to authorize again. For both, authorize AdCrunch again from the settings of your client. [Another client](/mcp/setup/other-clients#when-the-endpoint-refuses-the-token) gives the details for a developer.

## A change that fails is not a failed tool call

A change on Meta runs after the tool call, in a queue. [`get_mutation_status`](/mcp/tools/get-mutation-status) reports how it ended. When AdCrunch or Meta refused the change, the status is `complete`, and `result.error` holds the code, for example `budget_cap_exceeded`.

That call is a success: it read the status, and the status says that the change failed. So `isError` is not set. The agent reads the code in the result, and tells you why the change did not happen.

## What's next

- [Auth & scopes](/mcp/auth): what a token can touch.
- [What to expect](/mcp/what-to-expect): the rules that each tool follows.
- [Errors](/api/errors) in the API reference: the same codes on the REST API.
