> ## Documentation Index
> Fetch the complete documentation index at: https://docs.closient.com/llms.txt
> Use this file to discover all available pages before exploring further.

# MCP Error Contract

> How Closient's MCP tools surface errors — JSON-RPC envelopes carrying RFC 9457 Problem Details.

The Closient MCP server exposes the same business operations as the REST API
(GTIN validation, resolution, product lookup, QR-URL generation, more to come)
but speaks JSON-RPC 2.0 instead of HTTP. This page documents how MCP tools
surface failures so agents can react predictably — distinguishing "GTIN not
found" from "rate limited" from "internal error" without parsing free-form
strings.

## Envelope shape

When a tool fails, the result it returns to the client has an `error` field
matching the JSON-RPC 2.0 error-object shape:

```json theme={null}
{
  "error": {
    "code": -32200,
    "message": "Not Found",
    "data": {
      "type": "https://closient.com/docs/errors/not_found",
      "title": "Not Found",
      "status": 404,
      "detail": "No product found for GTIN 00614141123452",
      "error_code": "not_found",
      "retryable": false,
      "timestamp": "2026-04-26T02:34:56+00:00"
    }
  }
}
```

The `data` payload is an [RFC 9457 Problem Details](https://www.rfc-editor.org/rfc/rfc9457)
document — the same structure the REST API returns under
`Content-Type: application/problem+json`. Agents that already understand
Closient's REST error shape do not need a second mapping.

Successful tool calls do **not** include an `error` key — the tool's normal
payload is returned directly.

## Code table

JSON-RPC error codes are stable and grouped by failure class:

| Code     | Class                      | Meaning                                                  | Retryable? |
| -------- | -------------------------- | -------------------------------------------------------- | ---------- |
| `-32001` | Transport / authentication | Missing or invalid token (`unauthorized`).               | After auth |
| `-32002` | Transport / authentication | Token lacks required scope (`forbidden`).                | No         |
| `-32003` | Transport / authentication | Rate limit exceeded (`rate_limited`). See `retry_after`. | Yes        |
| `-32100` | Request validation         | Malformed request (`bad_request`).                       | No         |
| `-32101` | Request validation         | Field-level validation error (`validation_error`).       | No         |
| `-32102` | Request validation         | Semantically invalid (`unprocessable_entity`).           | No         |
| `-32200` | Business rule              | Resource does not exist (`not_found`).                   | No         |
| `-32201` | Business rule              | State conflict, e.g. already claimed (`conflict`).       | No         |
| `-32300` | Upstream / internal        | Unexpected server error (`internal_error`).              | Backoff    |

Adding new codes within these ranges is non-breaking. Renumbering existing
codes is a breaking change.

## Common errors per tool

| Tool              | Likely codes                                                                         |
| ----------------- | ------------------------------------------------------------------------------------ |
| `ping`            | none — health check has no failure modes.                                            |
| `validate_gtin`   | none — invalid GTINs return `valid: false` in payload.                               |
| `resolve_gtin`    | `-32101` (invalid GTIN), `-32200` (no rule).                                         |
| `lookup_product`  | `-32101` (invalid GTIN). Missing GTINs return `found: false` rather than an error.   |
| `generate_qr_url` | `-32101` (invalid GTIN), `-32002` (missing `qr:generate` scope, when auth wires up). |

## Handling errors in an agent

```python theme={null}
result = client.call_tool("resolve_gtin", {"gtin": "00614141123452"})

if "error" in result:
    err = result["error"]
    code = err["code"]
    problem = err["data"]

    if problem.get("retryable"):
        # Optional: respect retry_after on rate-limit responses.
        time.sleep(problem.get("retry_after", 5))
        ...
    elif code == -32200:
        # Resource not found — surface to the user, do not retry.
        ...
    else:
        # 4xx-class user error or 5xx-class server error.
        ...
else:
    # Success path — use the tool's normal payload.
    ...
```

## Reference

* Tracking ticket: [C-2491](https://linear.app/closient/issue/C-2491).
* Implementation: `backend/closient/mcp_errors.py`.
* REST counterpart: see [Errors](/errors).
