Errors, rate limits, and async builds
Failure shapes, retry semantics, and the limits to respect.
The two failure layers
Transport-level — the request itself fails:
| Failure | Meaning | Handle by |
|---|---|---|
401 + WWW-Authenticate | Missing/expired/revoked token | Re-run OAuth discovery — never retry with ad-hoc credentials |
400 | Malformed request | Fix the request |
InvalidParams | Unknown tool, or arguments failed schema validation | Fix the call — retrying unchanged cannot succeed |
429 + Retry-After | Rate limited | Back off for the indicated time, then retry |
Tool-level — the call succeeded, the work failed. These return a normal MCP result with isError: true; its text content contains:
{
"error": {
"code": "...",
"message": "...",
"retryable": false
}
}Surface the message and honor retryable. A malformed or rejected request will not succeed unchanged.
Async is the norm
Outlines and builds take minutes. A turn that starts one returns immediately, and you drive it to completion by following run.nextAction:
- An
ask_operatorcall returns right away; it does not block until completion. nextActionis{tool:"get_operation",arguments:{moduleId,operationId},pollAfterMs}. Wait at least that long, then callget_operationwith those arguments.- Repeat
get_operationwith the same typed arguments and its returnedpollAfterMswhileterminalisfalse. When the current operation becomes terminal, follow itsnextActionif one is present; an operation still in progress is not a failure.
Rate limits
Per-IP limits on the HTTP surfaces:
| Surface | Limit |
|---|---|
| Public routes | 75 requests / 10s |
| Authenticated routes | 250 requests / 10s |
429 responses carry Retry-After. Honor it; with normal conversational pacing you will never see these limits.
The authenticated tool layer also applies per-user, organization, and tool admission controls:
| Tool class | Rate limit | Concurrency |
|---|---|---|
| Deterministic reads | 120 / minute | 12 |
consult_operator | 20 / minute | 3 |
ask_operator | 10 / 10 minutes | 1 |
If admission storage is unavailable, the server fails closed rather than running an uncontrolled write.
Operational hygiene
- Keep the correlation IDs from every response — they are the trace handle for any investigation.
- Supply a stable, unique
idempotencyKeytoask_operatorand reuse it only when retrying that same logical request. Completed results are replayed. If the server reports an indeterminate attempt, inspect chat or module state before starting a new request with a new key; do not retry blindly. Read tools can be retried independently. - One plan-level edit cycle at a time per module: edits while a build is active are blocked by design — wait for the operator's build-complete message on the same chat first.
Last updated on