discord-dm-mcp
by 0xShaito
README.md
# discord-dm-mcp
A tiny **MCP server** that lets Claude send **Discord direct messages** through a bot —
**with nothing to host.**
Sending a DM is just two stateless HTTPS POSTs to Discord's REST API, so there is no
gateway/websocket connection to keep alive and no server to run. This package is a
**stdio MCP server**: it runs as a subprocess of whatever launches it — your local
Claude client, or a Claude **cloud routine** via a committed `.mcp.json`. Each user
brings their **own** Discord bot token; the package itself holds no secrets.
It exposes a single tool, **`send_discord_dm`**.
---
## How "no hosting" works
| Where you use it | Where the server runs | You host a machine? |
| --- | --- | --- |
| Local Claude Code / Desktop / Cursor | your laptop (a subprocess) | No |
| Claude **cloud routine** (via `.mcp.json`) | Anthropic's ephemeral sandbox | No |
The only path that *would* require hosting is a remote ("Connector chip") MCP server,
because a remote MCP must be reachable at a URL. This package avoids that by using
stdio. (See [the trade-off](#why-stdio-and-not-a-remote-connector).)
---
## Prerequisites (one-time Discord setup)
1. **Create a bot.** <https://discord.com/developers/applications> → New Application →
**Bot** → **Reset Token** → copy the token. This is your `DISCORD_BOT_TOKEN`.
- You do **not** need any privileged intents (Message Content, etc.) — those gate
the gateway, not REST sends. This bot never reads messages.
2. **Invite the bot to your server** with the `bot` scope and **zero** permissions
(DMs aren't governed by a permission bit):
```
https://discord.com/oauth2/authorize?client_id=YOUR_APP_ID&scope=bot&permissions=0
```
3. **The bot must share that server with every recipient.** Discord only delivers a DM
if the bot and the user have a **mutual guild**. Otherwise the send fails with error
`50007` ("Cannot send messages to this user"). The recipient must also not have DMs
disabled / the bot blocked.
4. **Get recipient user IDs.** In Discord: Settings → Advanced → **Developer Mode** on,
then right-click a user → **Copy User ID**. (Or build a consented
[GitHub→Discord mapping](#mapping-github-logins-to-discord-ids).)
---
## Install — local Claude clients
No repo, no GitHub needed — just npm:
```bash
claude mcp add discord-dm \
--env DISCORD_BOT_TOKEN="your-bot-token" \
-- npx -y discord-dm-mcp
```
For Claude Desktop / Cursor, add the equivalent to the client's MCP config:
```json
{
"mcpServers": {
"discord-dm": {
"command": "npx",
"args": ["-y", "discord-dm-mcp"],
"env": { "DISCORD_BOT_TOKEN": "your-bot-token" }
}
}
}
```
---
## Install — Claude cloud routine
A cloud routine can't use a *local* stdio server you added with `claude mcp add`
(that lives on your machine). Instead the routine **clones a repo and reads
`.mcp.json` from it**. So:
1. **Point the routine at a repo** that contains a `.mcp.json` (the one at the root of
this repo works as-is — it `npx`-installs the published package):
```json
{
"mcpServers": {
"discord-dm": {
"command": "npx",
"args": ["-y", "discord-dm-mcp"],
"env": { "DISCORD_BOT_TOKEN": "${DISCORD_BOT_TOKEN}" }
}
}
}
```
The repo can be a near-empty **stub** — its only job is to carry this file. The
`${DISCORD_BOT_TOKEN}` is expanded from the routine's environment variable.
2. **Set the env var.** In the routine's environment ("Default" or a custom one) add an
**Environment variable** `DISCORD_BOT_TOKEN=...`.
> ⚠️ Claude routines have **no dedicated secrets store yet** — env vars are visible
> to anyone who can edit the environment and may appear in transcripts. Treat this
> token as low-confidentiality and scope the bot to least privilege (it needs no
> guild permissions). Rotate it if a transcript is shared.
3. **Allow Discord egress.** The Default environment's "Trusted" network access
**blocks `discord.com`** (requests fail with `403 host_not_allowed`). Edit the
environment's **Network access** to **Full**, or **Custom** with `discord.com`
added to the allowlist. (Custom-allowlist propagation has had bugs; if Discord
still 403s, use Full.)
4. Reference the tool from the routine **Instructions**, e.g. *"If you're unsure whether
to merge, call `send_discord_dm` to ask the PR author."*
**Running from a checkout instead of npm** (e.g. before publishing): copy
[`examples/.mcp.local-repo.json`](examples/.mcp.local-repo.json) to the repo root as
`.mcp.json` and set the environment **Setup script** to `npm ci && npm run build`.
---
## Tool: `send_discord_dm`
| Field | Type | Notes |
| --- | --- | --- |
| `discordUserId` | string | Recipient's numeric Discord ID. **One of** this or `githubLogin` is required. |
| `githubLogin` | string | Resolved to a Discord ID via [`DISCORD_USER_MAP`](#mapping-github-logins-to-discord-ids). |
| `content` | string | Plain message text, sent as-is. |
| `escalation` | object | Structured decision request (formatted into the body). Fields below. |
| `escalation.prUrl` | string | Link to the PR / item. |
| `escalation.uncertainty` | string | What the agent is unsure about. |
| `escalation.evidence` | string | Relevant evidence / context. |
| `escalation.options` | string[] | Rendered as `A) … B) …` plus a `Custom)` option. |
| `escalation.recommendation` | string | The agent's recommended option. |
| `dryRun` | boolean | Resolve + format but never call Discord. |
Provide `content`, `escalation`, or both. Mentions are **always** disabled
(`allowed_mentions: { parse: [] }`), so messages never accidentally ping `@everyone`,
roles, or users.
**Errors are returned as tool errors (`isError: true`), not thrown:** unknown recipient,
missing token, the no-mutual-guild `50007`/`50278` case (not retried), and other Discord
API failures. Rate-limit `429`s are retried automatically honoring `Retry-After`.
---
## Configuration (environment variables)
| Var | Required | Purpose |
| --- | --- | --- |
| `DISCORD_BOT_TOKEN` | to send | Bot token. Not needed for dry runs. |
| `DISCORD_DRY_RUN` | no | `1`/`true`/`yes` forces dry-run globally. |
| `DISCORD_USER_MAP` | no | Inline JSON or path to a `{githubLogin: discordUserId}` file. |
| `DISCORD_API_BASE` | no | Override `https://discord.com/api/v10`. |
### Mapping GitHub logins to Discord IDs
Set `DISCORD_USER_MAP` to inline JSON (`{"octocat":"123..."}`) or a file path (see
[`examples/mapping.example.json`](examples/mapping.example.json)). Keys match the GitHub
login case-insensitively. For a **consented** mapping, have users link their own GitHub
via Discord OAuth2 (`identify` + `connections` scopes) and only DM logins present in the
map.
---
## Develop / verify
```bash
npm install
npm run build # tsc -> dist/
npm test # unit tests (pure functions)
npm run smoke # builds, starts the server over stdio, calls the tool in dry-run
```
`npm run smoke` proves the full MCP round-trip with no token and no network.
---
## Why stdio and not a remote connector?
A **custom connector** (the chip in Claude's "Connectors" UI) is a **remote** MCP server:
Claude connects out to a URL, so it must be hosted (even serverless = a deployment). A
**stdio** server runs as a child process of the client, so it needs no host. The trade:
- **stdio (this package):** no hosting; works locally and in cloud routines via
`.mcp.json`. Not shown as a Connector chip.
- **remote connector:** shown as a chip and reusable across clients, but you must host
(serverless like Cloudflare Workers is the lightest option).
---
## Responsible use
Discord's Developer Policy requires **consent before initiating processes on a user's
behalf** and prohibits unsolicited / bulk DMs. Use this for **opted-in, transactional,
one-at-a-time** notifications (e.g. asking a PR author a question), never to broadcast.
Using a **user token / selfbot** to DM is forbidden by Discord and can get the account
terminated — this package uses a proper **bot** token only.
## License
MIT
TDQS
A4.2/5.0
Scored across 1 tool
Disambiguation5/5
Only one tool exists, so there is no ambiguity. An agent cannot confuse it with another tool.
Naming Consistency5/5
The single tool name follows a clear verb_noun pattern (send_discord_dm), consistent with common MCP conventions.
Tool Count3/5
A single tool for sending DMs is minimal but arguably appropriate if the server's sole purpose is to send DMs. However, it feels thin and could benefit from at least one more tool (e.g., to verify user availability).
Completeness2/5
The server only supports sending messages, lacking any way to check user opt-in status, retrieve conversation history, or handle errors like 50007. This leaves significant gaps for an agent.
Maintenance
ActivityInactive
ResponsivenessNo issues