Error handling
discord-mcp treats Discord failures as tool results, not broken JSON-RPC
exchanges. Check isError, then branch on the stable structuredContent.code.
Wire contract
Section titled “Wire contract”Every formatted failure guarantees four structured fields:
{ "isError": true, "content": [{ "type": "text", "text": "Human-readable diagnosis and recovery" }], "structuredContent": { "code": "DISCORD_NOT_FOUND", "retriable": false, "category": "client", "recovery_hint": "Verify the ID, resource visibility, and credential scope." }}content[0].text carries the human-readable diagnosis. recovery_hint keeps
the next action machine-readable. Error-specific
structured fields can include retry_after_ms, missing, have, resource,
issues, suggested_tool, or trace_id; do not assume they exist for every
code.
Error families
Section titled “Error families”All custom errors extend DiscordError:
DiscordError├─ DiscordClientError└─ DiscordServerError| Code | Category | Retry as-is? | Meaning |
|---|---|---|---|
DISCORD_PERMISSION_DENIED | client | no | The route credential or resource lacks authorization. |
DISCORD_RATE_LIMITED | client | yes | Discord returned a rate limit and retry delay. |
DISCORD_NOT_FOUND | client | no | The resource does not exist or is not visible. |
VALIDATION_FAILED | client | no | Input failed schema or domain validation. |
DISCORD_AUTH_INVALID | client | no | Discord rejected the credential used by the route. |
DISCORD_CLOUDFLARE_BLOCKED | client | yes | Discord temporarily blocked the caller IP. |
SCOPE_REJECTED | client | no | The tool’s category is not enabled. |
GUILD_NOT_ALLOWED | client | no | Reserved; no runtime allowlist currently constructs it. |
DRY_RUN_PREVIEW | client | no | A confirm-gated tool was previewed, not executed. |
CANCELLED | client | no | The MCP client cancelled the call. |
DISCORD_SERVER_ERROR | server | yes | Discord returned an upstream server failure. |
CIRCUIT_OPEN | server | yes | The local circuit breaker is open. |
BULKHEAD_FULL | server | yes | The local REST concurrency limit rejected the call. |
INTERNAL_ERROR | server | yes | An unexpected failure reached the formatter. |
Raw @discordjs/rest failures are normalized before the generic fallback:
| Discord HTTP status | Public code | Retry as-is? |
|---|---|---|
400 | VALIDATION_FAILED | no |
401 | DISCORD_AUTH_INVALID | no |
403 | DISCORD_PERMISSION_DENIED | no |
404 | DISCORD_NOT_FOUND | no |
429 | DISCORD_RATE_LIMITED | yes, after retry_after_ms when supplied |
5xx | DISCORD_SERVER_ERROR | yes, with backoff |
The formatter exposes the HTTP status and numeric Discord error code when available, but never returns the REST URL or request body because either may contain webhook, interaction, or OAuth credentials.
There is currently no dedicated public timeout error. A Cockatiel timeout that
reaches the generic formatter is reported as INTERNAL_ERROR.
Dry-run preview
Section titled “Dry-run preview”Only tools registered with confirmation metadata-currently 29 tools-participate
in this gate. Ordinary writes such as messages_send can execute immediately.
{ "isError": true, "structuredContent": { "code": "DRY_RUN_PREVIEW", "retriable": false, "category": "client", "tool": "messages_delete", "preview": { "channel_id": "111122223333444455", "message_id": "999900001111222233" } }}To execute a gated call, the server must run with MCP_DRY_RUN=false and the
request must include __confirm: true. See Confirmation.
Resilience mappings
Section titled “Resilience mappings”The REST adapter explicitly maps only two Cockatiel failures:
| Cockatiel failure | Public code |
|---|---|
BrokenCircuitError | CIRCUIT_OPEN |
BulkheadRejectedError | BULKHEAD_FULL |
The formatter also handles those raw Cockatiel classes defensively. Cancellation
uses discord-mcp’s own CancelledError; Cockatiel timeout cancellation is not
currently converted into a dedicated domain error.