discord-mcp
by willuhmjs
README.md
# discord-mcp
An MCP server that exposes the Discord API to LLM agents. Node.js + discord.js +
TypeScript rewrite of [SaseQ/discord-mcp](https://github.com/SaseQ/discord-mcp)
(Java/Spring/JDA) — a **drop-in replacement** for all 75 of its tools, plus the
rich-messaging, interactions, threads, AutoMod and server-config coverage the
original lacked.
Built for [smartbot](https://github.com/willuhmjs/smartbot), a Discord bot that
lets an LLM call these tools over MCP, but works with any MCP client
(Claude, Inspector, ...).
## Highlights
- **All 75 legacy tools**, same names, same parameters (`send_message`,
`kick_member`, `create_forum_channel`, ...). smartbot connects unchanged.
- **Rich messages**: embeds, Components V2, polls, files, replies, forwards,
stickers, silent mode, allowed-mention policies — validated before anything
reaches Discord, with errors that name the bad field
(`embedsJson[0].fields[3].value: 1100 chars > 1024`).
- **Interactions**: role menus that survive restarts (behaviour encoded in
`mcp:` custom ids), ephemeral canned replies, and an interaction log the LLM
can read.
- **Full guild surface**: threads, members, moderation, roles, AutoMod,
welcome screen / onboarding / widgets / templates, events, invites,
webhooks, emoji/stickers/soundboard, audit log.
- **Security by default**: binds `127.0.0.1`, every URL fetch goes through a
SSRF-guarded fetcher (private/metadata IP blocking, redirect re-checks, size
and content-type limits), webhook tokens are never echoed back.
## Requirements
- Node.js **22+**
- A Discord bot token from the
[developer portal](https://discord.com/developers/applications)
## Setup
```bash
npm ci
npm run build
DISCORD_TOKEN=... DISCORD_GUILD_ID=... node dist/index.js
```
Output: `ready as <bot tag>, <N> tools`, then it serves MCP on
`http://127.0.0.1:8085/mcp`.
### Environment variables
| Variable | Default | Meaning |
|---|---|---|
| `DISCORD_TOKEN` | — (required) | Bot token. Exit non-zero with a readable error if invalid. |
| `DISCORD_GUILD_ID` | — | Default guild for tools when `guildId` is omitted. |
| `ENABLE_MEMBERS_INTENT` | off | Enables the privileged GuildMembers intent. Member-list tools degrade with a clear message when off. |
| `HOST` / `SERVER_ADDRESS` | `127.0.0.1` | HTTP bind address. `SERVER_ADDRESS` is the legacy Java name. |
| `PORT` / `SERVER_PORT` | `8085` | HTTP port. `SERVER_PORT` is the legacy name. |
| `MCP_TRANSPORT` | `http` | `stdio` to speak MCP on stdin/stdout instead of HTTP. |
| `SPRING_PROFILES_ACTIVE` | — | Legacy compat: if set to anything other than `http`, stdio mode (matches the Java server's behaviour). |
### Intents to enable in the developer portal
Enable under *Bot → Privileged Gateway Intents*:
- **Message Content** — required (the whole point is reading and posting
messages)
- **Server Members** — only if you run with `ENABLE_MEMBERS_INTENT=1`
Everything else the server requests (guilds, messages, moderation,
expressions, voice states, scheduled events, reactions, polls, AutoMod
config, invites, webhooks, DMs) is unprivileged and needs no portal toggle.
## Transports
- **Streamable HTTP (default):** `POST /mcp`, stateful sessions via the
`Mcp-Session-Id` header (JSON responses, `DELETE /mcp` terminates a
session). `GET /health` returns `200` once the Discord client is ready.
- **stdio:** `MCP_TRANSPORT=stdio` (or `SPRING_PROFILES_ACTIVE` ≠ `http`) —
status logs go to stderr, protocol on stdout.
smartbot's streamable-HTTP MCP client connects to `http://127.0.0.1:8085/mcp`
with no changes.
## Conventions (what tool consumers rely on)
- Every ID is a **string** (snowflakes overflow JS numbers).
- Every guild-scoped tool takes an optional `guildId` that falls back to
`DISCORD_GUILD_ID`.
- Structured payloads (`embedsJson`, `componentsJson`, `pollJson`,
`filesJson`, ...) are **JSON strings**, validated with field-level errors.
- Tool results are short plain text (confirmations, one line per message,
jump URLs). Errors come back as MCP tool errors with readable messages —
`Discord API error 50013 (HTTP 403): Missing Access — ... role is at/above
its highest role`.
- Every mutating tool takes an optional `reason` for the audit log.
- List tools cap output around 6 KB (smartbot cuts results at 6000 chars).
### Rich message parameters
`send_message`, `send_private_message`, `send_webhook_message` and
`create_forum_post` accept:
| Param | Shape |
|---|---|
| `message` | Plain text content (≤ 2000 chars). Optional when embeds/components/poll/files are present. |
| `embedsJson` | `[{title, description, url, timestamp, color, footer:{text, icon_url}, image:{url}, thumbnail:{url}, author:{name, url, icon_url}, fields:[{name, value, inline}]}]` — max 10, 6000 chars total. |
| `componentsJson` | Raw Discord component JSON: action rows (type 1) with buttons (2) / selects (3, 5, 6, 7, 8), or V2 types (9 section, 10 text display, 11 thumbnail, 12 media gallery, 13 file, 14 separator, 17 container). |
| `componentsV2` | Send as Components V2 — components only (no content/embeds/stickers/poll). |
| `pollJson` | `{question:{text}, answers:[{poll_media:{text, emoji?}}] (≤10), duration (hours 1–768), allow_multiselect}`. Polls can't be edited. |
| `filesJson` | `[{url, filename?, description?, spoiler?}` or `{base64, filename, description?, spoiler?}]` — `attachment://filename` works in embeds. |
| `replyToMessageId` | Reply to a message. |
| `stickerIds` | Up to 3 sticker IDs. |
| `allowedMentions` | `users` (default) / `users_roles` / `all` / `none`. |
| `silent` / `suppressEmbeds` | Notification suppression / hide embeds. |
### Built-in interactive components
Non-link buttons/selects must be acknowledged within 3 s — this server handles
them, encoded in `custom_id` so nothing is lost on restart:
| `custom_id` | Behaviour |
|---|---|
| `mcp:role:<roleId>` | Toggle that role on the clicker + ephemeral confirmation. Refuses managed/dangerous roles and roles at/above the bot's top role (checked at menu creation *and* click time). |
| `mcp:roleselect` | Role select menu: selected roles added, deselected removed (same safety checks). |
| `mcp:reply:<base64>` | Ephemeral canned reply (≤ 80 chars). |
| anything else | Acknowledged (deferUpdate) and recorded. |
`create_role_menu` posts embed+components menus in one call;
`list_interactions` shows who clicked what (last 200, in memory).
## Tools
### Messages
`| Tool | Key params | Requester permission |` — requester permission is what the calling Discord user (not the bot) needs, for smartbot's permissions.py. Optional params are marked with `?`.
| Tool | Key params | Requester permission |
|---|---|---|
| send_message | channelId, message?, embedsJson?, componentsJson?, componentsV2?, pollJson?, replyToMessageId?, stickerIds?, filesJson?, allowedMentions?, silent?, suppressEmbeds? | send_messages |
| edit_message | channelId, messageId, newMessage?, embedsJson?, componentsJson?, componentsV2?, keepAttachments?, suppressEmbeds? | manage_messages |
| delete_message | channelId, messageId | manage_messages |
| read_messages | channelId, count?, before?, after?, around? | read_message_history |
| add_reaction | channelId, messageId, emoji | add_reactions |
| remove_reaction | channelId, messageId, emoji | manage_messages |
| get_attachment | channelId, messageId, attachmentId? | read_message_history |
| forward_message | channelId, messageId, targetChannelId | send_messages |
| get_message | channelId, messageId | read_message_history |
| search_messages | guildId?, content?, authorId?, channelId?, mentionsUserId?, has?, before?, after?, limit? | read_message_history |
| pin_message | channelId, messageId, reason? | manage_messages |
| unpin_message | channelId, messageId, reason? | manage_messages |
| list_pins | channelId | read_message_history |
| bulk_delete_messages | channelId, messageIds?, count?, reason? | manage_messages |
| crosspost_message | channelId, messageId | manage_messages |
| list_reactions | channelId, messageId, emoji, limit? | read_message_history |
| clear_reactions | channelId, messageId, emoji? | manage_messages |
| remove_user_reaction | channelId, messageId, emoji, userId | manage_messages |
| end_poll | channelId, messageId | manage_messages |
| get_poll_voters | channelId, messageId, answerId, limit? | read_message_history |
| send_typing | channelId | send_messages |
### Channels
All tools in `src/tools/channels.ts`. Every Discord ID is a string. Legacy tools
(`create_text_channel`, the category tools, the permission-overwrite tools, …) keep the
exact legacy param names; boolean/number params accept both native JSON values and their
string spellings.
| Tool | Key params | Requester permission |
| --- | --- | --- |
| create_text_channel | guildId?, name, categoryId?, topic?, nsfw?, slowmode?, position?, reason? | Manage Channels |
| edit_text_channel | guildId?, channelId, name?, topic?, nsfw?, slowmode?, categoryId?, position?, reason? | Manage Channels |
| delete_channel | guildId?, channelId, reason? | Manage Channels |
| find_channel | guildId?, channelName | — (read-only) |
| list_channels | guildId? | — (read-only) |
| get_channel_info | guildId?, channelId | — (read-only) |
| move_channel | guildId?, channelId, categoryId?, position?, reason? | Manage Channels |
| create_category | guildId?, name, reason? | Manage Channels |
| edit_category | guildId?, categoryId, name?, position?, reason? | Manage Channels |
| delete_category | guildId?, categoryId, reason? | Manage Channels |
| find_category | guildId?, categoryName | — (read-only) |
| list_channels_in_category | guildId?, categoryId | — (read-only) |
| list_channel_permission_overwrites | guildId?, channelId | — (read-only) |
| upsert_role_channel_permissions | guildId?, channelId, roleId, allowRaw?, denyRaw?, allowPermissions?, denyPermissions?, reason? | Manage Channels |
| upsert_member_channel_permissions | guildId?, channelId, userId, allowRaw?, denyRaw?, allowPermissions?, denyPermissions?, reason? | Manage Channels |
| delete_channel_permission_overwrite | guildId?, channelId, targetType, targetId, reason? | Manage Channels |
| create_channel | guildId?, name, type, topic?, nsfw?, slowmodeSeconds?, bitrate?, userLimit?, parentId?, position?, overwritesJson?, defaultAutoArchiveMinutes?, defaultReactionEmoji?, availableTagsJson?, defaultSortOrder?, defaultForumLayout?, defaultThreadSlowmode?, rtcRegion?, videoQualityMode?, reason? | Manage Channels |
| edit_channel | channelId, name?, topic?, nsfw?, slowmodeSeconds?, bitrate?, userLimit?, parentId?, position?, overwritesJson?, defaultAutoArchiveMinutes?, defaultReactionEmoji?, availableTagsJson?, defaultSortOrder?, defaultForumLayout?, defaultThreadSlowmode?, rtcRegion?, videoQualityMode?, lockPermissions?, reason? | Manage Channels |
| follow_announcement_channel | channelId, targetChannelId | Manage Webhooks (on the target channel) |
| list_channel_invites | channelId | — (read-only) |
### Voice Forums
Every guild-scoped tool also takes an optional `guildId` (defaults to `DISCORD_GUILD_ID`);
`edit_voice_channel` and the tag tools are channel-scoped and skip it. `reason` is an optional
audit-log parameter on every mutating tool.
## Voice & Stage
| Tool | Key params | Requester permission |
| --- | --- | --- |
| create_voice_channel | name, categoryId?, userLimit?, bitrate? | Manage Channels |
| create_stage_channel | name, categoryId?, bitrate? | Manage Channels |
| edit_voice_channel | channelId, name?, bitrate?, userLimit?, rtcRegion? (empty = automatic) | Manage Channels |
| move_member | userId, channelId (member must be in voice) | Move Members |
| disconnect_member | userId (member must be in voice) | Move Members |
| modify_voice_state | userId, mute?, deafen? (at least one required) | Mute Members, Deafen Members |
| set_voice_channel_status | channelId, status (up to 500 chars; empty clears) | Manage Channels (Any) |
| create_stage_instance | channelId, topic, privacyLevel? (1 public / 2 guild-only), notify? | Mute Members, Move Members |
| edit_stage_instance | channelId, topic?, privacyLevel? | Mute Members, Move Members |
| delete_stage_instance | channelId | Mute Members, Move Members |
## Forums
| Tool | Key params | Requester permission |
| --- | --- | --- |
| create_forum_channel | name, categoryId?, topic?, nsfw?, slowmode?, position? | Manage Channels |
| edit_forum_channel | channelId, name?, topic?, nsfw?, slowmode?, categoryId?, position?, defaultSort?, defaultLayout? | Manage Channels |
| list_forum_channels | — | — |
| get_forum_channel_info | channelId | — |
| list_forum_tags | channelId | — |
| create_forum_post | channelId, title, message/embeds (rich send), tagIds? | Send Messages |
| list_forum_posts | channelId | — |
| modify_forum_post | postId, locked?, archived?, pinned?, tagIds? (empty clears) | Manage Threads |
| create_forum_tag | channelId, name, emoji?, moderated? | Manage Threads |
| edit_forum_tag | channelId, tagId, name?, emoji?, moderated? | Manage Threads |
| delete_forum_tag | channelId, tagId | Manage Threads |
### Threads Members
Requester = the bot (permissions are checked server-side against the bot's roles).
Every ID is a string. `guildId` is optional everywhere (defaults to `DISCORD_GUILD_ID`).
Mutating tools take an optional `reason` for the audit log where the Discord API supports one.
## Threads
| Tool | Key params | Requester permission |
| --- | --- | --- |
| list_active_threads | guildId? | Read Message History |
| create_thread | channelId, name, messageId? or private?, autoArchiveMinutes?, invitable?, slowmodeSeconds? | Create Public Threads / Create Private Threads (private threads need Create Private Threads) |
| edit_thread | threadId, name?, archived?, locked?, autoArchiveMinutes?, slowmodeSeconds?, appliedTagIds?, reason? | Manage Threads |
| add_thread_member | threadId, userId | Manage Threads |
| remove_thread_member | threadId, userId | Manage Threads |
| list_thread_members | threadId | Read Message History |
| join_thread | threadId | — |
| leave_thread | threadId | — |
| list_archived_threads | channelId, private?, before?, limit? | Read Message History (private archived also needs Manage Threads) |
## Members & Moderation
| Tool | Key params | Requester permission |
| --- | --- | --- |
| kick_member | guildId?, userId, reason? | Kick Members |
| ban_member | guildId?, userId, deleteMessageSeconds?, reason? | Ban Members |
| unban_member | guildId?, userId, reason? | Ban Members |
| timeout_member | guildId?, userId, durationSeconds, reason? | Moderate Members |
| remove_timeout | guildId?, userId, reason? | Moderate Members |
| set_nickname | guildId?, userId, nick?, reason? | Manage Nicknames |
| get_bans | guildId?, limit? | Ban Members |
| get_member | guildId?, userId | — |
| search_members | guildId?, query, limit? | — |
| list_members | guildId?, limit?, after? | — (needs the privileged GuildMembers intent) |
| edit_member | guildId?, userId, nick?, roleIds?, mute?, deaf?, voiceChannelId?, timeoutUntil?, reason? | depends on fields — nick: Manage Nicknames; roleIds: Manage Roles; timeoutUntil: Moderate Members; mute/deaf/voiceChannelId: Mute Members / Deafen Members / Move Members |
| bulk_ban | guildId?, userIds, deleteMessageSeconds?, reason? | Ban Members |
| get_ban | guildId?, userId | Ban Members |
| prune_members | guildId?, days, includeRoleIds?, dryRun? | Kick Members (dry run changes nothing) |
| set_bot_nickname | guildId?, nick | Manage Nicknames (or Manage Guild — the bot's own nickname) |
### Roles Server
Requester permission = what the bot (the MCP caller) needs on the target server.
`guildId` is optional everywhere and defaults to `DISCORD_GUILD_ID`; `reason` is an optional
audit-log reason on every mutating tool where Discord supports one.
## Roles
| Tool | Key params | Requester permission |
|---|---|---|
| list_roles | guildId? | — |
| create_role | guildId?, name, color?, hoist?, mentionable?, permissions?, reason? | Manage Roles |
| edit_role | guildId?, roleId, name?, color?, hoist?, mentionable?, permissions?, iconUrl?, unicodeEmoji?, secondaryColor?, tertiaryColor?, reason? | Manage Roles |
| delete_role | guildId?, roleId, reason? | Manage Roles |
| assign_role | guildId?, userId, roleId, reason? | Manage Roles |
| remove_role | guildId?, userId, roleId, reason? | Manage Roles |
| reorder_roles | guildId?, positionsJson, reason? | Manage Roles |
| get_role_member_counts | guildId? | — |
| get_role | guildId?, roleId | — |
| list_role_members | guildId?, roleId, limit? | — (requires the GuildMembers intent) |
For every role mutation the bot's highest role must also sit above the target role
(Discord enforces role hierarchy); assigning or deleting @everyone/@managed roles is not possible.
## Server
| Tool | Key params | Requester permission |
|---|---|---|
| get_server_info | guildId? | — |
| edit_server | guildId?, name?, description?, iconUrl?, bannerUrl?, verificationLevel?, defaultNotifications?, explicitContentFilter?, afkChannelId?, afkTimeout?, systemChannelId?, systemChannelFlags?, rulesChannelId?, publicUpdatesChannelId?, preferredLocale?, reason? | Manage Guild |
| get_welcome_screen | guildId? | Manage Guild |
| edit_welcome_screen | guildId?, enabled?, description?, channelsJson?, reason? | Manage Guild |
| get_onboarding | guildId? | Manage Guild |
| edit_onboarding | guildId?, promptsJson?, defaultChannelIds?, enabled?, mode?, reason? | Manage Guild + Manage Roles |
| set_incident_actions | guildId?, invitesDisabledUntil?, dmsDisabledUntil? | Manage Guild |
| get_widget | guildId? | Manage Guild (settings; the widget.json data itself is public) |
| edit_widget | guildId?, enabled?, channelId?, reason? | Manage Guild |
| get_vanity_url | guildId? | Manage Guild |
| list_integrations | guildId? | Manage Guild |
| delete_integration | guildId?, integrationId, reason? | Manage Guild |
| list_voice_regions | (none) | — |
| list_guild_templates | guildId? | Manage Guild |
| create_guild_template | guildId?, name, description?, reason? | Manage Guild |
| sync_guild_template | guildId?, templateCode, reason? | Manage Guild |
| edit_guild_template | guildId?, templateCode, name?, description?, reason? | Manage Guild |
| delete_guild_template | guildId?, templateCode, reason? | Manage Guild |
| get_audit_log | guildId?, userId?, actionType?, before?, limit? | View Audit Log |
Notes:
- Welcome screen, onboarding, templates, and incident actions only work on community servers
(onboarding edits additionally need Manage Roles, not just Manage Guild).
- Icon/banner/role-icon images are fetched by the MCP server from `iconUrl`/`bannerUrl`/`iconUrl`
(SSRF-guarded, image content-types only, 256KB for guild images / 512KB for role icons) and are
uploaded to Discord as base64 data URIs; raw URLs are never passed through.
- `list_voice_regions` is not guild-scoped and takes no parameters.
### Automod Events Invites
Legend: `?` marks optional params. All IDs are strings.
## AutoMod rules
All AutoMod tools hit the raw REST API (`/guilds/{guildId}/auto-moderation/rules`) and require the bot to hold **Manage Guild**.
| Tool | Key params | Requester permission |
| --- | --- | --- |
| `list_automod_rules` | `guildId?` | Manage Guild |
| `get_automod_rule` | `guildId?`, `ruleId` | Manage Guild |
| `create_automod_rule` | `guildId?`, `name`, `triggerType` (1 keyword, 3 spam, 4 preset, 5 mention spam, 6 member profile), `actionsJson` (required, JSON array), `keywords?`, `regexPatterns?`, `allowList?`, `presets?` (triggerType 4), `mentionTotalLimit?` (triggerType 5), `mentionRaidProtectionEnabled?`, `enabled?` (default true), `exemptRoleIds?`, `exemptChannelIds?`, `reason?` | Manage Guild |
| `edit_automod_rule` | `guildId?`, `ruleId`, plus any of `name`, `triggerType`, `keywords`, `regexPatterns`, `allowList`, `presets`, `mentionTotalLimit`, `mentionRaidProtectionEnabled`, `actionsJson`, `enabled`, `exemptRoleIds` (`""` clears), `exemptChannelIds` (`""` clears), `reason?` | Manage Guild |
| `delete_automod_rule` | `guildId?`, `ruleId`, `reason?` | Manage Guild |
## Scheduled events
All event tools use raw REST (`/guilds/{guildId}/scheduled-events`) for full recurrence-rule support.
| Tool | Key params | Requester permission |
| --- | --- | --- |
| `create_guild_scheduled_event` | `guildId?`, `name`, `scheduledStartTime` (ISO 8601), `entityType` (1 stage / 2 voice → `channelId` required; 3 external → `location` + `scheduledEndTime` required), `description?`, `scheduledEndTime?`, `recurrenceRuleJson?`, `coverImageUrl?`, `reason?` | Manage Events |
| `edit_guild_scheduled_event` | `guildId?`, `eventId`, any of `status` (1 Scheduled, 2 Active/start, 3 Completed, 4 Canceled), `name`, `description`, `scheduledStartTime`, `scheduledEndTime`, `channelId`, `location`, `coverImageUrl`, `recurrenceRuleJson`, `reason?` | Manage Events |
| `delete_guild_scheduled_event` | `guildId?`, `eventId`, `reason?` | Manage Events |
| `list_guild_scheduled_events` | `guildId?`, `withUserCount?` (default true) | — |
| `get_guild_scheduled_event` | `eventId`, `guildId?` | — |
| `get_guild_scheduled_event_users` | `guildId?`, `eventId`, `limit?` (default 100, max 100), `withMember?` (default true) | — |
## Invites
| Tool | Key params | Requester permission |
| --- | --- | --- |
| `create_invite` | `guildId?`, `channelId`, `maxAge?` (seconds, 0 = never, default 86400), `maxUses?` (0 = unlimited), `temporary?`, `unique?`, `targetType?` (1 stream, 2 embedded application), `targetUserId?`, `reason?` | Create Instant Invite |
| `list_invites` | `guildId?` | Manage Guild |
| `delete_invite` | `inviteCode` (code or full URL), `reason?` | Manage Guild |
| `get_invite_details` | `inviteCode` (code or full URL), `withCounts?` (default true) | — |
### Webhooks Expressions
`guildId` is optional everywhere it appears (defaults to `DISCORD_GUILD_ID`). All IDs are strings.
Webhook URLs/tokens are send credentials: list/get tools never show them; only `create_webhook` returns the URL (once).
## Webhooks
| Tool | Key params | Requester permission |
| --- | --- | --- |
| create_webhook | channelId, name, avatarUrl?, reason? | Manage Webhooks |
| delete_webhook | webhookId, reason? | Manage Webhooks |
| list_webhooks | channelId | Manage Webhooks |
| send_webhook_message | webhookUrl, message, embedsJson?, componentsJson?, username?, avatarUrl?, threadId?, threadName?, filesJson?, allowedMentions? | Manage Webhooks |
| get_webhook | webhookId | Manage Webhooks |
| edit_webhook | webhookId, name?, avatarUrl?, channelId?, reason? | Manage Webhooks |
| list_guild_webhooks | guildId? | Manage Webhooks |
| get_webhook_message | webhookId, messageId, threadId? | Manage Webhooks |
| edit_webhook_message | webhookId, messageId, newMessage?, embedsJson?, componentsJson?, threadId? | Manage Webhooks |
| delete_webhook_message | webhookId, messageId, threadId? | Manage Webhooks |
Notes: `send_webhook_message` authenticates with the webhook URL itself (the token in the URL is the credential), but viewing/managing webhooks requires **Manage Webhooks**. `threadName` creates a forum post when the webhook targets a forum channel. `get/edit/delete_webhook_message` only work for webhooks the bot manages (they need the webhook token).
## Expressions
| Tool | Key params | Requester permission |
| --- | --- | --- |
| list_emojis | guildId? | — |
| get_emoji_details | guildId?, emojiId | — |
| create_emoji | guildId?, name, image? / imageUrl?, roles? | Manage Expressions |
| edit_emoji | guildId?, emojiId, name?, roles? | Manage Expressions |
| delete_emoji | guildId?, emojiId, reason? | Manage Expressions |
| list_app_emojis | — | — |
| create_app_emoji | name, image? / imageUrl? | — |
| delete_app_emoji | emojiId | — |
| list_guild_stickers | guildId? | — |
| get_guild_sticker | guildId?, stickerId | — |
| create_guild_sticker | guildId?, name, tags, fileUrl? / fileBase64?, description?, reason? | Manage Expressions |
| edit_guild_sticker | guildId?, stickerId, name?, description?, tags?, reason? | Manage Expressions |
| delete_guild_sticker | guildId?, stickerId, reason? | Manage Expressions |
| list_sticker_packs | — | — |
| list_default_sounds | — | — |
| list_guild_sounds | guildId? | — |
| get_guild_sound | guildId?, soundId | — |
| create_guild_sound | guildId?, name, fileUrl? / fileBase64?, volume?, emojiId? / emojiName?, reason? | Manage Expressions |
| edit_guild_sound | guildId?, soundId, name?, volume?, emojiId? / emojiName?, reason? | Manage Expressions |
| delete_guild_sound | guildId?, soundId, reason? | Manage Expressions |
Notes: pairs marked `a? / b?` are mutually exclusive — provide exactly one. Application emojis are owned by the bot's application (no server permission needed; the application owner controls them). Sticker files must be PNG/APNG/GIF (≤512KB, animated ≤5s) or Lottie JSON; soundboard sounds must be mp3/ogg (≤512KB, ≤5.2s). `edit_emoji` roles: empty string = unrestrict for everyone. `edit_guild_sound`: empty-string emojiId/emojiName clears the emoji.
### Interactions
Self-service role menus (handled automatically via `mcp:` custom ids — no state, survives restarts) and the interaction log.
| Tool | Key params | Requester permission |
| --- | --- | --- |
| `create_role_menu` | `channelId`, `roles` (JSON array `[{roleId, label, emoji?, description?}]`, 1–25 entries) or `roleIds` (comma-separated), `style` (`buttons` \| `select`), `title?`, `description?`, `embedColor?` | Manage Roles |
| `list_interactions` | `channelId?`, `sinceMinutes?` (1–1440, default 30) | — (read-only, `readOnlyHint` annotation) |
## Security
- The HTTP endpoint has **no authentication** and controls the bot — the
default bind is `127.0.0.1`. Keep it loopback or behind a trusted reverse
proxy / private network. The Docker image binds `0.0.0.0` (override with
`HOST`) — put the container on an internal network.
- Every URL a tool fetches (emoji, stickers, sounds, files, avatars, covers)
goes through one guarded fetcher: http/https only, DNS resolved and checked
against loopback/RFC1918/CGNAT/link-local/metadata/ULA ranges **before the
request and after every redirect**, 25 MiB and 15 s caps, per-use
content-type allowlists.
- Webhook tokens/URLs are send credentials: `create_webhook` returns the URL
once; `list_webhooks` / `get_webhook` / `list_guild_webhooks` never include
tokens or URLs.
- No server-side file reads: no tool accepts a filesystem path.
## Migrating from SaseQ/discord-mcp (Java)
1. Point your MCP client at the same address (`SERVER_ADDRESS`/`SERVER_PORT`
are still honored; new names are `HOST`/`PORT`).
2. Replace the `java -jar ... SPRING_PROFILES_ACTIVE=http` invocation with
`node dist/index.js` (or `MCP_TRANSPORT=stdio` for stdio setups — the
`SPRING_PROFILES_ACTIVE` logic behaves the same).
3. All 75 tool names and their parameters are unchanged; new parameters on
existing tools are all optional.
4. If you run smartbot: add the new tools to `permissions.py`
(`TOOL_PERMISSIONS` with the Perm column of the tables above,
`POSTING_TOOLS`, `MEMBER_TARGET_TOOLS`, `ROLE_TARGET_TOOLS`).
## Development
```bash
npm ci
npm run build # tsc -> dist/
npm test # vitest
npm run typecheck # tsc --noEmit
```
Tests cover the validators (embed/poll/component limits, V2 exclusivity), the
guarded fetcher (private IPs, redirects, size), message formatting, and a
compatibility gate that asserts all 75 legacy tools exist with their legacy
parameters (fixture generated from the Java sources). No live Discord token
needed.
`TESTING.md` is the manual checklist for a run in a real test guild.
### Docker
```bash
docker build -t discord-mcp .
docker run --rm -e DISCORD_TOKEN=... -e DISCORD_GUILD_ID=... -p 8085:8085 discord-mcp
```
Multi-stage build on `node:22-slim`, runs as the non-root `node` user.
## License
MIT
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues