Skip to content
esc

Search pages and sections. Try a tool name, an error code, a route.

↑↓ navigate↵ openesc close

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: 60

The 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_found never 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_link returning link: null is an answer, not a refusal.
  • 409 conflict means what you asked already exists or no longer applies: retry with a different intent, not the same call.
  • 401 on POST /v1/auth/signup means 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_limited carries retry_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_exceeded names 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_rows refuses locally a filter on a column named order, limit or cursor: these are reserved query parameters on the wire. Use run_sql or rename the column.
  • upload_file refuses locally content that is not valid base64, and deploy_site refuses locally a site above 30 files or 100 KB in total, naming the GitHub workflow as the way to deploy a real build.