> ## 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.

# Startup checks and exit codes

> What the process checks before it registers a tool, where its diagnostics go, and what each exit code means.

The Vexlo MCP server refuses to start rather than fail on the first tool call. It reads
`VEXLO_API_URL` and `VEXLO_TOKEN` once, makes exactly two HTTP calls in order, and registers no
tool until both have answered. Each failure exits with its own code and one line on stderr.

## The two calls

| Order | Call | Credential | Checked for |
| - | - | - | - |
| 1 | `GET /v1/capabilities` | none; the route takes no token | `api_version` equals `v1` by exact string match |
| 2 | `GET /v1/whoami` | `Authorization: Bearer $VEXLO_TOKEN` | `role` equals `read` |

Both bodies, from the server these pages were captured against. The server writes compact
JSON, so each arrived as one line; the blocks below are `jq .` renderings of it:

```json theme={null}
{
  "server_version": "dev",
  "api_version": "v1",
  "features": [
    "in_flight_attempts"
  ],
  "auth_mode": "bearer",
  "limits": {
    "record_format_versions": [
      1,
      2
    ],
    "search_limit_default": 50,
    "search_limit_max": 500,
    "trace_steps_limit_default": 500,
    "trace_steps_limit_max": 500,
    "values_max_paths": 8,
    "trace_steps_max": 10000,
    "ingest_body_max_bytes": 4194304,
    "ingest_gzip_expansion_max_bytes": 33554432,
    "read_rate_limit_rps": 20,
    "read_rate_limit_burst": 40,
    "ingest_rate_limit_records_per_s": 4000,
    "ingest_rate_limit_records_burst": 8000,
    "ingest_rate_limit_bytes_per_s": 4194304,
    "ingest_rate_limit_bytes_burst": 8388608
  }
}
```

```json theme={null}
{
  "role": "read",
  "team_ids": [
    "demo",
    "fixture"
  ],
  "service_scope": null,
  "environment_scope": null
}
```

