dmine-mcp
Archives and mines Discord channels through a logged-in browser session, providing tools to capture/backfill channels, search archived messages, export channels to markdown or JSONL, list known servers and channels, check per-channel watermarks, and manage archived media attachments.
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., "@dmine-mcp search my archive for dinner plans"
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.
dmine
Bot-free Discord channel archiver & data miner. It drives a normal logged-in browser (Playwright + system Chrome) and reads whatever the account can see, exactly like a human scrolling. No bot token, no API key. Output: one shared SQLite archive plus the downloaded media files.
Unofficial and unaffiliated. This project is not affiliated with, endorsed by, or supported by Discord Inc. "Discord" is a trademark of Discord Inc. and is used here only to describe what the tool interacts with.
Read this before using it. dmine automates a user account. That is not what Discord's Terms of Service and API rules permit for bots — the supported path for programmatic access is the official API with a bot token. Automated account use can get an account limited or terminated, and the web client's internal behaviour can change at any time. Archive only content you are authorised to access, and respect the privacy of the people whose messages you are collecting. You are responsible for how you use this.
What it is not
Not a bot: it registers nothing with Discord's developer platform.
Not an API client: it makes no REST or gateway calls; it reads the rendered DOM.
Not a way to reach other people's private servers: it sees exactly what the logged-in account can already see in the client.
Related MCP server: tg-recall-mcp
Install
Requires Python 3.10+ and Google Chrome or Chromium installed on the system.
dmine drives your browser and starts it as a long-running background
daemon, so a single login survives across runs (that is what the persistent
profile and the --remote-debugging-port daemon are for). Chrome/Chromium is
looked up in the usual install locations and on PATH.
If none is found it falls back to Playwright's bundled Chromium
(playwright install chromium), which works but does not give you the
long-lived daemon — the browser then starts and stops with each command.
Installed from source — there is no package on PyPI:
git clone https://github.com/hschaefer/dmine.git && cd dmine
python3 -m venv .venv && .venv/bin/pip install -e .That gives you two entry points: the dmine CLI and dmine-mcp,
the MCP server (see Use as an MCP server).
One-time setup
dmine login
# prints a QR code (also written to --qr-out <path> if given)
# scan it with the Discord mobile app (Settings -> Scan QR code)
# the session token is injected into the browser profile automaticallyAlternatively, log in manually:
dmine browser start
# a browser window opens -> log into Discord once (2FA fine)The login persists in ~/.config/dmine/profile (never re-asked).
QR-code login (remote auth)
dmine login implements Discord's remote auth flow (the mechanism the
desktop app uses): it performs an RSA-OAEP key exchange with Discord's gateway,
renders https://discord.com/ra/<fingerprint> as a QR code, and — once you
scan it with the mobile app and confirm — exchanges the resulting ticket for a
regular session token, which is injected into the browser profile. No bot
token, no password, no API key.
This is an internal, undocumented flow. At the time of writing the web client ships the QR-login component internally but exposes no reachable UI toggle for it, so this library speaks to the remote-auth gateway directly. Treat it as best-effort: it is not covered by Discord's public API contract and can break without notice.
The flow is also available as a library:
from dmine.auth import RemoteAuth
from dmine.browser import DiscordBrowser
auth = RemoteAuth()
qr_url, png = auth.start() # handshake + QR code (PNG bytes)
token, user = auth.wait_for_token() # blocks until scanned+confirmed
browser = DiscordBrowser().start()
browser.set_session(token, user) # inject into the browser profile
browser.stop()set_session writes the session via CDP directly into the browser profile's
storage backend (JSON-encoded exactly as Discord Web's storage wrapper
expects). It deliberately avoids Discord's obfuscated localStorage hiding —
no unstable internal property names are used, so injection survives client
updates as long as Discord keeps the JSON-encoding storage convention.
Usage
dmine capture <channel-id-or-url> --full # one-time backfill
dmine capture <channel-id-or-url> # incremental since watermark
dmine search "keyword" [--channel <id>] [--server <id-or-name>]
dmine export <channel-id> --format md|jsonl [--out file]
dmine status [--channel <id>] # watermark per channel
dmine channels # known channelssearch is a case-insensitive substring match over message content, embeds and
author names — not a tokenised/ranked full-text index, and the searched columns
are unindexed, so a hit is found by scanning the archive. Fine for a personal
archive; expect it to get slower as the DB grows.
Use as an MCP server
dmine is also an MCP server (stdio
transport), so any MCP-capable client — Claude Desktop, Cursor, OpenCode,
Hermes, … — can use the archive as structured tools instead of shelling out to
the CLI. The MCP SDK is a normal dependency; there is nothing extra to install.
Register it by absolute path: MCP clients do not inherit your shell's
PATH or virtualenv.
{
"mcpServers": {
"dmine": {
"command": "/absolute/path/to/dmine/.venv/bin/dmine-mcp"
}
}
}python -m dmine.mcp_server is equivalent if you prefer to point the
client at an interpreter. DMINE_* variables (see below) can be passed
through the client's env block.
The tools fall into two groups:
Tool | Needs the browser session | What it does |
| no | most recent archived messages of a channel |
| no | substring search over content, embeds and author |
| no | total messages, per-channel watermark and counts |
| no | known servers (id, name, channel and message counts) |
| no | known channels, optionally of one server |
| no | archived attachments and embed images for a channel |
| no | export a channel to jsonl/markdown, sandboxed to |
| yes | archive a channel (incremental or full backfill) |
| yes | re-download media for already archived messages |
The read tools work against the SQLite archive alone and are safe to call at
any time. capture and remedia drive a real browser, so they need the
one-time setup above first; they are serialized by a lock and can run for a
long time, so they return a running job summary instead of blocking past a
short client timeout.
Architecture
dmine/— Python package (CLI + capture + storage). Harness-agnostic; the CLI is the shared core.dmine/mcp_server.py— thin MCP adapter (python -m dmine.mcp_server, stdio) so any MCP-capable client can call the same core as structured tools.archive.sqlite— single source of truth (messages,channels,servers). Message IDs are snowflakes →MAX(id)per channel is the incremental watermark.~/.config/dmine/media/<channel>/— downloaded attachments (CDN links expire, so media is captured at capture time).
Safety & limits
DOM-only; no network interception, no tokens, no API calls.
Scrolls with human-ish speed/jitter; stop conditions are convergence-based.
Only archives channels the account can actually view.
Threat model: the Chrome daemon exposes an unauthenticated CDP endpoint on 127.0.0.1 — any local process could attach and drive the logged-in Discord session. Keep the daemon profile on a trusted machine. See SECURITY.md.
Concurrency: all browser-driving operations take a non-blocking lock (
~/.config/dmine/capture.lock, PID-verified, stale locks are reclaimed). DB reads (search/status/recent/…) are never locked.The archive holds other people's messages. Treat it as personal data: don't commit it, don't redistribute it, don't attach exports or channel screenshots to issues.
.gitignorealready excludes the archive, the browser profile, media and QR codes.
Environment variables
Var | Default |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Tests
The suite is offline — no browser, no network, no Discord account, no existing archive. It uses synthetic IDs only.
.venv/bin/python -m pytest -qLicense
AGPL-3.0-or-later — see LICENSE. Running a modified version as a network service means section 13 requires you to offer its source to your users.
Contributing note
The archive path can be overridden with DMINE_DB; the package is imported as dmine.
Available Tools
9 toolscaptureA
Archive a Discord channel into the SQLite archive.
Incremental by default: only messages newer than the stored watermark (MAX message id per channel) are fetched. Use full=True for a one-time backfill. Requires the persistent Chrome daemon (auto-starts; the user must have logged into Discord in its profile at least once).
Long jobs run in the background: the call waits up to wait seconds
(default 45) and returns the full result if the capture finished in
time. If it is still running, it returns {"status": "running", ...}
immediately and the capture continues — call capture() again for the
same channel to fetch the final result, or watch progress via
status()/recent(). Never fire captures for several channels in
parallel: they queue on the global capture lock and serialize
automatically (capture() reports waiting_for_lock while queued).
Args: channel: channel ID or full discord.com/channels/... URL. full: backfill the entire history instead of incrementing. since: explicit watermark message ID (overrides the stored one). limit: hard cap on messages (test runs). no_media: skip attachment + embed-image downloads (CDN links expire!). refresh: re-extract ALL visible messages, retrofitting embed images onto rows captured before image extraction existed (combine with full=True for the whole channel). wait: seconds to wait synchronously for completion before returning a "running" summary (keep below your client's request timeout (many clients cap it at ~60 s). Returns: Dict with new_messages, seen, stop_reason, total_in_db — or a {"status": "running", "job": {...}} summary for long backfills.
Inputs: channel (required), full, wait, limit, +3 more.
channelsA
List channels known to the archive (optionally of one server).
Args: server: optional server ID or name to restrict to. Returns: Dict: {"channels": [ {id, server_id, server_name, name, last_message_id}, ... ]}.
Inputs: server.
exportA
Export a channel from the archive to a file (jsonl or markdown).
The output path is sandboxed to the export directory (DMINE_EXPORT_DIR, default ~/.config/dmine/exports/) — absolute paths and '..' traversal are rejected.
Args: channel: channel ID or full URL. format: "jsonl" or "md". since: only messages with id > this message ID. out: filename relative to the export dir (default .). Returns: Dict with path, message_count.
Inputs: channel (required), out, since, format.
mediaA
List attachments AND embed images archived for a channel.
Downloaded files live under DMINE_MEDIA (default ~/.config/dmine/media/<channel_id>/, embed images in /embeds/). Entries without local_path only carry the CDN URL, which may already be expired. Use remedia() to (re-)download while the links are valid.
Args: channel: channel ID or full URL. limit: max messages to scan (default 200). message_since: only messages with id > this message ID. Returns: Dict: {"media_dir": str, "count": int, "attachments": [ ... ]} with per-entry kind ("attachment"|"embed_image"), message_id, ts, author, filename, downloaded, local_path, url.
Inputs: channel (required), limit, message_since.
recentA
Read the most recent archived messages of a channel (no browser needed).
Args: channel: channel ID or full URL. since: only messages with id > this message ID (e.g. the watermark from status() before a capture, to see what's new). limit: max messages (default 100). include_embeds: include embed text (tweet payloads live there). Returns: Dict: {"messages": [ {id, ts, author, content, embeds, attachments}, ... ]}.
Inputs: channel (required), limit, since, include_embeds.
remediaA
Re-download media (attachments + embed images) for ALREADY archived messages and persist local paths (idempotent, skips existing files). Also retries entries whose earlier download failed (no local file).
Like capture(), long runs continue in the background: the call returns
a {"status": "running", ...} summary after wait seconds at the latest
and the download continues — call remedia() again for the same channel
to fetch the final result.
Args: channel: channel ID or full URL. limit: only the N most recent messages (None = all). wait: seconds to wait synchronously before returning a running summary. Returns: Dict: {"channel_id", "messages_with_media", "media_entries"}.
Inputs: channel (required), wait, limit.
searchA
Substring search over the archive (message content, embeds, author).
Without scope this spans every archived server. Use server= or channel= to restrict; every hit carries its server_id/server_name.
Args: query: search term (substring match; % and _ match literally). server: optional server ID or name (case-insensitive, unique substring ok) to restrict to. channel: optional channel ID/URL to restrict to (applies within the server scope, if any). limit: max hits (default 50). Returns: Dict: {"hits": [ {id, server_id, server_name, channel_id, author_name, ts, content}, ... ]}.
Inputs: query (required), limit, server, channel.
serversB
List Discord servers known to the archive (id, name, channels, messages).
Inputs: no named parameters.
statusB
Archive status: total messages, per-channel watermark and counts.
Args: channel: optional channel ID/URL to show only that channel. Returns: Dict with totals, per-channel rows (id, name, msgs, watermark) and active_captures (background capture/backfill jobs currently running or queued on the capture lock, if any).
Inputs: channel.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 9 tools
Each tool targets a distinct action on the Discord archive: capture ingests messages, recent/search read them, export writes them to a file, media/remedia manage attachments, and servers/channels/status provide metadata. No two tools have overlapping primary purposes, so an agent can select unambiguously.
All names are lowercase single tokens without underscores or camelCase, which is a consistent format. However, the set mixes verbs (export, capture, search, remedia) and nouns/adjectives (status, servers, channels, media, recent), making the pattern slightly less predictable than a pure verb_noun scheme.
Nine tools is well within the ideal 3–15 range for a focused archive server. Each tool corresponds to a clear capability (ingest, monitor, read, search, export, navigate, media management) and none feels redundant.
The set covers the core archive lifecycle: capture, status, recent, search, export, channel/server listing, and media listing/re-download. Minor gaps exist, such as no delete/prune operation for messages or channels and no way to cancel a running background capture, but these are workaround-able or outside typical agent workflows.
Maintenance
Related MCP Connectors
Agent-native MCP server over the public saagarpatel.dev corpus. Read-only, stateless.
Official remote MCP server for Archivist AI TTRPG campaign memory: characters, sessions, and more.
Read-only MCP server for The Quiet Protocol's engines, benchmarks, proof, and business data.
Related MCP Servers
- AlicenseAqualityDmaintenanceRead-only MCP server for finding Discord messages. It enables searching guild messages, locating messages from jump URLs, and reading context around results.71MIT
- AlicenseAqualityBmaintenanceLocal-first, read-only MCP server for searching and retrieving cited evidence from archived Telegram chats, including transcripts and media metadata.53MIT
- FlicenseNot gradedqualityBmaintenanceRead-only MCP server that exposes retrieval tools over a Telegram chat archive, including archive overview, full-text search with Russian morphology, aggregates, message retrieval, and local context. It lets LLM agents answer questions from chat history without syncing Telegram, downloading files, or modifying the index.-
- AlicenseNot gradedqualityCmaintenanceEnables MCP clients to control a logged-in Discord account through 70+ tools: browsing guilds and channels, sending and reacting to messages, moderating members, managing channels, roles, emojis, and webhooks, plus searching and analyzing captured message history stored in SQLite. Authenticates using tokens already cached by the local Discord client, requiring no bot account or manual token setup.163 npmMIT