Skip to main content
Glama
fmarcac

discord-mcp

by fmarcac
README.md
# discord-mcp

Read and act on Discord through your own running desktop client: channel
messages, direct messages, group chats, threads, forum posts, and search.

## How it works

Discord's desktop app is Electron. Started with `--remote-debugging-port`, its
renderer is reachable over the Chrome DevTools Protocol. discord-mcp attaches to
that renderer, observes the header set the client already sends on its own API
calls, and issues requests from inside the same page.

Every request therefore comes from the real client process, on the real session,
with the real fingerprint. There is no second client to detect, and the token is
never read out of Discord's storage: it is only ever observed on a request the
client was already making.

## Read this before using it

**This violates Discord's Terms of Service.** Automating a user account is
prohibited regardless of how the session was obtained, and section 11 separately
prohibits modifying the client.

The practical risk sits in the same bracket as client mods such as Vencord,
which Discord has publicly deprioritized and which have no confirmed bans as of
2026. That is a pattern of non-enforcement, not a promise. Risk concentrates in
writing, not reading.

A bot token would be fully compliant, but a bot can never read your direct
messages. That limitation is the entire reason this project exists.

## Setup

```sh
npm install
npm run build
```

Discord holds a single-instance lock, so the debugging flag only takes effect at
startup. Quit any running client first.

```sh
node dist/cli/main.js launch      # starts Discord on a random loopback port
node dist/cli/main.js doctor      # verifies attach, capture and a real read
```

`doctor` should print your account, guild count and direct message count.

The first attach reloads the renderer once, because an idle client makes no REST
requests to observe headers from. The capture is then remembered per port for
twelve hours in `~/.local/state/discord-mcp/headers.json`, so later runs attach
in about a second and leave your client alone.

### Wiring it to an MCP client

```json
{
  "mcpServers": {
    "discord": {
      "command": "node",
      "args": ["/path/to/discord-mcp/dist/cli/main.js", "serve"]
    }
  }
}
```

## Tools

Eighteen tools are exposed by default: twelve read and six write.

| Tool | Tier | Purpose |
| --- | --- | --- |
| `guild_list` | read | Guilds this account belongs to |
| `guild_channels` | read | Channels of one guild |
| `guild_roles` | read | Roles defined in a guild |
| `channel_get` | read | Describe a channel or thread |
| `channel_history` | read | A page of messages, paging in both directions |
| `channel_pins` | read | Pinned messages |
| `message_get` | read | One message by id |
| `dm_list` | read | Direct and group conversations |
| `thread_list` | read | Open threads of a channel, which is how forum posts are read |
| `thread_archived` | read | Archived threads, where quiet forum posts end up |
| `search_guild` | read | Search one guild |
| `search_channel` | read | Search one channel or conversation |
| `message_send` | write | Send a message or reply |
| `message_edit` | write | Edit a message this account sent |
| `message_react` | write | Add a reaction |
| `message_ack` | write | Mark a channel read up to a message |
| `thread_create` | write | Start a thread, optionally from an existing message |
| `forum_post` | write | Create a forum post with its opening message |

Reading a forum works by listing a forum channel's threads. Each post comes back
with the message that opened it and its applied tags, so a post is readable
without a second call.

### Destructive tools

Seven more operations exist and are **not registered at all** unless their
category is enabled. They do not appear in the tool list, so no host and no
model can reach them.

| Tool | Category |
| --- | --- |
| `message_delete` | `messages` |
| `thread_delete` | `threads` |
| `channel_delete` | `channels` |
| `guild_leave` | `guilds` |
| `member_kick`, `member_ban` | `members` |
| `member_roles` | `roles` |

Enable only what you need:

```json
{ "destructive": ["messages", "threads"] }
```

```sh
node dist/cli/main.js serve --config /path/to/policy.json
```

This is the load-bearing control. Host approval prompts are a second layer,
because host behaviour varies and some hosts approve automatically. Each
destructive tool resolves and returns what it acted on, so the result names the
channel rather than only echoing a snowflake.

## Security

An open DevTools port is a local authentication bypass: any process running as
you can drive the session, two-factor included. discord-mcp binds to loopback
only and picks a random high port rather than the default 9222. Do not leave the
client running with the port open when you are not using it.

**The header cache holds a live token.** Reusing a capture means keeping it, so
`~/.local/state/discord-mcp/headers.json` contains your `Authorization` header
until it expires. It is written owner-only, mode `0600`, and so is the launch
state beside it. Delete the file to force a fresh capture:

```sh
rm ~/.local/state/discord-mcp/headers.json
```

Run without a cache path if you would rather capture every time and never store
the token, at the cost of a renderer reload on each attach.

## Layout

```
src/attach/     CDP connection, target selection, launch, state
src/bridge/     header capture, request execution, rate limiting
src/api/        typed wrappers over Discord REST v9
src/normalize/  reduce Discord's wire format to what a reader needs
src/policy/     operation tiers and the destructive gate
src/tools/      MCP tool registration
src/cli/        launch, doctor, serve
```

Dependencies run downward only. `normalize/` and `policy/` are pure and depend
on nothing, which is where most of the test suite lives.

```sh
npm test         # 152 tests, no Discord required
npm run typecheck
```

The design document, including the Spike 0 findings that shaped the bridge, is
in `docs/superpowers/specs/`.