Discord MCP Server
This server exposes a fixed catalog of Discord actions centered on messaging, reactions, attachments, channels/DMs, friends, and presence.
Send, edit, and delete messages, including replies, TTS, and up to 10 file attachments.
Add or remove reactions on messages.
Read message history, search messages with filters, fetch a single message, and list attachments.
Download Discord attachments and clean up downloaded files.
Get channel details, list DM channels, and create or retrieve a DM with a user.
List friends and send or remove friend requests.
Update or clear presence and activity/status.
Provides comprehensive Discord bot management capabilities including server information, channel creation and management, message operations, role and permission management, member moderation, voice channel operations, webhook management, and audit logging through Discord's API.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@Discord MCP Server create a new text channel named announcements in the general category"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
Discord for language models, through one tool that runs code
The model discovers the live discord.js API, writes its own helpers, and keeps them across calls and restarts, instead of picking from a fixed menu of verbs.
Quick start · The run tool · Secrets · Other MCP servers · Configuration · Security
▶ Watch the teaser with sound
The one tool this server exposes runs JavaScript with the server's full authority: the network, the filesystem, the process. That is the design, not a mode, and there is no switch to turn it off. Run the server sandboxed (a container, VM or jail with only the access you intend to grant), and keep Discord tokens out of it with discord-mcp agent: then the code can use Discord but can never read a token.
Why one code tool
A fixed catalog of typed tools is the wrong interface for Discord. Every bot, permission set and library version supports a different slice of the API, and different models are good with different tools. So this server doesn't hand-pick verbs. It exposes a single tool, run, gives the code a live discord.js client, and lets each deployment's model build the tooling it needs.
// call 1: the model writes a helper...
async function channelByName(guildId, name) {
const guild = await client.guilds.fetch(guildId);
return (await guild.channels.fetch()).find((c) => c.name === name);
}
// call 2 (or next week, after a restart): ...and reuses it
const general = await channelByName("123456789012345678", "general");
await general.send(vars.announcement);Highlights
One tool, the whole API.
clientis a real discord.jsClient.describe(x)shows live state and every method's signature and doc, so the model can find its way.It remembers. Functions, classes and constants the model defines are saved and reloaded on restart. Side effects never are.
Tokens stay out of reach. An ssh-agent-style agent holds the tokens, so the server and the model only ever see
agent:<id>.Other MCP servers become functions.
mcp.github.get_issue(...)inside the same code, not a growing list of tools.Hardened networking. Optional proxy for all Discord traffic, https-only upstreams with
known_hostspinning, per-identity latency, and tokens kept out of logs and traces.
Related MCP server: discord-mcp
How it works
flowchart LR
M["Model<br>(any MCP client)"] -- "run(code)" --> S
subgraph S["discord-mcp server"]
direction TB
N["Persistent JS namespace<br>client · describe · ops · kept helpers"]
U["mcp.server.tool()"]
end
N -- "agent:id placeholder<br>(Unix socket)" --> A["discord-mcp agent<br>holds the decrypted tokens"]
A -- "real token<br>REST + gateway" --> D[("Discord")]
U --> O["Other MCP servers<br>https or stdio"]Quick start
Prerequisites: Node.js 22.15+, npm, and a Discord bot token.
git clone https://github.com/drvova/discord-mcp.git
cd discord-mcp
npm install # also builds dist/1. Store the token, encrypted, like an SSH key. You're prompted (hidden) for the token and a passphrase.
node dist/bootstrap.js identity add default-bot2. Start the agent in its own terminal, like ssh-agent. It asks for the passphrase and prints the socket to use.
node dist/bootstrap.js agent
# DISCORD_MCP_AGENT_SOCK=/run/user/1000/discord-mcp/agent.sock; export DISCORD_MCP_AGENT_SOCK;3. Point your MCP client at the server, with only the agent socket in its environment:
{
"mcpServers": {
"discord": {
"command": "node",
"args": ["/absolute/path/to/discord-mcp/dist/bootstrap.js"],
"env": { "DISCORD_MCP_AGENT_SOCK": "/run/user/1000/discord-mcp/agent.sock" }
}
}
}Then ask the model to call run with empty code: it gets the manual and takes it from there.
Skip the agent and give the server the token directly:
DISCORD_TOKEN=your_bot_token node dist/bootstrap.jsThis works, but the token then lives in the server process, where code in the run tool can read it (for example through the client's auth config). The server warns about this at startup. Secret environment variables are removed from process.env once read, in both modes.
Command | What it does |
| stdio server (the default MCP transport) |
| HTTP + SSE server on port 1455 (see HTTP mode) |
| rebuild on change and restart, with pretty logs |
| containerised server; mount a volume at |
The run tool
run is the only tool the server advertises.
Argument | Type | |
| string, required | JavaScript to run. Empty returns the manual. |
| string | Identity to bind as |
| object of strings | Named strings available as |
The code runs in one long-lived namespace with top-level await. The result is whatever it printed plus the value of its last expression.
What's in the namespace
Name | What it is |
| The bound discord.js |
| List and switch identities; |
|
|
| Live capabilities (for a client: ready, user, intents, guilds, REST latency) and every member's signature with a one-line doc from discord.js's JSDoc |
| The loaded Discord packages, their symbols and exports, and the discord.js module |
| Structured operations with dry runs and preflight checks, as plain functions |
| Other MCP servers as functions |
| Proxy and TLS policy, latency per identity and server, gateway ping |
| Saved definitions |
| I/O and utilities |
Examples
Each block is the code of one run call.
// What can this bot do right now?
describe(client)// Search what discord.js exports
(await symbols("TextChannel#send")).items.map((s) => s.name)// Check a call before making it
await ops.preflight({
packageAlias: "discordjs",
symbol: "TextChannel#send",
kind: "function",
target: "channel",
context: { channelId: "123456789012345678" },
})// Switch to another identity the agent holds
await bind("ops-bot");
client.user.tagHelpers survive restarts
After each successful call, every top-level definition (a function or class declaration, or a const bound to a literal, function or class) is saved as its own file and loaded back when the server starts. Each one loads in isolation, so a broken file is reported by kept() and never stops the server. Statements that do work (calls, let, const x = await …) are never saved or replayed.
Definitions live in DISCORD_MCP_KEPT_DIR (default ~/.config/discord-mcp/kept). In a container, mount it as a volume.
Secrets: the SSH model
Discord tokens are handled the way SSH handles private keys.
SSH | discord-mcp |
|
|
|
|
|
|
the agent signs; the key never leaves it | the agent puts the token on Discord requests; the server only holds |
"UNPROTECTED PRIVATE KEY FILE!" | secret files readable by others are refused, with the |
|
|
REST. The server's discord.js client logs in as
agent:default-botand sends its REST requests to the agent over the Unix socket. The agent swaps the placeholderAuthorizationheader for the real token and forwards the request tohttps://discord.com/api, always to that base, whatever path the client sends.Gateway. The gateway URL Discord returns is replaced with an agent address. The agent relays the WebSocket and swaps the token inside the
IDENTIFYandRESUMEframes. It only connects to gateway URLs Discord itself returned, which clients see as opaque handles, and only onwss://*.discord.gg.The result. Code in
runcan use Discord (client,ops.*, its own helpers) but can't read a token. It isn't inclient.token, the auth config,process.env, the encrypted file, or the agent's API.connect()is disabled with the agent, so tokens never pass through the model.Adding identities.
identity addencrypts into the file and, whenDISCORD_MCP_AGENT_SOCKis set, also loads the identity into the running agent, likessh-add; thenbind("id").Input. Tokens and passphrases come from a hidden prompt, a mode 600 file (
--token-file,DISCORD_MCP_PASSPHRASE_FILE) or stdin, never from command-line arguments (visible inps).--token-env DISCORD_TOKENmigrates an existing variable.Unattended agents (containers, services): set
DISCORD_MCP_PASSPHRASE_FILEto a mode 600 file. For a Docker secret, give itmode: 0400.Proxy. With
DISCORD_MCP_PROXYset, the agent sends REST and the gateway through it.
Threat model. Like ssh-agent, this protects tokens from code running in the server, not from other processes running as the same OS user with debugging rights. Start the agent separately (not as the server's child). For a hard boundary, run it as a different OS user or in its own container, sharing only the socket. Anything that can reach the socket can use the identities, exactly like SSH agent forwarding.
Other MCP servers as functions
Other MCP servers aren't re-advertised as tools. Their tools become async functions in the namespace (the "code mode" pattern), so the model composes several calls in one run, filters results in code, and prints only what it needs:
// GitHub issues mentioned in the last 50 messages of a channel
const channel = await client.channels.fetch("123456789012345678");
const refs = [...(await channel.messages.fetch({ limit: 50 })).values()]
.flatMap((m) => [...m.content.matchAll(/#(\d+)/g)].map((x) => Number(x[1])));
for (const n of new Set(refs)) {
const issue = await mcp.github.get_issue({ owner: "acme", repo: "app", issue_number: n });
print(`#${n} ${issue.state} ${issue.title}`);
}Results: a call resolves to the tool's structured content, or its text (parsed when it's JSON). A tool error is thrown.
Discovery: tool names become identifiers (
get-issue→get_issue).describe(mcp.github)lists each tool's signature, generated from its input schema.Configuration: servers in
DISCORD_MCP_UPSTREAMS(inline JSON or a file, in the usualmcpServersshape) connect at startup.${VAR}is read from the environment, so secrets stay out of the file:{ "mcpServers": { "github": { "url": "https://api.githubcopilot.com/mcp/", "headers": { "Authorization": "Bearer ${GITHUB_TOKEN}" } }, "files": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "/data"] } } }Transports:
urlservers use streamable HTTP with SSE fallback (force one with"type": "http"or"sse").commandservers use stdio. An unreachable server shows up inmcpServers()and doesn't stop startup.At runtime:
mcpConnect(name, config)adds a server until the next restart. Helpers that wrapmcp.*calls are saved like any other definition.
Network
Proxy |
|
Not proxied | In direct mode, the Discord gateway WebSocket ( |
Encrypted upstreams | Upstream MCP URLs must be |
known_hosts | Upstream servers and an https proxy are pinned by the SHA-256 of their TLS public key on first use. A changed key is refused ("REMOTE HOST IDENTIFICATION HAS CHANGED"); clear it with |
Latency | Discord REST is timed per identity and upstream calls per server (count, errors, last, avg, p50, p95), shown in |
No tokens in telemetry | Latency is labelled by identity or server name, never by token. Trace spans have Discord webhook and interaction tokens removed from paths ( |
Configuration
Environment variables, all optional. A .env file in the working directory is loaded too.
Variable | |
| The agent socket. Recommended: then the server needs no token at all |
| Direct mode only |
| Default guild |
| Comma-separated gateway intents (privileged ones must be enabled in the Developer Portal) |
| Where |
| Override those two paths individually |
| Mode 600 file with the identities passphrase, for unattended agents |
| Legacy direct-mode store for |
Variable | |
| Saved definitions (default |
| Other MCP servers: inline JSON or a path, |
|
|
|
|
|
|
| Append every |
Variable | |
|
|
|
|
| Colors on/off; |
| OpenTelemetry (on by default; |
| Where traces go (OTLP over HTTP) |
| Prometheus metrics (default |
| Serve over HTTP + SSE instead of stdio |
| OAuth bot-install flow in HTTP mode |
HTTP mode
With MCP_HTTP_PORT set, the server speaks MCP over HTTP and SSE:
Endpoint | |
| MCP JSON-RPC: |
| MCP over SSE |
| Health check |
| OAuth bot install |
A typed client is generated from the route types:
import { createHttpClient } from "./src/http-client.js";
const client = createHttpClient("http://localhost:1455");
const health = await (await client.health.$get()).json();Security
Sandbox the server.
runexecutes code with the server's full authority. Give the container, VM or jail only the network, filesystem and identities you intend the model to use.Use the agent. It keeps tokens out of the server process. In direct mode,
runcode can read them.Least privilege on Discord. Give bots only the permissions they need, and rotate tokens.
Audit. Every
runcall is written to the audit log; setDISCORD_MCP_AUDIT_LOG_PATHin production.Know what the policy covers.
DISCORD_MCP_BLOCK_HIGH_RISK,dryRunandallowWriteapply toops.*only. Code that usesclientdirectly is bounded by the sandbox and the bot's Discord permissions.
Development
The toolchain is Rstack: Rslib builds, Rstest tests, Rslint lints and type-checks.
Script | |
| Build |
| Build, then rebuild and restart on change, with pretty logs |
| Build, then run the Rstest suite (some tests spawn the built server) |
| Rslint: lint, type-check (TypeScript's native compiler), or both |
The animated banner and the teaser trailer are authored with Clayzo in assets/banner, its own small project; the trailer's soundtrack is synthesised by a Python script there, and its player page lives in site.
The default branch is new-architecture-main.
License
Available Tools
19 toolsdiscord_add_friendB
Send a friend request to a user
Inputs: no named parameters.
discord_add_reactionC
Add a reaction to a message
Inputs: channelId (required), messageId (required), emoji (required).
discord_cleanup_downloadC
Clean up a previously downloaded Discord attachment file
Inputs: path (required).
discord_clear_presenceB
Clear your Discord presence/activity
Inputs: no named parameters.
discord_create_dmC
Create or get existing DM channel with a user
Inputs: userId (required).
discord_delete_messageC
Delete a message
Inputs: channelId (required), messageId (required).
discord_download_attachmentB
Download a Discord attachment and return it as MCP file reference
Inputs: url (required).
discord_edit_messageC
Edit a previously sent message
Inputs: channelId (required), messageId (required), content (required).
discord_get_channelB
Get information about a Discord channel
Inputs: channelId (required).
discord_get_dm_channelsB
Get list of DM channels for the current user
Inputs: no named parameters.
discord_get_friendsB
Get list of friends for the current user
Inputs: no named parameters.
discord_get_messageB
Get a specific message by ID with full attachment details
Inputs: channelId (required), messageId (required).
discord_get_message_historyC
Get message history from a Discord channel with optional filtering
Inputs: channelId (required), limit, before, after, +1 more.
discord_list_attachmentsC
List all attachments in a Discord channel from recent messages
Inputs: channelId (required), limit, imageOnly, minSize, +1 more.
discord_remove_friendC
Remove a friend or cancel a friend request
Inputs: userId (required).
discord_remove_reactionC
Remove a reaction from a message
Inputs: channelId (required), messageId (required), emoji (required), userId.
discord_search_messagesC
Search for messages in a Discord channel with text query and filters
Inputs: channelId (required), query (required), limit, authorId, +5 more.
discord_send_messageC
Send a message to a Discord channel, optionally with up to 10 files
Inputs: channelId (required), content, files, tts, +1 more.
discord_update_presenceB
Update your Discord presence/status
Inputs: status, activity, afk.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 19 tools
Each tool has a clearly distinct purpose with no overlap; for example, discord_get_message retrieves a single message, discord_get_message_history fetches historical messages, and discord_search_messages searches with queries, making misselection unlikely. The tools cover different actions (e.g., add/remove friend, send/edit/delete message) without ambiguity.
All tool names follow a consistent pattern: 'discord_' prefix followed by a verb_noun structure (e.g., discord_send_message, discord_get_channel). There are no deviations in style or convention, making the naming predictable and easy to parse.
With 19 tools, the count is slightly high but reasonable for a Discord server covering messaging, channels, friends, and presence. It includes core operations without being excessive, though it might feel heavy compared to simpler servers. The tools are well-scoped for the domain.
The tool set provides complete coverage for Discord interactions, including CRUD operations for messages (send, edit, delete, get), friends (add, remove, list), channels (get, DM management), and presence (update, clear). There are no obvious gaps, and agents can handle typical workflows without dead ends.
Maintenance
Related MCP Connectors
Official Duck Framework MCP server for AI-assisted development and project scaffolding.
Official MCP server for OmniDimension. Drive voice agents, dispatch calls, and run bulk campaigns.
A MCP server built for developers enabling Git based project management with project and personal…
MCP server for Gainium — manage trading bots, deals, and balances via AI assistants
Related MCP Servers
- AlicenseBqualityDmaintenanceEnables Discord bot integration through MCP, allowing users to manage servers, channels, roles, members, and messages including moderation actions like bans, timeouts, and bulk deletions through natural language.33MIT
- AlicenseBqualityDmaintenanceEnables MCP clients to interact with Discord servers, allowing operations such as sending messages and reading message history through the Discord API.23190 npmMIT
- AlicenseBqualityDmaintenanceEnables Discord server moderation actions including kick, ban, timeout, role management, channel operations, audit logs, and member management through MCP tools.297MIT
- FlicenseBqualityDmaintenanceEnables Discord server administration through MCP, providing tools for role management, member moderation, channel operations, and message purging.17-