*MCP › Guides*

# Errors

> A tool error is a result with `isError: true` and the problem in the first text block. Branch on its `code`.

## The shape of an error

The server reports a failed call as a result, not as a JSON-RPC error. The result has `isError: true` and the RFC 9457 problem of the REST API as JSON in `content[0].text`. It has no `structuredContent`: the specification requires `structuredContent` to match the tool's `outputSchema`, and an error does not.

*A 429 result*

```json
{
  "isError": true,
  "content": [
    {
      "type": "text",
      "text": "{\"type\":\"https://docs.filmo.io/errors/rate_limited\",\"title\":\"Rate limit exceeded\",\"status\":429,\"code\":\"rate_limited\",\"detail\":\"Rate limit exceeded: retry in 17 seconds\",\"requestId\":\"req_…\",\"retryAfter\":17,\"meta\":{\"rateLimit\":{\"limit\":120,\"remaining\":0,\"reset\":17}}}"
    }
  ]
}
```

The problem carries `type`, `title`, `status`, `code`, `detail`, `requestId` and, when they apply, `errors[]`, `requiredScope`, `requiredAddon` and `retryAfter`. A call with an idempotency key also returns `meta.idempotencyKey` inside the problem. Quote `requestId` when you ask for help. It is the same problem document the REST API returns: see [Errors](https://docs.filmo.io/rest-api/guides/errors).

## Protocol errors

A malformed request, an unknown method or an unknown tool is a JSON-RPC error with a code such as `-32600`, `-32601`, `-32602` or `-32700`, and its message is in English. A call without a credential is not JSON-RPC: it is an HTTP 401 with a problem body. See the [protocol reference](https://docs.filmo.io/mcp/reference/protocol).

## Reading an error with the official SDKs

`Client.callTool` of the TypeScript SDK and `call_tool` of the Python SDK return the result; neither raises for a tool error. Check `isError` and parse the first text block. Only protocol failures, such as an unknown tool or a timeout, raise.

**TypeScript**

```typescript
const result = await client.callTool({ name, arguments: args });
if (result.isError) {
  const text = (result.content as { type: string; text?: string }[]).find((c) => c.type === "text")?.text ?? "{}";
  const problem = JSON.parse(text); // { code, status, detail, requestId, retryAfter?, requiredScope? }
  if (problem.code === "rate_limited") await sleep(problem.retryAfter * 1000);
}
```

**Python**

```python
import asyncio, json

async def call(session, name, args):
    result = await session.call_tool(name, arguments=args)
    if result.isError:
        problem = json.loads(result.content[0].text)  # code, status, detail, requestId, retryAfter?, requiredScope?
        if problem["code"] == "rate_limited":
            await asyncio.sleep(problem["retryAfter"])
    return result
```

In the 2.x packages the field is `is_error` in Python and the transport is built differently; the contract of the server is the same. See [Build your own client](https://docs.filmo.io/mcp/connect/build-your-own).

## Branch on the code

| Code | Status | What to do |
| --- | --- | --- |
| `unauthorized` | 401 | Send a credential. The `api_key_*` codes say why a key is not valid: revoked, expired or its owner is no longer an administrator. |
| `insufficient_scope` | 403 | The key or connection lacks `requiredScope`. See below. |
| `validation_error` | 400 | Fix the fields listed in `errors[]` and call again. |
| `not_found` | 404 | The record does not exist in this account. |
| `conflict` | 409 | Read `errors[0].code` for the cause; read the record again before retrying. |
| `precondition_failed` | 412 | The record changed since you read it. Read it again. |
| `idempotency_key_reused` | 422 | That key was used with different arguments. Use a new key. |
| `rate_limited` | 429 | Wait `retryAfter` seconds. |
| `addon_required` | 402 | The account needs the add-on named in `requiredAddon`. |
| `service_unavailable` | 503 | The feature is switched off for the account; retrying does not help. |

## Insufficient scope

A tool that needs a scope the connection does not have returns [`insufficient_scope`](https://docs.filmo.io/rest-api/errors/insufficient_scope) with `requiredScope`. The server does not answer with a 403 that asks the client to widen its scopes, so no client adds the scope by itself. To add one, remove the connector and add it again, and check the scope in the consent screen. With a key, create another key with the scope.

## Retries

The decision table of the REST API applies. A result tells you it is a repetition with `meta.idempotentReplay: true`.

| Status | Idempotency-Key | Idempotent-Replay: true | What to do |
| --- | --- | --- | --- |
| 409 conflict · in_progress | Yes | No | `errors[0].code` is `in_progress`: another request with the same `Idempotency-Key` is still running. Wait and retry with the same key (exponential backoff, up to 3 times): you get its stored response when it finishes. If the 409 persists for about two minutes, the reservation is treated as abandoned and the next attempt with the same key runs; read the resource before retrying. |
| 409 conflict · lock_timeout | — | No | `errors[0].code` is `lock_timeout`: another write held the resource, or the database aborted the transaction on a transient conflict; nothing was written. Retry with the same key and an exponential backoff, up to 3 times; if it persists, read the resource again. |
| 409 conflict · invalid_state | — | — | `errors[0].code` is `invalid_state`: the state of the resource forbids the operation (a unique violation, a line of an atomic batch, an item deleted while saving). Do not retry unchanged: waiting never fixes it. Read the resource again; if the request still fits, you may send it again. |
| 409 contact_in_use · address_in_use · insufficient_stock | — | — | Do not retry. `errors[]` says what blocks the operation: fix that first. |
| 412 precondition_failed | — | — | Read the resource again, redo your change on top of it and send the new `ETag`. |
| 422 idempotency_key_reused | Yes | — | A bug in the client: the key was used with a different request (a different method, path or body, compared byte by byte). Use a new key only if it really is a different operation. |
| 429 rate_limited | — | — | Wait the seconds in `Retry-After` and retry with the same key. |
| 500 internal_error | Yes | Yes | Stop. The failure happened after the commit and is stored and repeated for 24 h with that key: read the resource to see where it ended up. |
| 500 internal_error | Yes | No | Retry with exponential backoff and the same key. |
| 500 internal_error | No | — | Read before retrying a write: without a key nothing protects you from a duplicate. |
| 503 service_unavailable | — | — | Do not retry: the feature is switched off for the account. |
| 207 (`returnOrderLineItems` with `atomic: false`) | Yes | — | The lines that failed were not returned and the rest were. Resend only the failed ones with a new key; repeating the key would return the same 207. |
| 413 · 502 · 504 (not `application/problem+json`) | Yes | — | An answer from the platform, not from the API: decide by the status and retry with the same key. A 413 is not fixed by retrying (the body is too large). |
| Any validation 4xx | — | — | Fix the request and resend it; never in a loop. |
| 401 · 402 · 403 (key, owner or plan) | — | — | Do not retry: resending the same request fails again. Act on the key, its owner or the plan: use a valid key with the right scope, ask an administrator for the missing permission, or add the add-on. |

Every error code has its own page in the REST API tab, with when it happens and what to do.

---

Canonical URL: https://docs.filmo.io/mcp/guides/errors  
Last updated: 2026-10-25