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

--jsonPrint 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-runPrint 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:

commandidempotent
descant run cancelno — verify before retrying
descant repo updateno — verify before retrying
descant key createno — verify before retrying
descant key revokeno — verify before retrying
descant key rotateno — verify before retrying
descant invite createno — verify before retrying
descant invite revokeno — verify before retrying
descant repo grooming dispatchno — 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

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.

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.
conflict4091The resource is not in a state that admits this.
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.
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.