# MCP Errors and Retries
URL: https://docs.autociso.io/developers/mcp/errors/
Description: Map MCP error codes to SDK exceptions and understand retry and rate-limit behavior.

This page explains how AutoCISO MCP reports failures, how the SDKs turn them into typed exceptions, and how retries and rate limits work. For the REST API error format, see [Errors and limits](https://docs.autociso.io/developers/errors-and-limits/).

## The error envelope

Most failures are **not** transport errors. The server returns **HTTP 200** with a tool result flagged `isError`, and the result body is an envelope keyed on **`error`**:

```json
{ "error": "SCOPE_FORBIDDEN", "scope": "mcp:read" }
```

The key is **`error`** — there is **no `message` field**. Extra fields are code-specific (`scope`, `reason`, `feature`, `detail`, `tool`, `limit`). The SDKs read `error`, synthesize a human-readable message from the code plus the passthrough fields, and raise a mapped exception. Never write code that reads a `message` key from this envelope — it doesn't exist.

## Two error planes

AutoCISO MCP failures arrive on one of two planes:

**1. Tool-result codes (HTTP 200, inside the `{"error": CODE, ...}` envelope):**

`SCOPE_FORBIDDEN`, `NOT_PERMITTED`, `INVALID_ARGUMENTS`, `INTERNAL_ERROR`, `ORG_SUSPENDED`, `UNKNOWN_TOOL`, `RATE_LIMITED`, `RESULT_TOO_LARGE`.

**2. HTTP statuses the MCP app itself emits:**

`401`, `404`, and `429` — and only those. The MCP app never emits `403` or any `5xx`. If you ever see a `403` or a `5xx`, it came from the network infrastructure in front of AutoCISO, not the MCP application.

## Code → exception → cause → fix

### Tool-result codes (HTTP 200)

| Code | SDK exception | Cause | Fix |
|------|---------------|-------|-----|
| `SCOPE_FORBIDDEN` | `AutocisoScopeError` | Token lacks the required scope | Recreate the token with the scope the tool needs. See [MCP Auth and Scopes](https://docs.autociso.io/developers/mcp/authentication/) |
| `NOT_PERMITTED` | `AutocisoPermissionError`; for `reason: "FEATURE_NOT_ENTITLED"` a `ToolError` exposing `.reason` and `.feature` | Write tool called but write disabled for org; **or** `reason: "FEATURE_NOT_ENTITLED"` with a `feature` ID — the tool's feature is no longer in your organisation's [plan](https://docs.autociso.io/getting-started/plans-and-features/) (a registration that outlived a downgrade) | Expected on read-only orgs — not a bug. For `FEATURE_NOT_ENTITLED`, refresh `tools/list`; not retried — upgrade the plan in user menu → **Billing & Subscription** or drop the tool |
| `INVALID_ARGUMENTS` | `AutocisoValidationError` | Bad or missing tool argument | Check the argument against the [Tool Catalog](https://docs.autociso.io/developers/mcp/tools/) |
| `INTERNAL_ERROR` | `AutocisoServerError` | Server-side failure; also returned when a typed tool unexpectedly produces no result at all | Retry; if persistent, contact support |
| `ORG_SUSPENDED` | `AutocisoUnavailableError` | Org is suspended | Resolve the account status |
| `UNKNOWN_TOOL` | `AutocisoUnknownToolError` | Tool name not recognized — including `employee_resolve` called by a token without `pii:read`, and any tool whose feature is outside your organisation's plan; both are unregistered for that token rather than merely hidden | Check the spelling against the catalog; for `employee_resolve`, have an [Owner](https://docs.autociso.io/getting-started/roles-and-permissions/) create a token with `pii:read` (see [Employee identity](https://docs.autociso.io/developers/employee-identity/)); for a plan-gated tool, compare the **Requires plan** column with your plan |
| `RATE_LIMITED` | `ToolError` with `code === "RATE_LIMITED"` and `retryAfterSeconds` (`retry_after_seconds` in Python) | The token's per-call window for `employee_resolve` or `iso_audit_simulate` is exhausted | Wait `retryAfterSeconds`, then retry yourself — the SDK does **not** retry this code (see below) |
| `RESULT_TOO_LARGE` | `ToolError` with `code === "RESULT_TOO_LARGE"` and `limit` (bytes) | The tool's result exceeded the 256 KiB (`262144`-byte) cap and was replaced by this error | Not retried — lower `limit`, add filters or page with `offset`; the same call will fail the same way |

### HTTP statuses

| Status | SDK exception | Cause | Fix |
|--------|---------------|-------|-----|
| `401` | `AutocisoAuthError` | Token missing, malformed, or revoked | Re-copy or recreate the token. See [API tokens](https://docs.autociso.io/developers/api-tokens/) |
| `404` | `ServerUnavailableError` | MCP is not enabled for your deployment | Contact support. See [Troubleshooting](https://docs.autociso.io/developers/mcp/troubleshooting/) |
| `429` | `AutocisoRateLimitError` | Rate limit exceeded | Back off and retry (the SDK does this for you) |

## Retries

- **On by default for reads.** Idempotent read tools retry automatically on `429` and transient errors, using exponential backoff with jitter, capped at ~3 attempts.
- **Writes are never auto-retried.** None of the five write tools (`risk_create_draft`, `iso_audit_simulate`, `supplier_questionnaire_clone`, `supplier_questionnaire_update_question`, `supplier_questionnaire_update_option`) is retried: the server has no idempotency key, so a blind retry could create a duplicate, and `iso_audit_simulate` spends an AI request on every call. Handle write failures yourself.
- **`FEATURE_NOT_ENTITLED` and `RESULT_TOO_LARGE` are never retried.** Neither changes on its own: the first needs a plan change (then a fresh `tools/list`), the second needs a smaller request.
- **Tool-plane `RATE_LIMITED` is not automatically retried.** It arrives inside the envelope at HTTP 200 with a `retryAfterSeconds` hint, so the HTTP `429` path never fires and the read-retry loop would burn its attempts in seconds against a window that can be up to an hour long. Both SDKs surface it as a non-retryable `ToolError` carrying `retryAfterSeconds`; the caller decides when to try again. In TypeScript it is a `ToolError`, **not** the HTTP-plane `RateLimitError` class.

### `employee_resolve` and `retryAfterSeconds`

`employee_resolve` and `iso_audit_simulate` are the only tools that return `RATE_LIMITED`. The `employee_resolve` window is currently 30 calls per hour per token, counted per server instance, so it is approximate — treat the number as client guidance, never as a property to design against. Batch up to 50 IDs per call and store the mapping on your side; a well-behaved integration rarely meets the limit. Rate-limited calls disclose nothing and are not written to the [audit trail](https://docs.autociso.io/console/settings/audit-trail/).

```json
{ "error": "RATE_LIMITED", "retryAfterSeconds": 1740 }
```

### `FEATURE_NOT_ENTITLED` and `RESULT_TOO_LARGE`

```json
{ "error": "NOT_PERMITTED", "reason": "FEATURE_NOT_ENTITLED", "feature": "privacy_operations" }
```

```json
{ "error": "RESULT_TOO_LARGE", "limit": 262144 }
```

Both arrive as a `ToolError` (`.reason`/`.feature` on the first, `.limit` on the second; snake_case in Python) with `retryable === false`. Before calling a tool, the SDKs' generated descriptors (`TOOL_META[name].requiresFeature` in TypeScript, `TOOL_META[name].requires_feature` or `REQUIRES_FEATURE[name]` in Python) tell you which feature it needs — compare with the **Requires plan** column of the [Tool Catalog](https://docs.autociso.io/developers/mcp/tools/#plan-gating).

## Rate limits

**Warning — Rate limits**

Requests are rate-limited per token and per org; repeated failed authentications are also limited per IP, so a client with a valid token is never throttled by that check. A `429` carries a `Retry-After` header (in seconds). The SDKs back off automatically; if you call the endpoint directly, honour `Retry-After` and back off exponentially on repeats.

## Handling errors in code

**TypeScript**

```typescript
  AutocisoScopeError,
  AutocisoPermissionError,
  AutocisoRateLimitError,
} from '@autociso/mcp-client';

try {
  const risks = await client.risk.registerList();
} catch (err) {
  if (err instanceof AutocisoScopeError) {
    console.error('Token is missing a required scope');
  } else if (err instanceof AutocisoPermissionError) {
    console.error('Write not permitted for this org');
  } else if (err instanceof AutocisoRateLimitError) {
    console.error('Rate limited — the SDK already backed off and gave up');
  } else {
    throw err;
  }
}
```

**Python**

```python
from autociso_mcp import (
    AutocisoScopeError,
    AutocisoPermissionError,
    AutocisoRateLimitError,
)

try:
    risks = client.risk.register_list()
except AutocisoScopeError:
    print("Token is missing a required scope")
except AutocisoPermissionError:
    print("Write not permitted for this org")
except AutocisoRateLimitError:
    print("Rate limited — the SDK already backed off and gave up")
```
