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 | The resolved guild is not in ALLOWED_GUILDS. |
GUILD_SCOPE_UNRESOLVED |
client | no | An active guild allowlist cannot prove the target guild before execution. |
BOT_SCOPE_UNRESOLVED |
client | no | An application-scoped write cannot be tied to the identity-locked bot application. |
RUNTIME_ACCESS_DENIED |
client | no | Complete runtime evidence proves a declared permission, intent, or hierarchy requirement is missing. |
RUNTIME_ACCESS_UNKNOWN |
client | no | Runtime identity, scope, permission, or intent evidence could not be completed. |
DRY_RUN_PREVIEW |
client | no | A confirm-gated tool was previewed, not executed. |
WRITE_PREVIEW |
client | no | Global write-preview mode blocked a mutation before Discord. |
DM_CONSENT_REQUIRED |
client | no | A recipient-bound caller approval is required before opening a DM channel. |
DM_CONSENT_REJECTED |
client | no | A DM approval is missing, expired, replayed, or bound to a different recipient. |
DM_OUTCOME_UNKNOWN |
client | no | A DM POST may have landed, but its response was lost; verify state before any new attempt. |
PAYLOAD_CONFIRMATION_REQUIRED |
client | no | A Components V2 send/edit/template call was previewed and needs an exact payload hash approval. |
PAYLOAD_CONFIRMATION_MISMATCH |
client | no | The supplied Components V2 approval hash does not match the current payload. |
PAYLOAD_CONFIRMATION_APPROVAL_MISSING |
client | no | The one-time Components V2 approval ID is absent or unknown to this process. |
PAYLOAD_CONFIRMATION_APPROVAL_EXPIRED |
client | no | The one-time Components V2 approval expired before execution. |
PAYLOAD_CONFIRMATION_APPROVAL_REPLAYED |
client | no | The one-time Components V2 approval was already consumed. |
PAYLOAD_CONFIRMATION_APPROVAL_MISMATCH |
client | no | The approval ID is bound to a different locked bot, tool, target, or payload. |
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”Legacy confirmation metadata currently covers 31 destructive tools. Components
V2 send/edit/send_from_template writes use the separate payload-bound gate; ordinary writes
such as messages_send can still execute immediately unless
MCP_WRITE_MODE=preview is enabled.
{ "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.
For a Components V2 write, the first call returns
PAYLOAD_CONFIRMATION_REQUIRED with payload_hash, approval_id, its expiry,
and risk_flags. Retry before expiry with MCP_DRY_RUN=false, __confirm:true,
the exact __confirm_hash, and __confirm_id. The ID is consumed once; a
replayed or expired ID must be replaced by a fresh preview.
If any target or component value changes, the server returns
PAYLOAD_CONFIRMATION_MISMATCH and makes no Discord request.
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.

