Skip to content

Error handling

discord-mcp treats Discord failures as tool results, not broken JSON-RPC exchanges. Check isError, then branch on the stable structuredContent.code.

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.

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.

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.

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.