Skip to main content
Glama

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 automatically

Alternatively, 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 channels

search 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

recent

no

most recent archived messages of a channel

search

no

substring search over content, embeds and author

status

no

total messages, per-channel watermark and counts

servers

no

known servers (id, name, channel and message counts)

channels

no

known channels, optionally of one server

media

no

archived attachments and embed images for a channel

export

no

export a channel to jsonl/markdown, sandboxed to DMINE_EXPORT_DIR

capture

yes

archive a channel (incremental or full backfill)

remedia

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. .gitignore already excludes the archive, the browser profile, media and QR codes.

Environment variables

Var

Default

DMINE_DB

~/.config/dmine/archive.sqlite

DMINE_PROFILE

~/.config/dmine/profile

DMINE_MEDIA

~/.config/dmine/media

DMINE_PORT

9223

DMINE_STATE

~/.config/dmine (locks, pid files)

DMINE_EXPORT_DIR

~/.config/dmine/exports (MCP export sandbox)

DMINE_MAX_MEDIA_MB

1024 (per-file download cap)

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 -q

License

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 tools
captureA

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.

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

    A3.9/5.0

    Scored across 9 tools

    Disambiguation5/5

    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.

    Naming Consistency4/5

    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.

    Tool Count5/5

    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.

    Completeness4/5

    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

    ActivitySlowing
    ResponsivenessNo recent community issues

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      A
      quality
      D
      maintenance
      Read-only MCP server for finding Discord messages. It enables searching guild messages, locating messages from jump URLs, and reading context around results.
      7
      1
      MIT
    • A
      license
      A
      quality
      B
      maintenance
      Local-first, read-only MCP server for searching and retrieving cited evidence from archived Telegram chats, including transcripts and media metadata.
      5
      3
      MIT
    • F
      license
      Not graded
      quality
      B
      maintenance
      Read-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.
      -
    • A
      license
      Not graded
      quality
      C
      maintenance
      Enables 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 npm
      MIT