The MCP server
The same operations div drives, as tools an MCP-capable agent calls directly — runs, evaluations, the registry, storage, secrets, arXiv, the catalog, billing.
@divergent/mcp is a second surface for what div does. It is not a wrapper around the binary — it talks to the control plane directly, the same way div does, so an agent with an MCP client skips the subprocess and the exit-code parsing entirely.
DIVERGENT_API_KEY=dv_live_... node apps/mcp/dist/index.js
Point an MCP client at that command. In Claude Code’s .mcp.json:
{
"mcpServers": {
"divergent": {
"command": "node",
"args": ["/path/to/divergent-studio/apps/mcp/dist/index.js"],
"env": { "DIVERGENT_API_KEY": "dv_live_..." }
}
}
}
The key
DIVERGENT_API_KEY is required. Mint one from the dashboard under Settings, or with div login — both work the same way here, since the server treats it as an ordinary bearer credential. DIVERGENT_API_URL defaults to https://api.divergentlabs.xyz; set it to talk to a local control plane instead.
There is no login flow in the server itself. div login writes a key to ~/.config/divergent/config.json so a person never has to paste one twice; this server never writes anything to disk, so the process that spawns it is the one holding the secret.
The tools
| Tool | What it does |
|---|---|
whoami |
which org this key belongs to |
catalog |
what gpu takes, the container each comes with, and what an hour of each costs |
create_run |
ships a directory to a volume and places a run, with any checks its result must pass, and cpu and memory_gib for a bigger container. A telemetry.toml in the directory lays out its telemetry |
list_runs |
the org’s runs, optionally narrowed to a project or a status |
get_run |
one run, by its id or its name, with its checks and their verdicts |
get_run_result |
what a finished run wrote to DIVERGENT_RESULT_PATH, or the value at one path in it |
get_run_telemetry |
what a run has written to DIVERGENT_TELEMETRY_PATH: every key with its last, lowest and highest value, or the curve of the keys asked for. See div telemetry |
cancel_run |
stops a run — a no-op, not an error, on one already finished |
get_run_logs |
a run’s log from after_seq on |
run_evaluation |
measures a model version for an evaluation — or answers with the run that already did. suite_paths says which files are the eval’s code, like --suite-path; setup is what one model’s runtime needs, never part of the suite, like --setup |
list_evaluations |
the org’s evaluations, each with the version that leads it |
get_evaluation_report |
an evaluation’s versions, best first, with earlier suites apart |
list_models |
the registry: every model’s versions, with where the weights are and what they weigh, like div model ls |
publish_model_version |
names weights on a volume as model:tag, sized on the volume, like div model publish |
list_volumes |
every volume the org holds |
list_volume_files |
what sits directly under a prefix on a volume |
storage_put |
uploads a local file or directory to a volume; a file that fails is listed under failed and the rest still go |
storage_get |
reads one file back — refused over 8MB |
list_secrets |
names and timestamps, never a value |
set_secret |
creates a secret or replaces its value |
delete_secret |
removes a secret |
billing_summary |
the org’s plan, credit, and spend |
search_papers |
searches arXiv, like div papers search: total matched, and each paper with its abstract |
get_papers |
reads papers by id, like div papers get, with the ids arXiv does not have under missing |
create_run does not wait
div run ships the directory, places the run, and stays attached until it stops — that is the right shape for a terminal, where someone is watching. create_run returns as soon as the run is placed: the id, its starting status, and what was uploaded. A run can run for hours, and a tool call that blocked for all of it would outlast any reasonable timeout.
Poll get_run for its status, or get_run_logs for its output — pass the highest seq you have seen back as after_seq to read only what is new, the same reconnect logic div logs uses:
create_run → {"run": {"id": "run-k4t9", "status": "queued", ...}, "uploaded": {...}}
get_run → {"status": "provisioning", ...}
get_run_logs(after_seq: 0) → [{"seq": 1, "message": "queued on H100", ...}, ...]
get_run_logs(after_seq: 1) → [{"seq": 2, ...}, ...]
get_run → {"status": "succeeded", "terminal": true, ...}
What a file comes back as
storage_get decides the same way the dashboard’s file explorer does: it tries to decode the bytes as UTF-8, and returns {"encoding": "utf8", "text": "..."} when that works or {"encoding": "base64", "base64": "..."} when it does not — never by looking at the extension. Anything over 8MB is refused rather than returned, with a pointer to div storage get instead.
Secrets are still write-only
list_secrets prints names and timestamps and nothing else — there is no tool, and no route on the API, that returns a value. set_secret takes the value as a plain argument, unlike div secrets set, which reads it from stdin so it never sits in shell history. An MCP tool call has no shell history to sit in, but it does travel through the calling client’s own conversation, so treat it with the same care you would a credential typed into a chat.