Skip to main content
Glama
Discord MCP Server

Discord MCP Server

by drvova

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.

Node MCP discord.js Toolchain License

Quick start · The run tool · Secrets · Other MCP servers · Configuration · Security

▶ Watch the teaser with sound

WARNING

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. client is a real discord.js Client. 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_hosts pinning, 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-bot

2. 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.js

This 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

npm start

stdio server (the default MCP transport)

npm run web

HTTP + SSE server on port 1455 (see HTTP mode)

npm run dev

rebuild on change and restart, with pretty logs

docker compose up

containerised server; mount a volume at /data to keep the model's helpers

The run tool

run is the only tool the server advertises.

Argument

Type

code

string, required

JavaScript to run. Empty returns the manual.

identity

string

Identity to bind as client before the code runs, e.g. default-bot

vars

object of strings

Named strings available as vars.<name>, for long text or payloads you'd rather not escape inside code

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

client

The bound discord.js Client; drive its full API directly

identities(), bind(id), connect(id, { mode, token })

List and switch identities; connect only in direct mode

identity

{ id, mode } of the bound identity

describe(x)

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

packages(), symbols(query), pkg(alias), discord

The loaded Discord packages, their symbols and exports, and the discord.js module

ops.packages / symbols / preflight / refresh / invoke / batch

Structured operations with dry runs and preflight checks, as plain functions

mcp.<server>.<tool>(args), mcpServers(), mcpConnect(), mcpDisconnect()

Other MCP servers as functions

network()

Proxy and TLS policy, latency per identity and server, gateway ping

kept(), kept(name), forget(name)

Saved definitions

vars, print, console, fetch, require, sleep, man()

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.tag

Helpers 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

~/.ssh/id_*, mode 600, passphrase-protected

~/.config/discord-mcp/identities, mode 600, AES-256-GCM under a scrypt key from your passphrase

ssh-add · ssh-add -l · ssh-keygen -p

identity add ID · identity list · identity passwd · identity remove ID

ssh-agent + SSH_AUTH_SOCK

discord-mcp agent + DISCORD_MCP_AGENT_SOCK

the agent signs; the key never leaves it

the agent puts the token on Discord requests; the server only holds agent:<id>

"UNPROTECTED PRIVATE KEY FILE!"

secret files readable by others are refused, with the chmod that fixes them

known_hosts, StrictHostKeyChecking

known_hosts pinning for upstream servers and an https proxy

  • REST. The server's discord.js client logs in as agent:default-bot and sends its REST requests to the agent over the Unix socket. The agent swaps the placeholder Authorization header for the real token and forwards the request to https://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 IDENTIFY and RESUME frames. It only connects to gateway URLs Discord itself returned, which clients see as opaque handles, and only on wss://*.discord.gg.

  • The result. Code in run can use Discord (client, ops.*, its own helpers) but can't read a token. It isn't in client.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 add encrypts into the file and, when DISCORD_MCP_AGENT_SOCK is set, also loads the identity into the running agent, like ssh-add; then bind("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 in ps). --token-env DISCORD_TOKEN migrates an existing variable.

  • Unattended agents (containers, services): set DISCORD_MCP_PASSPHRASE_FILE to a mode 600 file. For a Docker secret, give it mode: 0400.

  • Proxy. With DISCORD_MCP_PROXY set, the agent sends REST and the gateway through it.

NOTE

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 usual mcpServers shape) 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: url servers use streamable HTTP with SSE fallback (force one with "type": "http" or "sse"). command servers use stdio. An unreachable server shows up in mcpServers() and doesn't stop startup.

  • At runtime: mcpConnect(name, config) adds a server until the next restart. Helpers that wrap mcp.* calls are saved like any other definition.

Network

Proxy

