Skip to content

aegis · Reference

Error codes

Every JSON-RPC error code aegis returns, what stage produced it, the matching HTTP status, and how a client should handle each refusal.

On this page

Every refusal from aegis is a JSON-RPC 2.0 error object with a specific code, so your client can branch on exactly why a call stopped: a policy denial, a threat block, a held call or an unavailable upstream. This page lists every code the invoke path (/mcp and /v1/invoke) returns, what triggers it and what to do next.

How errors come back#

An error response has the standard shape: error.code, a readable error.message and, for a few codes, an error.data object. The id echoes your request, except on an HTTP 400, where the body could not be read as a request and id is null. Every JSON-RPC response also carries an X-Correlation-ID header, which ties it to the gateway's logs and audit records. If you send a valid W3C traceparent header, its trace-id becomes the correlation ID.

Most refusals return HTTP 200 with the error in the body, so check error.code, not the status. These are the exceptions:

HTTPWhenBody
400The body is not JSON, or is not a valid JSON-RPC 2.0 request-32700 or -32600
401Missing, expired or invalid bearer token, or a tenant this gateway does not serve{"error":"unauthorized: ..."}, not JSON-RPC
401The X-Session-ID you sent does not exist or has expired-32603, Invalid or expired session
402Monthly quota exceeded (commercial, enforce mode only)-32060
403RBAC denied the call-32003
403The session belongs to another subject-32603, Session does not belong to caller
413The request body is larger than 2 MiBPlain text, not JSON-RPC
429Rate limit exceeded-32029
500The session store could not be read or written-32603

A notification, a request with no id or a null one, gets HTTP 202 with an empty body once it is authenticated and parsed. It never produces a JSON-RPC error.

Standard JSON-RPC codes#

CodeMeaningHTTPWhen aegis returns it
-32700Parse error400The body is not JSON.
-32600Invalid request400 or 200HTTP 400 when the JSON is not a valid request: jsonrpc is missing or not "2.0", or method is missing, empty or not a string. HTTP 200 when a tools/call has no params.name.
-32601Method not found200The method is not initialize, ping, tools/list or tools/call (Method not found: ...), or the tool is unknown (Method not found: Unknown tool: ...). A tool that belongs to another tenant returns the same unknown-tool error.
-32602Invalid params200The arguments failed validation against the tool's inputSchema. The message names the tool and the reason.
-32603Internal error401, 403 or 500A session problem, as listed in the table above.

aegis codes#

The twelve codes from -32003 to -32064 are specific to aegis. -32000 is the generic upstream error, covered under Upstream errors below.

CodeMeaningStageHTTPWhat to do
-32003Access denied by RBACAuthorization403Ask an operator for a role whose policy allows this tool. Retrying does not help.
-32029Rate limit exceeded on a tools/call. The message names the scope, session or tool.Rate limiting429Back off until the next one-minute window.
-32050Tool quarantined because its definition changed after it was pinnedThreat: integrity200An operator reviews the new definition and releases the quarantine with POST /admin/v1/quarantine, which re-pins the current definition. Until then every call is refused.
-32051Injection pattern in the call's params. The message names the category, rule and location.Threat: injection200Change the input. The same arguments are refused again.
-32052Response withheld because it contained a credential, card number or SSN. The message names the classes found.Response scan200Returned only with PII_ACTION=block. The tool has already run.
-32053A credential, card number or SSN in the call's params (egress DLP). The message names the classes found.Threat: egress200Remove the sensitive value from the arguments. The call did not reach the upstream.
-32054The credential broker could not supply the server's credential, so aegis did not forward the callCredential broker (commercial)200An operator checks that the broker is enabled and that the server's secret_ref resolves.
-32060Monthly quota exceededQuota (commercial)402Returned only when billing is enabled and BILLING_ENFORCE=1. An operator moves the tenant to a higher tier, or you wait for the next UTC calendar month.
-32061Held for approval, denied, or the approvals store is not configured or unreachableApprovals200If data.status is pending, resend the identical call after an operator approves it.
-32062Stopped by the emergency kill-switch at tenant, server or tool scopeKill-switch200Wait for an operator to deactivate the switch.
-32063Upstream fast-failed: at its concurrency cap, or its circuit is openResilience200Nothing was sent. Retry after a short delay.
-32064Upstream did not answer within the deadline, so the outcome is unknownForwarding200The tool may have run. Confirm the outcome before you retry a non-idempotent call.
-32000Upstream errorForwarding200See below.

Checks run in a fixed order. The session, rate limit and quota come first, then routing, the kill-switch and RBAC. So another tenant's tool returns -32601, and a stopped tool returns -32062 whatever the caller's policy says. How aegis works gives the full order of checks.

