Skip to content

Safe message interactions

Message interactions are a separate exact-ID policy boundary from reads and deletion. Set capabilities.interactions: true and list every writable channel or thread by its own ID in scopes.interactionChannelIds. An allowlisted parent grants read access to its threads but never grants interaction access to them. MCP hosts should treat processing signals, ordinary sends, edits, and own-reaction tools as writes and require approval before each call. Static Components V2 use the reviewed workflow above instead of this immediate interaction path.

signal_command_processing is the only transient processing-feedback tool. Call it only after receiving a command expected to take several seconds, with the exact channel and source message IDs. The source must be a regular or reply message from an ordinary non-bot, non-system user, must explicitly identify the verified connector bot in both Discord's parsed mentions and literal content, and must have mutually consistent Discord timestamp and snowflake evidence no more than two minutes old. The MCP adapter marks the tool non-destructive and non-idempotent because its visible effect can be repeated after a process restart.

The signal accepts only a text or announcement channel or an active unlocked public, private, or announcement thread. It freshly verifies pinned identity, exact interaction scope, thread-parent read scope, exact private-thread bot membership when applicable, the complete bounded role and overwrite inventory, VIEW_CHANNEL, READ_MESSAGE_HISTORY, and the applicable message-send permission. A source-bound process-local ledger coalesces concurrent and repeated calls while that source remains fresh. One shared rolling write budget still bounds all interactions, while independent transient and durable per-channel cooldown lanes prevent a processing signal from delaying the actual response.

After a content-free pending activity record, the connector sends one empty non-retried POST and accepts only Discord's exact 204 success. The result contains only exact identifiers, activity status, local replay state, and the ten-second expiry time; source content, mentions, user display data, and Discord response details are never returned or persisted. Discord provides no typing-state readback, so the expiry is the documented endpoint lifetime rather than verified remote state. A restart clears duplicate memory and can therefore issue another one-shot signal for the same still-fresh source.

send_message accepts plain text only and requires an idempotency key between 16 and 128 safe ASCII characters. Generate one key for one intended message, such as a UUID, and reuse that exact key with unchanged arguments for every retry. The connector derives a channel-bound 25-character nonce without sending, logging, or returning the raw key. Matching concurrent and recent in-process calls share one result. Discord also enforces nonce uniqueness for the past few minutes, which covers a connector restart inside that window. Reusing a key with different arguments is rejected, including when Discord returns an earlier nonce match whose content differs.

Idempotency is intentionally bounded rather than permanent. The local result ledger retains identifiers for ten minutes, and Discord documents only a past-few-minutes nonce window. If an uncertain send is left unresolved beyond those windows, inspect list_activity and the target channel before retrying. Never choose a fresh key merely because a result was uncertain, since that would authorize a second message.

All mention classes are suppressed by default. A call can notify only exact IDs present in scopes.mentionUserIds, up to ten per message, and each ID must also appear as a visible <@user-id> mention in the submitted content. Role, @everyone, and @here notifications remain suppressed. Reply-author notification is a separate explicit boolean; the connector fetches the exact reply target and permits that notification only when its author ID is configured. Replies use Discord's fail-if-target-missing behavior.

edit_own_message replaces the complete plain-text content of one exact message after a fresh ownership check. Webhook messages and messages owned by anyone other than the verified bot are rejected. An exact same-content request with no notification users is a journaled no-op that consumes no write budget. add_reaction and remove_own_reaction accept one Unicode emoji or custom name:snowflake value and use Discord's exact own-reaction endpoints with state-aware no-op handling and post-write verification. add_reactions applies the same primitive sequentially to two through ten unique logical emoji after validating the whole ordered set. Use it for acknowledgements, status markers, or a small emoji menu; use a native poll when participants are casting votes and the caller needs poll-specific aggregate state.

Every actual interaction write first reserves a local rolling budget and per-channel interval. These limits reject immediately with retryAfterMs; they do not sleep and are not hardcoded assumptions about Discord's dynamic rate limits. A content-free pending activity record must then succeed before the request leaves the process. Terminal records distinguish completed, failed, and uncertain outcomes. A success whose terminal journal write fails is reported as completed-audit-failed rather than hiding the external write.

The interaction tools return identifiers, jump URLs, status, activity IDs, aggregate reaction-set progress counts, and send nonces, but do not echo message content or reaction emoji. Discord's message resource reference documents allowed mentions, enforced nonces, replies, edits, reactions, and dynamic rate-limit behavior. Discord's typing-indicator contract documents the narrow multi-second command use, exact empty success, Gateway event, and ten-second expiry.

Canonical source: docs/reference.md

Documentation generated for guildcontrol@0.0.0. Canonical source and edit history remain in the public repository. GuildControl is an independent project and is not affiliated with or endorsed by Discord Inc. Discord is used only to identify the platform that GuildControl connects to.