DISCORD_MCP_PROXY (http:// or https://) carries Discord REST, upstream MCP over HTTP and fetch in run. With the agent, the Discord gateway too. Requests are tunnelled with CONNECT, so TLS stays end-to-end. An invalid or SOCKS URL stops startup rather than silently connecting directly.

Not proxied

In direct mode, the Discord gateway WebSocket (@discordjs/ws has no proxy option; it is always wss://). Stdio upstream servers, which are local processes.

Encrypted upstreams

Upstream MCP URLs must be https://, except to localhost. With NODE_TLS_REJECT_UNAUTHORIZED=0, https upstreams are refused.

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 discord-mcp known-hosts remove HOST:PORT. DISCORD_MCP_STRICT_HOST_KEY_CHECKING=yes refuses unknown hosts. Discord isn't pinned: its certificates rotate often.

Latency

Discord REST is timed per identity and upstream calls per server (count, errors, last, avg, p50, p95), shown in network(), mcpServers() and describe(client), and exported as discord_mcp_network_latency_ms.

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 (/webhooks/123/:token) and query strings stripped. Proxy URLs are shown without credentials.

Configuration

Environment variables, all optional. A .env file in the working directory is loaded too.

Variable

DISCORD_MCP_AGENT_SOCK

The agent socket. Recommended: then the server needs no token at all

DISCORD_TOKEN / DISCORD_USER_TOKEN

Direct mode only

DISCORD_GUILD_ID

Default guild

DISCORD_INTENTS

Comma-separated gateway intents (privileged ones must be enabled in the Developer Portal)

DISCORD_MCP_CONFIG_DIR

Where identities and known_hosts live (default ~/.config/discord-mcp)

DISCORD_MCP_IDENTITIES_FILE, DISCORD_MCP_KNOWN_HOSTS

Override those two paths individually

DISCORD_MCP_PASSPHRASE_FILE

Mode 600 file with the identities passphrase, for unattended agents

DISCORD_MCP_MASTER_KEY, DISCORD_MCP_IDENTITY_STORE_PATH

Legacy direct-mode store for connect()

Variable

DISCORD_MCP_KEPT_DIR

Saved definitions (default ~/.config/discord-mcp/kept)

DISCORD_MCP_UPSTREAMS

Other MCP servers: inline JSON or a path, mcpServers shape

DISCORD_MCP_PROXY

http:// or https:// proxy; credentials allowed in the URL

DISCORD_MCP_STRICT_HOST_KEY_CHECKING

accept-new (default) or yes

DISCORD_MCP_BLOCK_HIGH_RISK

true blocks high-risk ops.* calls

DISCORD_MCP_AUDIT_LOG_PATH

Append every run call to this file

Variable

LOG_LEVEL

error, warn, info (default), debug

LOG_STYLE

pretty or json (default: pretty in a terminal or dev, json otherwise). Logs always go to stderr, so stdout stays clean for the stdio protocol

LOG_COLOR, ENABLE_LOGGING

Colors on/off; false disables logging

OTEL_ENABLED, OTEL_SERVICE_NAME

OpenTelemetry (on by default; false turns it off) and the service name

OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, OTEL_EXPORTER_OTLP_HEADERS

Where traces go (OTLP over HTTP)

OTEL_PROMETHEUS_PORT, OTEL_PROMETHEUS_ENDPOINT

Prometheus metrics (default :9464/metrics)

MCP_HTTP_PORT (or PORT)

Serve over HTTP + SSE instead of stdio

DISCORD_CLIENT_ID, DISCORD_CLIENT_SECRET, DISCORD_OAUTH_REDIRECT_URI

OAuth bot-install flow in HTTP mode

HTTP mode

With MCP_HTTP_PORT set, the server speaks MCP over HTTP and SSE:

Endpoint

POST /

MCP JSON-RPC: initialize, tools/list, tools/call

GET /sse, POST /message

MCP over SSE

GET /health

Health check

GET /oauth/discord/start, GET /oauth/discord/callback

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. run executes 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, run code can read them.

  • Least privilege on Discord. Give bots only the permissions they need, and rotate tokens.

  • Audit. Every run call is written to the audit log; set DISCORD_MCP_AUDIT_LOG_PATH in production.

  • Know what the policy covers. DISCORD_MCP_BLOCK_HIGH_RISK, dryRun and allowWrite apply to ops.* only. Code that uses client directly 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

npm run build

Build src/ to dist/ (bundleless ESM, one file per module)

npm run dev

Build, then rebuild and restart on change, with pretty logs

npm test

Build, then run the Rstest suite (some tests spawn the built server)

npm run lint · npm run typecheck · npm run check

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

MIT

Available Tools

19 tools
discord_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

    B3.4/5.0

    Scored across 19 tools

    Disambiguation5/5

    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.

    Naming Consistency5/5

    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.

    Tool Count4/5

    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.

    Completeness5/5

    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

    ActivityMaintained
    ResponsivenessNo recent community issues

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      B
      quality
      D
      maintenance
      Enables 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.
      33
      MIT
    • A
      license
      B
      quality
      D
      maintenance
      Enables MCP clients to interact with Discord servers, allowing operations such as sending messages and reading message history through the Discord API.
      23
      190 npm
      MIT
    • A
      license
      B
      quality
      D
      maintenance
      Enables Discord server moderation actions including kick, ban, timeout, role management, channel operations, audit logs, and member management through MCP tools.
      29
      7
      MIT