Discord MCP
by FabioCG01
README.md
# Discord MCP
[](https://github.com/FabioCG01/discordMCP/actions/workflows/ci.yml)
[](LICENSE)

A Discord bot that is also an [MCP](https://modelcontextprotocol.io) server. Connect Claude, Cursor, Codex or any other MCP client to it and your agent can build and run Discord servers: channels, roles, permissions, members, messages, threads, emojis, stickers, soundboard, webhooks, invites, events, AutoMod, slash commands and more.
- **160 tools** across 19 toolsets, generated reference in [docs/TOOLS.md](docs/TOOLS.md)
- **Everything a bot can do**: dedicated tools for the common things, plus a raw API tool for the rest
- **Whole servers in one call**: describe roles, categories, channels and permissions as a [blueprint](#blueprints) and apply it, idempotently, with a dry run
- **Permissions that make sense**: names instead of bitfields, merge-style overwrites, and a tool that explains *why* a member can or cannot do something
- **Designed, not just configured**: the bot renders its own header banners and dividers, so info channels look like someone cared
- **Several agents at once**: stdio for one client, Streamable HTTP for many
- **Guard rails**: read-only and safe modes, per-server allowlist, no accidental `@everyone`, token never leaves the process

> The server it was first used on was rebuilt by the bot itself: 10 categories, 45 channels, 9 roles, onboarding, AutoMod and the banners in its info channels, all from one blueprint: [examples/dev-studio.json](examples/dev-studio.json).
## Quick start
### 1. Create the bot
1. Open the [Discord Developer Portal](https://discord.com/developers/applications), create an application and add a **Bot**.
2. Copy the bot **token**.
3. Under **Bot → Privileged Gateway Intents**, switch on **Server Members Intent** and **Message Content Intent**. They are optional, but without them the bot cannot list all members or read what other people write.
### 2. Install
Nothing to install if you just want to use it: `npx -y github:FabioCG01/discordMCP` downloads, builds and runs the server (see the client configs below). To work from a checkout instead:
```bash
git clone https://github.com/FabioCG01/discordMCP.git
cd discordMCP
npm install # also builds
```
Give it the token in any one of these ways:
```bash
# a) environment variable
export DISCORD_TOKEN="your-bot-token"
# b) a file (never committed; credentials.txt and .env are in .gitignore)
echo "your-bot-token" > credentials.txt
# c) a file somewhere else
export DISCORD_TOKEN_FILE=/path/to/token.txt
```
Check that it works and get the invite link:
```bash
node dist/index.js doctor
```
### 3. Invite the bot
Open the invite URL printed by `doctor` (or `node dist/index.js invite`) and add the bot to your server. Administrator is the simplest; for a tighter setup ask your agent to call `discord_get_invite_url` with only the permissions you want.
**Then drag the bot's role near the top of the role list** (Server Settings → Roles). A bot can only edit or assign roles that sit below its own, no matter which permissions it has.
### 4. Connect your agent
All of these start the server over stdio. Replace the path with where you cloned the repo, or skip the checkout and use `"command": "npx", "args": ["-y", "github:FabioCG01/discordMCP"]` (on Windows: `"command": "cmd", "args": ["/c", "npx", "-y", "github:FabioCG01/discordMCP"]`).
<details open>
<summary><b>Claude Code</b></summary>
```bash
claude mcp add discord --scope user --env DISCORD_TOKEN_FILE=/path/to/discordMCP/credentials.txt -- node /path/to/discordMCP/dist/index.js
```
In Windows PowerShell, quote the separator: `'--'`.
</details>
<details>
<summary><b>Claude Desktop, Cursor, Windsurf, Cline, and other JSON-configured clients</b></summary>
Add this to the client's MCP configuration (`claude_desktop_config.json`, `~/.cursor/mcp.json`, …):
```json
{
"mcpServers": {
"discord": {
"command": "node",
"args": ["/path/to/discordMCP/dist/index.js"],
"env": { "DISCORD_TOKEN": "your-bot-token" }
}
}
}
```
Clients that limit the number of tools (Cursor warns above 40) should add `"--compact"` to `args`, or pick toolsets with `"DISCORD_MCP_TOOLSETS": "server,channels,roles,permissions,messages"`. See [Fewer tools](#fewer-tools).
</details>
<details>
<summary><b>VS Code (GitHub Copilot)</b></summary>
`.vscode/mcp.json`:
```json
{
"servers": {
"discord": {
"type": "stdio",
"command": "node",
"args": ["/path/to/discordMCP/dist/index.js"],
"env": { "DISCORD_TOKEN": "${input:discord-token}" }
}
},
"inputs": [{ "id": "discord-token", "type": "promptString", "description": "Discord bot token", "password": true }]
}
```
</details>
<details>
<summary><b>Codex CLI</b></summary>
`~/.codex/config.toml`:
```toml
[mcp_servers.discord]
command = "node"
args = ["/path/to/discordMCP/dist/index.js"]
env = { DISCORD_TOKEN = "your-bot-token" }
```
</details>
<details>
<summary><b>Gemini CLI</b></summary>
`~/.gemini/settings.json`:
```json
{
"mcpServers": {
"discord": { "command": "node", "args": ["/path/to/discordMCP/dist/index.js"], "env": { "DISCORD_TOKEN": "your-bot-token" } }
}
}
```
</details>
<details>
<summary><b>Any client over HTTP (several agents sharing one bot)</b></summary>
```bash
DISCORD_MCP_AUTH_TOKEN="choose-a-long-random-string" node dist/index.js --http --port 3333
```
Point clients at `http://127.0.0.1:3333/mcp` with the header `Authorization: Bearer <that string>`. See [Running as a service](#running-as-a-service).
</details>
Then just ask:
> *"List my Discord servers and give me an overview of the first one."*
> *"Set up this server for a study group. Show me the plan first."*
> *"Why can't @sam post in #announcements?"*
> *"Make #staff private to the Moderator role and lock #general for an hour."*
## What it can do
| Toolset | Tools | Covers |
| --- | ---: | --- |
| `bot` | 5 | Identity, health check, invite link, profile |
| `server` | 19 | Settings, one-call overview, audit log, onboarding, welcome screen, integrations, raid pause, pruning |
| `channels` | 10 | Create, edit, move, reorder, clone, delete; forums and tags; voice status |
| `permissions` | 10 | Overwrites (merge or replace), bulk changes, private/public, lock/unlock, effective-permission trace, security audit |
| `roles` | 9 | Create, edit, order, assign, bulk-assign, self-service role menus |
| `members` | 12 | Search, inspect, edit, time out, kick, ban, bulk ban |
| `messages` | 22 | Send (embeds, files, replies, polls, banners), read, edit, delete, purge by filter, pin, react, forward, search |
| `threads` | 9 | Threads, private threads, forum posts, archive and lock |
| `expressions` | 13 | Emojis (server and application), stickers, soundboard (upload, edit, play) |
| `webhooks` | 7 | Create and manage webhooks, post as a custom persona |
| `invites` | 4 | Create, inspect, revoke |
| `events` | 5 | Scheduled events |
| `automod` | 4 | AutoMod rules: keywords, regex, presets, mention spam |
| `voice` | 9 | Who is in voice, join/leave, stage channels |
| `templates` | 4 | Discord server templates |
| `commands` | 6 | Slash and context-menu commands; answering interactions |
| `gateway` | 6 | Presence, live event buffer, wait for an event |
| `blueprints` | 4 | Describe and build whole servers |
| `raw` | 2 | Any other REST endpoint |
The full list with every argument is in **[docs/TOOLS.md](docs/TOOLS.md)**. The server also exposes MCP **prompts** (`setup-server`, `security-audit`, `moderation-report`, `explain-access`) and **resources** (`discord://guide`, `discord://permissions`, `discord://blueprints/{id}`).
### Permissions by name
Wherever a tool takes permissions you write names, in any casing: `ViewChannel`, `SEND_MESSAGES`, `manage roles`. Groups expand to several: `@text_basic`, `@voice_basic`, `@member`, `@moderator`, `@manager`, `@send`, `@all`.
```jsonc
// discord_set_channel_permissions — merged into what is already there
{ "channel_id": "…", "target_id": "<role id>", "allow": ["ViewChannel", "@send"], "deny": ["MentionEveryone"] }
```
`discord_get_effective_permissions` replays Discord's algorithm and returns a trace:
```json
{
"subject": "role Developer",
"channel": "#swing-signals (announcement)",
"check": { "ViewChannel": true, "SendMessages": false },
"trace": [
"@everyone grants 21 permissions.",
"Role \"Developer\" adds: CreateInstantInvite, ManageThreads, …",
"Channel overwrite for @everyone denies: ViewChannel, SendMessages, …",
"Channel overwrites for roles [Developer] allows: ViewChannel."
]
}
```
### Blueprints
A blueprint is a JSON description of a server's structure. Everything is referenced by name, and applying it is idempotent: what exists is matched by name and updated, what is missing is created, and a second run does nothing.
```json
{
"everyone_permissions": ["@member"],
"roles": [
{ "name": "Admin", "color": "red", "hoist": true, "permissions": ["Administrator"] },
{ "name": "Moderator", "color": "green", "permissions": ["@moderator"] }
],
"categories": [
{
"name": "📌 Info",
"read_only": true,
"writable_by": ["Moderator"],
"channels": [
{ "name": "rules", "messages": [{ "content": "Be kind.", "pin": true }] },
{ "name": "announcements", "type": "announcement" }
]
},
{
"name": "💬 Community",
"channels": [
{ "name": "general" },
{ "name": "help", "type": "forum", "tags": ["Question", "Solved"], "require_tag": true },
{ "name": "Lounge", "type": "voice" }
]
},
{ "name": "🔒 Staff", "private_to": ["Admin", "Moderator"], "channels": [{ "name": "staff-chat" }, { "name": "mod-log" }] }
],
"server": { "community": true, "rules_channel": "rules", "public_updates_channel": "mod-log" }
}
```
- `private_to: [roles]` hides a channel or category from `@everyone`; `read_only: true` lets everyone read but only `writable_by` post. Channels inherit their category's rules and can add their own with `permissions`.
- `discord_apply_server_blueprint` with `dry_run: true` lists every action without doing anything.
- `remove_unlisted_channels` / `remove_unlisted_roles` turn it into a full reset; they need `confirm_delete: true`.
- `was: ["old name"]` on a role, category or channel renames the existing one in place, keeping its ID, history and settings. That is how a layout evolves without losing anything.
- `copy_from_guild_id` clones the structure of another server the bot is in.
- `discord_export_server_blueprint` captures an existing server, as a backup or a starting point.
- Also in a blueprint: forum tags and seed posts, server settings, `member_roles`, and Community `onboarding` questions that hand out roles and channels.
Built-in presets (`discord_list_blueprints`): `minimal`, `community`, `gaming`, `developer-hub`, `study-group`. A larger real-world example is [examples/dev-studio.json](examples/dev-studio.json).
### Banners and dividers
`discord_send_banner` draws a header image and posts it: a dark panel with a bold title, an optional label, subtitle and decorative motif, and a glow in the accent colour you choose. `style: "divider"` gives a thin gradient line to separate sections. Everything is rendered inside the server (SVG rasterised with WebAssembly, fonts bundled), so it needs no image files and looks the same on every machine.
```jsonc
{ "channel_id": "…", "label": "My Server", "title": "Welcome", "subtitle": "Read this first", "motif": "01", "accent": "#6366F1" }
```
The pattern that makes an info channel look finished is banner, then text, then divider:
1. `discord_send_banner` for the heading
2. `discord_send_message` with an embed for the content
3. `discord_send_banner` with `style: "divider"` before the next section
Pick one accent colour per topic and reuse it for that topic's role, banner and embeds. `preview: true` returns the image without posting, so an agent that can see images can check its work. Blueprint seed messages accept a `banner` too, and `discord_pin_message` takes `quiet: true` to remove the "pinned a message" notice.
### Live features (gateway)
Most tools use Discord's REST API and need no connection. A few need the gateway, which connects on first use in stdio mode and at startup in HTTP mode:
- **Events**: `discord_get_events` and `discord_wait_for_event` let an agent react to new messages, reactions, joins, voice changes and more.
- **Slash commands and buttons**: register a command with `discord_create_command`; when someone uses it the bot acknowledges it within Discord's 3-second window, and the agent answers with `discord_respond_to_interaction` any time in the next 15 minutes.
- **Role menus**: `discord_create_role_menu` posts buttons that give or remove roles. The server handles the clicks by itself, and refuses roles that carry moderation or management permissions.
- **Voice and soundboard**: join a voice channel and play soundboard sounds. The bot does not transmit audio streams.
- **Presence**: `discord_set_presence`.
## Safety
An agent with a bot token can do real damage, so the defaults are conservative and everything else is opt-in.
| Control | How | Effect |
| --- | --- | --- |
| Mode | `DISCORD_MCP_MODE=full \| safe \| readonly` | `safe` removes every destructive tool (delete, kick, ban, prune, raw writes). `readonly` leaves only tools that read, and the Discord client itself refuses anything but GET. |
| Toolsets | `DISCORD_MCP_TOOLSETS=roles,channels` | Only those groups are exposed. `DISCORD_MCP_DISABLE_TOOLS` hides single tools. |
| Server allowlist | `DISCORD_MCP_GUILDS=id,id` | Requests touching any other server are refused before they reach Discord. |
| Mentions | default | `@everyone`, `@here` and role mentions render but do not ping unless the agent passes `allow_mentions`. |
| Confirmation | built in | Pruning, bulk bans, leaving a server and blueprint deletions require an explicit `confirm` argument. |
| Local files | `DISCORD_MCP_FILE_ROOTS=/dir` | Off by default. Uploads from disk are limited to those folders; URLs that resolve to private or loopback addresses are refused. |
| Tool annotations | automatic | Every tool declares `readOnlyHint` / `destructiveHint`, so clients can ask before running the dangerous ones. |
| Token | automatic | Read from env or file, never returned by a tool, and redacted from all output and logs. |
| Audit log | `reason` argument | Mutating tools accept a reason that appears in Discord's audit log. `DISCORD_MCP_DEFAULT_REASON` sets one for every action. |
Two things no setting can fix, so keep them in mind:
- **Prompt injection.** Messages, usernames and channel topics are written by other people. The server tells agents to treat that text as data, but a model can still be talked into things. Use `safe` mode or a narrow toolset when your agent reads untrusted channels.
- **Blast radius.** Give the bot the permissions the job needs. Administrator is convenient; it also means a leaked token is a lost server.
Found a vulnerability? See [SECURITY.md](SECURITY.md).
## Fewer tools
160 tool schemas are a lot of context, and some clients cap the count.
- **Compact mode** (`--compact` or `DISCORD_MCP_COMPACT=1`) exposes three tools instead: `discord_search_tools`, `discord_describe_tool` and `discord_run_tool`. The agent looks up what it needs on demand; nothing is lost.
- **Toolsets** (`--toolsets server,channels,roles,permissions`) expose only what a task needs.
## Running as a service
HTTP mode is one long-lived process that any number of agents connect to. The bot stays online, keeps its event buffer, and role menus keep working.
```bash
DISCORD_TOKEN=… DISCORD_MCP_AUTH_TOKEN=… node dist/index.js --http --host 0.0.0.0 --port 3333
```
- Endpoint: `POST/GET/DELETE /mcp` (Streamable HTTP, one session per client). Health check: `GET /health`.
- It binds to `127.0.0.1` by default and refuses to listen on any other address without `DISCORD_MCP_AUTH_TOKEN`.
- Put it behind HTTPS (a reverse proxy or tunnel) if it leaves your machine.
With Docker:
```bash
docker build -t discord-mcp .
docker run -d --name discord-mcp -p 3333:3333 \
-e DISCORD_TOKEN=… -e DISCORD_MCP_AUTH_TOKEN=… discord-mcp
```
## Configuration
Every option is an environment variable; the common ones also have a flag. A `.env` file in the working directory is loaded automatically ([.env.example](.env.example)).
| Variable | Flag | Default | Meaning |
| --- | --- | --- | --- |
| `DISCORD_TOKEN` | | | Bot token (`DISCORD_BOT_TOKEN` also works) |
| `DISCORD_TOKEN_FILE` | `--token-file` | `./credentials.txt` | File holding the token |
| `DISCORD_MCP_MODE` | `--mode` | `full` | `full`, `safe` or `readonly` |
| `DISCORD_MCP_TOOLSETS` | `--toolsets` | all | Comma-separated toolsets |
| `DISCORD_MCP_DISABLE_TOOLS` | `--disable-tools` | | Comma-separated tool names to hide |
| `DISCORD_MCP_COMPACT` | `--compact` | off | Three meta-tools instead of all tools |
| `DISCORD_MCP_GUILDS` | `--guilds` | any | Server IDs the bot may touch |
| `DISCORD_MCP_TRANSPORT` | `--http` / `--stdio` | `stdio` | Transport |
| `DISCORD_MCP_HOST` | `--host` | `127.0.0.1` | HTTP bind address |
| `DISCORD_MCP_PORT` | `--port` | `3333` | HTTP port |
| `DISCORD_MCP_AUTH_TOKEN` | `--auth-token` | | Bearer token required from HTTP clients |
| `DISCORD_MCP_ALLOWED_ORIGINS` | | localhost only | Browser origins allowed to call the HTTP endpoint |
| `DISCORD_MCP_GATEWAY` | `--gateway` | `lazy` (stdio), `on` (http) | `on`, `lazy` or `off` |
| `DISCORD_MCP_INTENTS` | `--intents` | auto | Gateway intents; auto = all non-privileged plus the privileged ones enabled in the portal |
| `DISCORD_MCP_INTERACTIONS` | | `public` | How slash commands are acknowledged: `public`, `ephemeral` or `off` |
| `DISCORD_MCP_EVENT_BUFFER` | | `1000` | Events kept in memory |
| `DISCORD_MCP_FILE_ROOTS` | `--file-roots` | none | Folders local files may be uploaded from |
| `DISCORD_MCP_ALLOW_PRIVATE_URLS` | | `false` | Allow fetching files from private or loopback addresses |
| `DISCORD_MCP_DEFAULT_REASON` | | | Audit-log reason used when a call gives none |
| `DISCORD_MCP_MAX_OUTPUT_CHARS` | | `60000` | Tool results longer than this are trimmed |
| `DISCORD_MCP_MAX_RATE_LIMIT_WAIT` | | `60` | Seconds to wait out a rate limit before reporting it |
| `DISCORD_MCP_CACHE_SECONDS` | | `10` | How long channel/role lists are reused |
| `DISCORD_MCP_LOG_LEVEL` | `--log-level` | `info` | `debug`, `info`, `warn`, `error`, `silent` (logs go to stderr) |
Commands: `discord-mcp` (serve), `doctor`, `invite`, `tools [--json]`, `--help`, `--version`.
## What a bot cannot do
These are limits of Discord, not of this project:
- **Create servers.** Discord removed that for bots. A person creates the server and invites the bot; the bot can then build everything inside it.
- **Join a server on its own**, or accept an invite. Someone with Manage Server has to add it.
- **Manage roles above its own.** Not even with Administrator. Move the bot's role up.
- **Read message text without the Message Content intent**, or list all members without the Server Members intent. `discord_health_check` tells you which are on.
- **Answer a slash command faster than an agent can think.** That is why interactions are acknowledged automatically and answered afterwards.
- **Stream audio** into voice channels. Joining and soundboard sounds work; music does not.
- **Act as a user.** No DMs to strangers, no friend requests, no reading servers it is not in.
## Development
```bash
npm run build # compile to dist/
npm test # unit tests (no network)
npm run docs # regenerate docs/TOOLS.md
npm run dev # run from source
# end-to-end against a real server; creates and removes its own "mcp-selftest" sandbox
DISCORD_TEST_GUILD_ID=<server id> npm run test:live
# call a single tool through the real MCP protocol
npm run call -- discord_list_servers
npm run call -- discord_get_server_overview '{"guild_id":"123456789012345678"}'
```
Layout: `src/discord/` (REST client, gateway, permission engine, file handling), `src/tools/` (one file per area; each tool is a small declarative object), `src/server.ts` (MCP wiring), `src/http.ts` (HTTP transport), `blueprints/` (presets).
Adding a tool is about fifteen lines: a name, a description, a zod schema and a `run` function. See [CONTRIBUTING.md](CONTRIBUTING.md).
## License
[MIT](LICENSE). The fonts in `assets/fonts` (Inter and JetBrains Mono) are under the SIL Open Font License; their licence files sit next to them. Not affiliated with Discord or Anthropic.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues