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

# Errors, rate limits, retries

> The two shapes a failed tool call takes, the nine error kinds and what each one means for an agent, and the one retry the process makes by itself.

A tool call fails in one of two shapes, and an AI agent has to tell them apart before it parses
anything. A call the Vexlo server answered with an error returns a JSON object: a `kind` to
branch on, plus the server's own `code` and `message` where there is one. A call whose arguments
the MCP server's own schema rejected returns one line of plain text instead. Both arrive as a
tool result with `isError` set, and only the first is JSON, so branch on whether the text starts
with `{`: anything else is the MCP layer reporting the call, not the Vexlo server reporting an
error. This page covers the JSON shape first and the text shape under
[rejected arguments](#rejected-arguments).

```json theme={null}
{
  "kind": "not_found",
  "code": "trace_not_found",
  "message": "no trace with id deadbeefdeadbeefdeadbeefdeadbeef",
  "details": {
    "trace_id": "deadbeefdeadbeefdeadbeefdeadbeef"
  }
}
```

## Shape decides error from data, not the status code

Whether a response is an error is decided by whether its body parses as the API's error
envelope, `{"error":{"code","message"}}`, never by the HTTP status alone. 400, 401, 403, 404,
422, 429, and 500 all use the envelope.

It matters for one route. A server under disk pressure answers `GET /v1/health` with
`{"status":"degraded","mode":"shape_only",...}` and HTTP 503. That body is not the error
envelope, so `vexlo_check_health` returns it as its answer: a degraded server is a fact about the
server, not a failed call.

A body that is not valid JSON, or that matches neither the envelope nor the shape the tool
expects, is `malformed_response`, with the HTTP status. A request that throws before any
response exists, a timeout at the 30-second tool deadline included, is `network_error`.

## The nine kinds

| `kind` | Fields besides `kind` | Raised by | The agent's next move |
| - | - | - | - |
| `authentication_failed` | `message` | `401 unauthenticated`: the token was revoked after startup. Tokens carry no expiry | stop; nothing a call can change. Fix `VEXLO_TOKEN` and restart the process |
| `wrong_role` | `message` | `403 forbidden_role`. Unreachable: startup refuses any non-`read` token, and `vexlo token` can create, list, and revoke a token but never change its role | stop |
| `not_found` | `code`, `message`, `details` | any `*_not_found` code, or any other 404 | try another id, or accept that it is not there |
| `invalid_arguments` | `code`, `message`, `details` | `invalid_request`, `invalid_cursor`, or any other 4xx the server raises | fix the argument the `message` names |
| `too_many_values_requested` | `message` | `422 too_many_paths`. Unreachable: the argument schema caps `path` at `values_max_paths` first | ask for fewer paths |
| `rate_limited` | `retryAfterSeconds`, and `limit`, `remaining`, `resetSeconds` when the response carried them | `429 rate_limited` | wait `retryAfterSeconds`, then retry |
| `server_error` | `code`, `httpStatus`, `message` | `internal_error`, or any status of 500 and above | report it; different arguments will not help |
| `malformed_response` | `httpStatus`, `message` | a body that is neither the envelope nor the expected shape | report it; not argument-fixable |
| `network_error` | `message` | the request threw: refused connection, DNS failure, timeout | safe to retry; every call here is a `GET` |

A code the MCP server does not recognise still reaches the agent: `code` and `message` are
preserved and `kind` falls back on the status, so a newer server is less specific here rather
than unusable.

Every `message` above is the Vexlo server's own, with one exception. On `malformed_response` the
MCP server writes the sentence itself, one of `response body is not valid JSON (status N)`,
`response body matched neither the error envelope nor the expected shape (status N)`, or
`could not read the response body (status N): <reason>`. A `message` describing the response
body is the client speaking, so do not report it as something the server said.

A 404 says nothing about the token's team-set. A trace that does not exist and a trace on a team
this token cannot see produce the same answer, by design, so "not found" is never evidence about
the team-set. `vexlo_check_coverage` and the `scope_note` on [find](https://docs.vexlo.ai/mcp/tools/find.md)'s tools are
what answer a question about it.

Several API codes cannot be reached from these eleven tools. `malformed_batch`,
`service_scope_mismatch`, `environment_scope_mismatch`, `invalid_policy`, and `body_too_large`
belong to the ingest routes, which no tool calls. `service_required` is unreachable because both
function tools require `service`. `conflicting_filters` is unreachable because naming an
environment and `unset` at once is not representable in the argument schema. `too_many_paths`
and `forbidden_role` are the two the table marks unreachable.

## Rejected arguments

An argument the schema rejects never reaches the handler, and the answer is not the JSON object
above. It is an ordinary tool result whose one text block is a sentence:

```
$ printf '%s\n' \
    '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"probe","version":"0"}}}' \
    '{"jsonrpc":"2.0","method":"notifications/initialized"}' \
    '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"vexlo_get_values","arguments":{"traceId":"0f1a2b3c4d5e6f708192a3b4c5d6e7f8","path":["","AA","AB","AC","AD","AE","AF","AG","AH"]}}}' \
  | node dist/index.js
{"result":{"protocolVersion":"2025-06-18","capabilities":{"tools":{"listChanged":true}},"serverInfo":{"name":"vexlo-mcp","version":"0.1.0"}},"jsonrpc":"2.0","id":1}
{"result":{"content":[{"type":"text","text":"MCP error -32602: Input validation error: Invalid arguments for tool vexlo_get_values: Too big: expected array to have <=8 items at path"}],"isError":true},"jsonrpc":"2.0","id":2}
```

There is no JSON-RPC `error` member and no `kind`. Every such text opens
`MCP error -32602: Input validation error: Invalid arguments for tool <name>:` and ends with the
argument at fault, so it is readable without parsing. Each bound the three tool pages state
rejects this way, and these are the endings:

| Call | The text's ending |
| - | - |
| `vexlo_search_traces`, `limit: 101` | `Too big: expected number to be <=100 at limit` |
| `vexlo_check_trace_status`, 101 ids | `Too big: expected array to have <=100 items at traceIds` |
| `vexlo_find_related_traces`, `windowHours: 169` | `Too big: expected number to be <=168 at windowHours` |
| `vexlo_get_function_examples`, `limit: 6` | `Too big: expected number to be <=5 at limit` |
| `vexlo_search_traces`, a `teams` argument | `Unrecognized key: "teams"` |
| `vexlo_check_trace_status`, a `team` argument | `Unrecognized key: "team"` |
| `vexlo_get_function`, `environment: "prod"` | `Invalid input at environment` |

The last row is what an `environment` that is neither `{"named": [...]}` nor `{"unset": true}`
returns, both keys at once included.

## Two argument errors, captured

A trace id the server cannot parse is `invalid_request`, and the message says what the format
is:

```json theme={null}
{
  "kind": "invalid_arguments",
  "code": "invalid_request",
  "message": "trace_id must be 32 lowercase hex characters, got 14"
}
```

A `path` that is not a valid step path is the same kind:

```json theme={null}
{
  "kind": "invalid_arguments",
  "code": "invalid_request",
  "message": "path: \"!!not-base32!!\" does not decode"
}
```

A path that decodes but matches no step is not an error at all: it comes back in
`unknown_paths`, which [values](https://docs.vexlo.ai/mcp/tools/values.md) covers.

## A token revoked mid-session

Startup reads the token once. Revoking it while the process runs does not stop the process; the
next call that uses it returns:

```json theme={null}
{
  "kind": "authentication_failed",
  "message": "token revoked"
}
```

Restarting with a fresh token is the fix, and startup will refuse at once if the new one is
wrong.

## Rate limits and the one retry

The server's read limit is per token, and
[conventions](https://docs.vexlo.ai/api/conventions.md) has the numbers. This process does one thing on its own about
them: when a 429 carries `Retry-After` of 2 seconds or less, it waits that long and retries the
call once, silently. A `Retry-After` that small means a burst was grazed, and the extra
round trip costs less than an error the agent has to reason about.

Above 2 seconds, or when the retry comes back 429 as well, the error surfaces immediately. There
is no second retry and no growing backoff:

```json theme={null}
{
  "kind": "rate_limited",
  "retryAfterSeconds": 1,
  "limit": 20,
  "remaining": 0,
  "resetSeconds": 1
}
```

That one came from 120 `vexlo_list_tags` calls sent back to back in one session: 60 returned
their result and 60 returned this, each of the 60 having already been retried once. A 429 with
no `Retry-After` header reports `retryAfterSeconds: 60`, the cap the server applies to it.
`X-RateLimit-*` headers are copied onto an error result when the response carried them and
ignored on success: the process keeps no pacing state between calls.

Under [no-auth mode](https://docs.vexlo.ai/server/authentication.md) the server skips read rate limiting entirely, since
there is no token to key a bucket on, so none of this fires on a local development server.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.