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 '.runs[] | {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` take no flags at all, 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 |
|---|---|
descant run cancel | no — verify before retrying |
descant repo update | no — verify before retrying |
descant key create | no — verify before retrying |
descant key revoke | no — verify before retrying |
descant key rotate | no — verify before retrying |
descant invite create | no — verify before retrying |
descant invite revoke | no — verify before retrying |
descant repo grooming dispatch | no — verify before retrying |
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. |
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. |
| conflict | 409 | 1 | The resource is not in a state that admits this. |
| 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. |
| 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.