What those answers settle holds for the whole process life. `team_ids` is the token's team-set,
which every scoped tool compares a requested `team` against before it builds a query.
`limits` supplies the bounds the tool schemas carry: the search ceiling is the lower of 100 and
`search_limit_max`, the function and tag list ceiling the lower of 200 and the same field,
`vexlo_get_values` accepts `values_max_paths` paths, and `vexlo_get_trace` asks for
`trace_steps_limit_max` steps per call. `service_scope` and `environment_scope` carry the
token's own pins, `null` when it has none; `vexlo token create` takes `-service` and
`-environment` for a read token too, which
[authentication and tokens](https://docs.vexlo.ai/server/authentication.md) covers. A service-pinned read token never
gets past this call, so in any session that did start `service_scope` is `null`, and a
misspelled service name is indistinguishable from a real, empty one until the server answers:
[find](https://docs.vexlo.ai/mcp/tools/find.md) covers `vexlo_check_coverage`, which is how an AI agent learns the real
names. `features` is stored and read by no tool today.

One module reads the two variables, once, before anything touches the network or stdio, and the
session it builds is frozen. Nothing re-reads either variable and nothing re-runs the two calls,
so a token revoked while the process is up still passed startup: the next tool call that uses it
returns `authentication_failed`, which [errors, rate limits,
retries](https://docs.vexlo.ai/mcp/errors-and-rate-limits.md) covers. Restarting the process is what picks up a new
token or a new server.

## stdout carries the protocol, stderr carries everything else

The MCP stdio transport owns stdout for as long as the process runs: every byte on it is
newline-delimited JSON-RPC. Diagnostics, including every message below, go to stderr. A wrapper
script that prints anything to stdout breaks the session.

## Exit codes

| Code | Condition | What to fix |
| - | - | - |
| 1 | `VEXLO_API_URL` or `VEXLO_TOKEN` missing or empty, or `VEXLO_API_URL` not parseable as a URL | the environment the host passes |
| 2 | `GET /v1/capabilities` could not be reached, or answered something unusable | the URL, or the server |
| 3 | the server's `api_version` is not `v1` | upgrade `vexlo-mcp`, or point it at a compatible server |
| 4 | `GET /v1/whoami` returned a mapped error | the token |
| 5 | the token's role is not `read` | mint a `read` token |
| 70 | anything else the process rejects with | report it; this one is a defect, not configuration |

Code 70 is `EX_SOFTWARE` from `sysexits.h`, kept distinct from 1 because Node's own default for
an uncaught failure is 1, which would collide with the configuration case. Two process-level
handlers route a late failure to it as well, so a bug after the session is up never exits 1.

A missing variable names itself:

```
$ VEXLO_API_URL=http://127.0.0.1:18300 node dist/index.js
invalid startup configuration —
VEXLO_TOKEN: Invalid input: expected string, received undefined
exit status: 1
```

An unreachable server says so, and only this case says "could not reach":

```
$ VEXLO_API_URL=http://127.0.0.1:18349 VEXLO_TOKEN=$READ_TOKEN node dist/index.js
could not reach the vexlo server at http://127.0.0.1:18349 (GET /v1/capabilities): fetch failed
exit status: 2
```

A version mismatch prints both versions. This is the one run on this page that needs a stand-in
server. No `vexlo` build reports an `api_version` other than `v1`, and the whole `limits` block
is parsed before the versions are compared, so the line below came from a stand-in that answers
`GET /v1/capabilities` with `api_version: "v2"` and a complete `limits` object:

```
$ VEXLO_API_URL=http://127.0.0.1:18301 VEXLO_TOKEN=$READ_TOKEN node dist/index.js
server at http://127.0.0.1:18301 reports API version "v2", but this client only supports "v1" — upgrade vexlo-mcp, or point VEXLO_API_URL at a server running a compatible API version
exit status: 3
```

Every other run here needs no stand-in. Exits 4 and 5 run against the reader's own server.
Exits 1, 2 and 70 reach no server at all: `VEXLO_TOKEN` is unset, the port is empty, and the
request URL cannot be built.

A token the server does not know fails at the second call, carrying the server's own message:

```
$ VEXLO_API_URL=http://127.0.0.1:18300 VEXLO_TOKEN=$UNKNOWN_TOKEN node dist/index.js
GET /v1/whoami failed: unknown token
exit status: 4
```

A read token pinned to a service fails at the same call, but with the body-shape message
instead, because the client expects `service_scope` to be an array of names and the server
sends a bare string:

```
$ VEXLO_API_URL=http://127.0.0.1:18300 VEXLO_TOKEN=$SERVICE_PINNED_TOKEN node dist/index.js
GET /v1/whoami failed: response body matched neither the error envelope nor the expected shape (status 200)
exit status: 4
```

That is this client's own gap, not a server error: the same token answers `/v1/whoami` with
`"service_scope": "delivery-service"` and HTTP 200, and an environment-pinned read token starts
normally. Mint the MCP server's read token without `-service`.

A token of the wrong role names the role it resolved to:

```
$ VEXLO_API_URL=http://127.0.0.1:18300 VEXLO_TOKEN=$INGEST_TOKEN node dist/index.js
VEXLO_TOKEN resolves to role "ingest" — vexlo-mcp requires a read-role token, since every tool it exposes calls a read-role route
exit status: 5
```

`VEXLO_API_URL` has to carry its scheme. `localhost:18300` parses as a URL whose scheme is
`localhost:`, so it passes the check behind code 1 and fails when the first request is built:

```
$ VEXLO_API_URL=localhost:18300 VEXLO_TOKEN=$READ_TOKEN node dist/index.js
TypeError: Invalid URL
    at new URL (node:internal/url:911:25)
... 9 lines omitted
  code: 'ERR_INVALID_URL',
  input: '/v1/capabilities',
  base: 'localhost:18300'
}
exit status: 70
```

`http://127.0.0.1:18300` is the same address written correctly. `127.0.0.1:18300` exits 1
instead: `127.0.0.1` cannot be a URL scheme, where `localhost` can.

## What startup costs

As the server sent them, one line each, the two bodies measure 580 bytes (198 tokens) and 92
bytes (26 tokens), so the handshake is 672 bytes and 224 tokens, measured on this server with
the tokenizer [token costs](https://docs.vexlo.ai/mcp/token-costs.md) names. The indented blocks above are wider than
that, because indenting them added bytes the wire never carried. The handshake is paid once per
process launch and appears in no tool result. A larger `features` or `limits` block on a future
server moves the capabilities half of that figure.

Each startup call gets 10 seconds; each later tool call gets 30. Both are chosen, not measured:
long enough for a cold local server, short enough that a hung connection does not leave the host
waiting a full minute.


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