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:

commandidempotentkey required
descant run startyesno
descant run cancelyesno
descant run restartyesno
descant repo connectyesno
descant repo activateyesno
descant repo disconnectyesno
descant repo reconnectyesno
descant repo validateyesno
descant repo updateyesno
descant key createyesno
descant key revokeyesno
descant key rotateyesno
descant invite createyesno
descant invite revokeyesno
descant invite sendyesno
descant repo grooming dispatchyesno
descant settings engine-tier setyesno
descant billing credit-pack buyyesyes
descant billing auto-reload setyesno
descant settings repo-defaults setyesno
descant settings usage setyesno
descant team organization renameyesno
descant team invitation createyesno
descant team invitation revokeyesno
descant team member removeyesno
descant billing spend-cap setyesno
descant notifications mark-readyesno
descant notifications email setyesno
descant settings privacy setyesno
descant capability decideyesno

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

codemeaning
0The call succeeded.
1The server refused; change something and retry.
2This invocation reached no verdict: an unknown command, a bad flag, or an answer that could not be read.
3No credential, or the credential was rejected.
4Rate limited. Honour Retry-After and come back.
5No capacity yet. Not a refusal: read the response and wait.
6The action may have taken effect. Verify before retrying; a blind retry can act twice.
7The service, or something it depends on, failed. Retrying a read is safe.
8Runs 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.

codeHTTPexitmeaning
invalid_body4001The request body failed validation.
invalid_path4001A path parameter failed validation.
invalid_query4001A query parameter failed validation.
unauthenticated4013No valid credential was presented.
forbidden4033The credential is valid but may not do this.
not_found4041No such resource for this caller.
billing_paused4098This tenant's runs are paused for billing; resume billing before retrying.
conflict4091The resource is not in a state that admits this.
github_link_required4091The account that created this credential has no usable GitHub link; retrying will not help until one is linked or reconnected.
idempotency_key_in_flight4094The first request with this Idempotency-Key is still running; retry with the same key and body shortly.
idempotency_key_reused4221This Idempotency-Key was used with a different body.
invite_limit_reached4221The tenant's invite limit is reached.
rate_limited4294Too many requests for this credential or source.
internal_error5007The request failed for a reason the caller cannot fix.
upstream_auth5027An upstream service rejected this deployment's credential.
capacity_exhausted5035No capacity is available for this request yet.
upstream_rate_limited5034An upstream service is rate limiting us; retry after the stated interval.
upstream_timeout5037An upstream service did not answer in time.
upstream_unavailable5037A service this operation depends on did not answer.
outcome_unknown5046The 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.