Building on Descant
For agents and scripts
How a program drives Descant through the descant binary: what it prints, what it does before it sends, what a retry costs, and what every exit code and refusal means. The commands themselves are on the CLI reference; setup is descant login, which keeps the key out of the environment your program inherits — or a variable on a CI runner or in a container, where the job is ephemeral and holds it as a secret. A machine with no keychain is not the case for the variable: the binary keeps the key in a file there, which your program does not inherit and which no process table shows.
Machine output
--json — Print the API's response unmodified. The default when stdout is not a terminal. A field the API adds after your copy of the binary was built still reaches your program, because nothing is parsed and re-serialised on the way out.
descant run list --json | jq '.items[] | {id, state}'On failure the binary reports the refusal's code, title, detail and requestId when the server sent them, and retryAfterSeconds when it was told to wait — and exits with the code from the table below. Branch on the exit code; read the fields for the reason.
Dry runs
--dry-run — Print the request that would be sent — method, URL, headers, body — and exit 0 without sending it. The bearer is replaced by where it came from — a variable name, or the store `descant login` put it in. Works on every command that builds a request, reads as well as mutations; `login`, `logout` and `whoami` do not take it, and `version` sends nothing. A credential must still resolve, because the request is built in full.
descant run list --dry-run
Retries and idempotency
These commands change state. For each, whether a replay with the same body returns the first response instead of acting twice:
| command | idempotent | key required |
|---|---|---|
descant run start | yes | no |
descant run cancel | yes | no |
descant run restart | yes | no |
descant repo connect | yes | no |
descant repo activate | yes | no |
descant repo disconnect | yes | no |
descant repo reconnect | yes | no |
descant repo validate | yes | no |
descant repo update | yes | no |
descant key create | yes | no |
descant key revoke | yes | no |
descant key rotate | yes | no |
descant invite create | yes | no |
descant invite revoke | yes | no |
descant invite send | yes | no |
descant repo grooming dispatch | yes | no |
descant settings engine-tier set | yes | no |
descant billing credit-pack buy | yes | yes |
descant billing auto-reload set | yes | no |
descant settings repo-defaults set | yes | no |
descant settings usage set | yes | no |
descant team organization rename | yes | no |
descant team invitation create | yes | no |
descant team invitation revoke | yes | no |
descant team member remove | yes | no |
descant billing spend-cap set | yes | no |
descant notifications mark-read | yes | no |
descant notifications email set | yes | no |
descant settings privacy set | yes | no |
descant capability decide | yes | no |
Exit code 6 means this CLI could not learn whether the call took effect. On the commands above, look before you retry: a blind retry of a mutation that landed can act twice. On a read it means only that the answer never reached you, so send it again.
Exit codes
| code | meaning |
|---|---|
| 0 | The call succeeded. |
| 1 | The server refused; change something and retry. |
| 2 | This invocation reached no verdict: an unknown command, a bad flag, or an answer that could not be read. |
| 3 | No credential, or the credential was rejected. |
| 4 | Rate limited. Honour Retry-After and come back. |
| 5 | No capacity yet. Not a refusal: read the response and wait. |
| 6 | The action may have taken effect. Verify before retrying; a blind retry can act twice. |
| 7 | The service, or something it depends on, failed. Retrying a read is safe. |
| 8 | Runs are paused for billing: out of credit, at the spend cap, or held on the account. A person clears it by adding credit, raising the cap, or asking support to lift the hold; retrying will not. |
Every refusal a command can meet
The closed set of problem codes the customer operations declare, plus the two the gate answers before any operation runs. Each maps to one exit code, so a wrapper never has to parse a sentence.
| code | HTTP | exit | meaning |
|---|---|---|---|
| invalid_body | 400 | 1 | The request body failed validation. |
| invalid_path | 400 | 1 | A path parameter failed validation. |
| invalid_query | 400 | 1 | A query parameter failed validation. |
| unauthenticated | 401 | 3 | No valid credential was presented. |
| forbidden | 403 | 3 | The credential is valid but may not do this. |
| not_found | 404 | 1 | No such resource for this caller. |
| billing_paused | 409 | 8 | This tenant's runs are paused for billing; resume billing before retrying. |
| conflict | 409 | 1 | The resource is not in a state that admits this. |
| github_link_required | 409 | 1 | The account that created this credential has no usable GitHub link; retrying will not help until one is linked or reconnected. |
| idempotency_key_in_flight | 409 | 4 | The first request with this Idempotency-Key is still running; retry with the same key and body shortly. |
| idempotency_key_reused | 422 | 1 | This Idempotency-Key was used with a different body. |
| invite_limit_reached | 422 | 1 | The tenant's invite limit is reached. |
| rate_limited | 429 | 4 | Too many requests for this credential or source. |
| internal_error | 500 | 7 | The request failed for a reason the caller cannot fix. |
| upstream_auth | 502 | 7 | An upstream service rejected this deployment's credential. |
| capacity_exhausted | 503 | 5 | No capacity is available for this request yet. |
| upstream_rate_limited | 503 | 4 | An upstream service is rate limiting us; retry after the stated interval. |
| upstream_timeout | 503 | 7 | An upstream service did not answer in time. |
| upstream_unavailable | 503 | 7 | A service this operation depends on did not answer. |
| outcome_unknown | 504 | 6 | The action may have taken effect; verify before retrying. |
The MCP server
The same operations, reached without a shell. The server builds one tool per programmatic operation from the registry the commands above come from, so a tool cannot exist there that the API does not serve, and the two surfaces cannot drift apart — every tool is one of the commands above.
It has its own page: MCP tools — the catalogue, the arguments each tool takes, the dry-run envelope, the refusal vocabulary a client branches on, and the environment the server reads its credential from.