MCP · Agents
Errors for agents
How a refusal reaches an agent, what to do with a 428, and how to find a failed call again.
What a refusal looks like#
Every API refusal is rendered as text lines in the tool result, plus a machine-readable copy in structuredContent.error:
✗ not_found (HTTP 404): Table 'habit' does not exist.
hint: Available tables: habits, checkins. Call list_tables for the full schema.Two optional lines follow when the refusal carries them:
confirmation_required: re-send the same call with confirmation_token="…". This will: <summary>
retry_after_seconds: 60The codes are the API's closed catalogue — see Errors. The hint is written so that the next call can be correct without asking a human: it names the exact problem, lists the valid values, and points at the tool that helps (list_tables, get_logs).
Successes are compact JSON projections of the response, also copied in structuredContent. A list carries cursor when there is a next page.
Confirming a guarded gesture#
Some gestures are irreversible or reach beyond what the call names: destructive SQL, changing a preset, deleting a project or a user, issuing a reset link, changing the app URL. The first call answers 428 confirmation_required — nothing has happened — with a summary of the consequence and a confirmation_token. To proceed, re-send the same call with the same arguments plus confirmation_token. The token is single-use and lives 5 minutes; if the target changed in between, the API answers 428 again with a fresh summary and token. Read the summary before confirming: it is the only description of what the gesture will do.
Some of these gestures also require a signed-in human. The tool description says so ("Requires human elevation"), and without the elevation token the API answers 401 with a hint that names the gesture to make from the dashboard — no confirmation token is issued. Which gestures, and how the elevation token works, is in The human boundary.
Refusals that are not errors#
404 not_foundnever tells you whether something exists. A project of another account, a project outside the key's scope and a project that never existed answer the same bytes.get_github_linkreturninglink: nullis an answer, not a refusal.409 conflictmeans what you asked already exists or no longer applies: retry with a different intent, not the same call.401onPOST /v1/auth/signupmeans the publishable key is unknown, revoked or rotated. (Until 2026-09-07 it could also mean the key was “inert” because an agent had created it — that rule is gone.)429 rate_limitedcarriesretry_after_seconds; wait that long. On a management gesture — creating, deleting or exporting a project, deploying or rotating a function, listing its secret names, setting a project secret, deploying a site, applying a shared secret — it can also mean the administration budget is full, and then the delay is 300 seconds. Nothing ran: wait, then send the same call again, idempotency key included. Looping faster only keeps the budget full — a tight retry loop is what this budget exists to refuse.402 quota_exceedednames the quota and its figure — a human frees space or upgrades.
Finding a failed call again#
Every response carries a request_id, in the body of every 5xx and in the X-Request-Id header of every response. The same id is written in the project's request log: get_logs with status: "gte.400" shows the refusal with its error_code, path and plane. Retrying a 500 is fine; a 502 says a third party refused and retrying identically will not change anything; a 503 says retry in a few seconds — or after retry_after_seconds when the platform is limiting requests, or when a management gesture ran out of administration budget part-way through. That last one is the only refusal here that is not all-or-nothing: some of the work landed, so read the current state before sending the call again.
Two things the local rendering does before calling#
query_rowsrefuses locally a filter on a column namedorder,limitorcursor: these are reserved query parameters on the wire. Userun_sqlor rename the column.upload_filerefuses locally content that is not valid base64, anddeploy_siterefuses locally a site above 30 files or 100 KB in total, naming the GitHub workflow as the way to deploy a real build.
