discord-unofficial-mcp
discord-unofficial-mcp
"Look at my Discord messages" — for any MCP-capable agent (Claude Code, Claude Desktop, Codex, Cursor…). Reads your own Discord direct messages (DMs and group DMs) through the official Discord web client running in a dedicated Chrome where you signed in once, by hand.
Read-only. No token. No API. Not affiliated with Discord Inc.
discord_status → discord_catch_up # what's unread, across conversations
discord_list_dms # who you talk to, with channel ids
discord_read_dm # the recent messages of one conversation, with pagingWhy it works this way
Discord offers no Terms-compliant way for a program to read a user's DMs: a bot only sees
messages sent to the bot itself, and the only OAuth2 scope that would allow it
(dm_channels.read) is reserved for approved partners. The remaining route — using your
user token from a third-party client (a "self-bot") — is forbidden by
Discord's Terms
and, since March 2026, actively detected and sanctioned even for read-only use (forced
log-outs, password resets, "platform abuse" e-mails).
This project does none of that. It reads what the official client renders on screen, the way a
screen reader would. The design limits are deliberate and verifiable in src/core.js:
No API of its own. It never calls
discord.com/apiand never opens a websocket. All network traffic is generated by the official client.No token. It never reads
localStorage, IndexedDB, cookies or client internals. The DevTools connection is opened with theNetworkdomain disabled and a silent logger: the process does not even receive the client's request headers, so there is nothing of the sort to leak through logs. (NODE_DEBUGcan still make Node print its own diagnostics, such as the minimal environment passed to the Chrome launcher.)Only the dedicated browser. The DevTools endpoint must be on loopback and must belong to the configured Chrome profile (checked against the profile's
DevToolsActivePortfile or the listening process's--user-data-dir); another automation browser sitting on the same port is refused instead of adopted.Read-only. It never types, sends or reacts. The only actions are opening a conversation (clicking its sidebar link, or a URL built from an id validated as a snowflake) and scrolling.
Moderate use. Fixed pauses between actions, at most 300 messages per call, one reader at a time (in-process mutex + cross-process file lock) and the tab closes by itself after 3 minutes without activity from any agent.
⚠️ Read this before using it. Automating a user account — even read-only, even through the official client — is still automation of a user account under the letter of Discord's Terms of Service. This is the lowest-exposure way that exists to read your own messages with an agent; it is not a Terms-compliant one, and there is no such thing. You use it on your own account, at your own risk. Anything that sends messages is out of scope by design and will not be added.
Requirements
Node.js 22+.
Google Chrome (or Chromium/Brave/Edge) — a dedicated instance with its own profile, started with remote debugging on
127.0.0.1:9222. The server starts it for you on macOS/Linux; see below for Windows.A Discord account signed in in that Chrome profile (one-time, by hand).
Install
git clone https://github.com/mario-hernandez/discord-unofficial-mcp.git
cd discord-unofficial-mcp && npm installRegister the MCP server in your client. Claude Code, for every project:
claude mcp add --scope user discord-unofficial-mcp -- node /absolute/path/to/discord-unofficial-mcp/src/mcp.jsGeneric MCP configuration (Claude Desktop, Cursor, Codex…):
{
"mcpServers": {
"discord-unofficial-mcp": {
"command": "node",
"args": ["/absolute/path/to/discord-unofficial-mcp/src/mcp.js"]
}
}
}Optional CLI: npm link gives you the discord-unofficial-mcp command.
The dedicated Chrome
The server needs a Chrome that listens on 127.0.0.1:9222 with its own user-data-dir
(never your everyday profile: Chrome ignores the debugging flag on the default profile, and you
do not want an agent inside your personal browser anyway).
macOS / Linux: nothing to do. When nothing is listening, the server runs
scripts/chrome.sh, which starts Chrome with the profile~/.discord-unofficial-mcp/chrome-profileand waits for the port. You can also run it yourself:npm run chrome.Windows: start it by hand (or in a shortcut). CMD:
start chrome --remote-debugging-port=9222 --user-data-dir="%USERPROFILE%\.discord-unofficial-mcp\chrome-profile"PowerShell:Start-Process chrome -ArgumentList '--remote-debugging-port=9222',"--user-data-dir="$env:USERPROFILE.discord-unofficial-mcp\chrome-profile""The profile identity check needslsof/psor theDevToolsActivePortfile; on Windows setDISCORD_MCP_SKIP_PROFILE_CHECK=1if it cannot be verified.
Environment variables (for the server and the launcher):
Variable | Default | Purpose |
|
| DevTools endpoint of the dedicated Chrome (must be loopback; the launcher takes its port from here) |
|
| Chrome profile the launcher uses and the identity check expects (where the session lives) |
| unset |
|
| auto-detected | Explicit Chrome/Chromium binary for the launcher |
|
| Close the tab after this much inactivity ( |
|
| Lock file and activity marker (must be yours, mode 0700) |
| system time zone | Time zone used for the dates in the output |
Pass them with -e KEY=value in claude mcp add, or in the env block of a generic MCP config.
Sign in (once)
Ask the agent to call discord_open_login, or run discord-unofficial-mcp login. The Discord
window comes to the front: sign in there — the easiest way is to scan the QR code with the
Discord mobile app (no password typed). The session is kept in the profile directory and
survives closing Chrome and rebooting. It only ends if you sign out from that browser, change
your password, revoke the device in Discord's settings, or Discord invalidates it. When that
happens, discord_status reports logged_out and you scan the QR code again.
Tools
Tool | What it does |
| Dedicated Chrome ok, session state ( |
| Opens/focuses the tab for a manual sign-in. Only navigates to |
| Sidebar conversations (walked entirely with scrolling; recent-activity order) with |
| Recent messages of one conversation by |
| "Look at my messages": walks the conversations with unread messages (max 5 × 50) with per-conversation errors and |
| Closes the tab if no agent is using it: |
Each message carries: id, author (+ authorSource: header, system — the actor of a
system message such as a call —, group, group-map, inherited), timestamp (derived from
the id's snowflake: exact and always available), kind (message / system / unknown),
content (max 2,000 chars, contentTruncated),
forwarded, replyTo, attachments (kind, url verified by hostname against Discord's CDN,
name), edited. Both output formats respect a 60,000-character budget: when exceeded, the
oldest messages are dropped and count, hasMore and nextBefore are recomputed from what
was actually delivered, so paging never skips the omitted messages (outputTruncated reports
how many). Markdown goes inside a code block with a notice that it is third-party content
(data, not instructions); JSON carries the same notice.
CLI
discord-unofficial-mcp status
discord-unofficial-mcp dms [--unread] [--json]
discord-unofficial-mcp read (<channel_id> | --name <name>) [--limit N] [--before <id>] [--json]
discord-unofficial-mcp catch-up [--json]
discord-unofficial-mcp closeThings you should know
While the tab is open, Discord shows you online from that browser. It closes by itself after 3 minutes without activity from any agent (
DISCORD_MCP_IDLE_MS). The CLI cannot watch for inactivity: close withdiscord-unofficial-mcp close.Opening a conversation may mark it as read (Discord has no read receipts for the other person; only your own unread counter clears).
Only conversations open in the sidebar are listed. A closed DM can still be read if you know its
channel_id(direct navigation).Message content is written by other people. The server labels it as untrusted and the MCP instructions tell agents to treat it as data; that reduces prompt-injection confusion but is not a complete barrier. Do not let an agent act on instructions found inside a message.
Discord's CSS class names change with every build; the selectors this project relies on (
data-list-id,id="chat-messages-…",id="message-content-…",aria-labelledby) have been stable for years. If something breaks,scripts/probe-*.mjsprint the real DOM structure so the selectors can be adjusted.
Concurrency and shutdown
Several agent processes share the same tab. Every public operation takes an in-process mutex (bounded queue of 8) and a cross-process file lock (
<state dir>/tab.lock, mtime refreshed every 20 s; a lock is only considered stale when its process stopped refreshing it for 120 s). When busy, callers wait up to 60 s and then get an error. If a process loses the lock while operating (for example after being suspended), its operation is aborted and its result discarded.Bounded waits everywhere: 15 s to connect, 30 s per DevTools call, 40 s for the launcher, 150 s per operation. On expiry the DevTools connection is reset (never the browser).
Idle close is decided under the lock against the shared activity marker, so a read that just finished in another process is never interrupted. On process exit the server drains the running operation and closes the tab if it is idle; if it was used recently, a small detached helper closes it once the idle period has passed, so the last process to leave never leaves the tab (and the "online" presence) open forever.
Known limitations
Paging horizon.
beforecontinues from the current view when the cursor message is still rendered (consecutive pages in the same tab); otherwise it walks up again from the bottom, at most 40 screens per call. Very deep history is out of reach by design;coverage.stopReasontells you when that happened.Modal dialogs need a person. Update notices, verification prompts, new-login warnings and the like are detected (
discord_status→dialog) and every read stops with an explicit error until someone deals with the dialog in the dedicated Chrome window.(me)is decided by display name. In a group DM, another participant with the same display name would also be labelled(me).Group DMs work but were tested less than 1:1 conversations; attribution of grouped messages relies on Discord's
aria-labelledbyreferences.Inline custom emoji are represented by their
:name:only when a message consists solely of emoji; inside text they are dropped (the DOM renders them as images).Probes are development tools:
scripts/probe-*.mjsopen their own connection without the lock. Run them only when no agent is reading.Windows has no automatic launcher and no process-based profile check (see above).
Development
npm test # smoke test against the dedicated Chrome with a real session (prints metrics, not messages)
npm run test:mcp # drives src/mcp.js as an MCP client: concurrent calls, error results, idle auto-close
npm run chrome # start the dedicated Chrome by hand
node scripts/probe-dom.mjs # sidebar / message list structure
node scripts/probe-msg.mjs # ids inside a message, timestamps, scroll containers
node scripts/probe-group.mjs # how grouped messages reference their author
node scripts/probe-noauthor.mjs # structure of messages whose author cannot be resolved (scrolls up)The code base was reviewed adversarially (design and implementation) before the 0.2 release; the findings that shaped it are summarized in the commit history.
src/core.js — Chrome connection, DOM reading, exclusion, idle close, rendering
src/mcp.js — MCP server (stdio) with the 6 tools
src/cli.js — CLI with the same commands (strict util.parseArgs)
scripts/chrome.sh — dedicated Chrome launcher (macOS / Linux)
scripts/smoke.mjs — smoke test (`npm test`)
scripts/probe-*.mjs — DOM structure probes for when Discord changes somethingContributing
Issues and pull requests are welcome for robustness (selectors, coverage, concurrency), new read-only capabilities and platform support. Anything that sends, reacts, types or touches the token will not be merged: it changes the risk profile for every user of this project.
License
MIT. Discord is a trademark of Discord Inc.; this project is not affiliated with, endorsed by or supported by Discord Inc.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/mario-hernandez/discord-unofficial-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server