Skip to main content
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

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:
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 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 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 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 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:
An unreachable server says so, and only this case says “could not reach”:
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:
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:
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:
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 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:
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 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.