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.
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
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’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: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:
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 isinvalid_request, and the message says what the format
is:
path that is not a valid step path is the same kind:
unknown_paths, which values 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:Rate limits and the one retry
The server’s read limit is per token, and conventions has the numbers. This process does one thing on its own about them: when a 429 carriesRetry-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:
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 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.