Codes that carry data#

Three codes put structured detail in error.data.

-32060 carries tier (free, growth or enterprise), limit, used and period (the UTC month as YYYYMM), so a client can report which cap it hit.

-32061 carries a status of pending, denied or unavailable. A pending hold also carries the approval_id, which operators see as the id of the request in GET /admin/v1/approvals:

json
{
  "jsonrpc": "2.0",
  "error": {
    "code": -32061,
    "message": "Approval required for echo on mock-primary",
    "data": { "approval_id": "9b1d3c2e-...", "status": "pending" }
  },
  "id": 3
}

An approval is tied to the caller, the server, the tool and a hash of the exact params. Sending the same call again while it is pending returns the same approval_id. Once approved, the next identical call from the same caller runs and uses up the approval. Both the approval and a denial apply only within APPROVAL_TTL_SECS (default 3600) of when the call was first held. See Hold, approve and stop tool calls.

-32064 tells the caller plainly that the outcome is unknown:

json
{
  "jsonrpc": "2.0",
  "error": {
    "code": -32064,
    "message": "Upstream error: upstream did not respond within 10s; the call may have executed",
    "data": {
      "executed": "unknown",
      "safe_to_retry": false,
      "guidance": "The upstream received this call and did not answer in time. It may have executed. Confirm the outcome before retrying; if the tool is idempotent, retry with the SAME idempotency key."
    }
  },
  "id": 7
}

The deadline is UPSTREAM_TIMEOUT_SECS (default 10), and the audit log records the call as indeterminate, not error.

Upstream errors#

aegis returns -32000 with a message that starts Upstream error: when the upstream could not be reached, answered with a non-2xx HTTP status, or sent a reply aegis could not read. Before giving up, aegis retries connection failures, where the request never left, up to UPSTREAM_MAX_RETRIES times (default 2). It does not retry timeouts or HTTP errors, because the tool may have run. For the same reason, treat a -32000 on a non-idempotent tool the way you treat -32064. The detail is capped at 300 characters, and any brokered credential is removed from it.

Each -32000 or -32064 counts toward the circuit breaker for that tenant and server. After CIRCUIT_FAILURE_THRESHOLD consecutive failures (default 5), the circuit opens and calls fast-fail with -32063 for CIRCUIT_COOLDOWN_SECS (default 10). After that, one probe call goes through, and a success closes the circuit.

If the MCP server answers with a 2xx response that holds its own JSON-RPC error, aegis passes it through with the server's code, after removing any brokered credential and scanning it for PII and secrets like any other response. That reply does not count against the circuit breaker. A code not listed on this page came from the MCP server, not from aegis.

When a code is not returned#

Several checks can log a finding and let the call through instead of refusing it:

  • THREAT_MODE=monitor: -32050, -32051, -32053 and -32052 are logged, not returned.
  • INPUT_VALIDATION=monitor or off: no -32602 from schema validation.
  • EGRESS_ACTION=monitor or off: no -32053.
  • PII_ACTION=redact (the default) or monitor: no -32052. Hits are masked or only logged.
  • Email addresses, phone numbers and dates of birth never cause -32052 or -32053. Only credentials, card numbers and SSNs block.
  • Billing not enabled, or BILLING_ENFORCE unset or 0: no -32060. With billing on and enforcement off, overages are recorded. If the usage meter is unreachable, or the tier store has never loaded, the call counts as under quota.
  • RATE_LIMIT_PER_SESSION=0 or RATE_LIMIT_PER_TOOL=0 turns that limit off (defaults are 120 and 60 calls per minute). If Redis is unreachable, the limiter lets calls through.
  • UPSTREAM_MAX_CONCURRENT=0 turns off backpressure (default 64 in-flight calls per upstream), so -32063 then comes only from an open circuit.

Each setting is described in Configuration, and the threat checks in Threat detection.

Handling errors in a client#

ResponseCodes
Retry after a delay-32029, -32063
Resend the identical call after approval-32061 with status pending
Fix the request-32700, -32600, -32601, -32602, -32051, -32053
Needs an operator-32003, -32050, -32054, -32060, -32061 with status denied or unavailable, -32062
Confirm the outcome before any retry-32064, -32052, and -32000 on a non-idempotent tool

On HTTP 401 with a body of {"error":"unauthorized: ..."}, get a fresh token. If the message says your tenant is not served, the token is valid but this gateway instance does not serve that tenant. If the body is a -32603 session error, drop the X-Session-ID header. aegis mints a new session and returns its ID in the X-Session-ID response header. Log X-Correlation-ID with every error so an operator can find the call in the gateway's records. For the endpoints behind these codes, see the API reference.