Discord Mcp

io.github.jaimenbell由 GitHub 帳號 jaimenbell 發布v0.1.1更新於 Oct 24, 2026

MCP server for the Discord REST API: 5 read tools always on, 7 write tools env-gated, default off.

已驗證STDIO僅桌面效率與工作流溝通與協作

概覽

AI 產生的概覽

一個以 Discord REST API 為基礎的本機 MCP 伺服器,讓助理讀取伺服器頻道、角色與權限,並可選擇建立或編輯頻道、角色、伺服器資訊及發送訊息。

可以做什麼
讀取伺服器結構:列出頻道、分類和角色,取得某個頻道的權限覆寫或某成員已指派角色,這些工具一律可用且唯讀。管理頻道:啟用寫入群組後可建立、重新命名、修改主題、調整上層、排序或刪除頻道。管理角色與伺服器資訊:建立和編輯角色,更新伺服器名稱、圖示、橫幅或描述。發送訊息:對文字頻道發送不超過 2000 字元的純文字內容。每次呼叫都回傳結構化的成功結果或分類錯誤物件,而不是直接崩潰。
適用情境
適合讓助理檢視 Discord 伺服器的頻道、角色與權限配置,或透過 Discord API 進行小規模的伺服器結構調整與純文字訊息發送,而不必自行撰寫帶驗證的請求。它是作品集裡的參考實作,並非 Discord 官方伺服器。
執行需求
以本機 Python 程序(stdio)執行,從 PyPI 套件安裝,並以入口腳本的絕對路徑註冊。實際呼叫需要 DISCORD_BOT_TOKEN 中的機器人權杖,且機器人須在目標伺服器內並具備相應權限。寫入工具另需設定 DISCORD_MCP_ENABLE_WRITE=1。另有選用變數用於請求逾時與實網冒煙測試。
安裝前請注意
DISCORD_BOT_TOKEN 中的機器人權杖屬於憑證,README 建議不要寫進 MCP 設定檔,而由啟動程序提供。啟用 DISCORD_MCP_ENABLE_WRITE 會開啟七個寫入工具,其中 delete_channel 具破壞性且不可復原,卻與其他工具共用同一個開關。寫入工具從未對真實 Discord API 實際執行過,伺服器圖示與橫幅以 PNG 處理的假設也未經驗證。速率限制只以錯誤回傳,不會自動重試。

安裝

在 SourceWeft 中

  1. 開啟 儀表板中的 Discord Mcp,將其新增到工作區。
  2. 為需要使用其工具的對話啟用該服務。

Desktop only,透過 STDIO。 STDIO 服務會啟動本機處理程序,因此需要 SourceWeft 桌面主機。

其他 MCP 客戶端

參照 儲存庫 中的啟動說明。

README

discord-mcp

[License: MIT] [Tests]

An MCP server over the real Discord REST API -- so a Claude agent calls list_channels(guild_id="...") instead of hand-rolling an authenticated httpx request. Built to the github-mcp/bus-mcp standard in this portfolio (own pyproject, fastmcp server, typed errors, real test suite, honest README) -- fifth flagship, first over Discord.

5 read-only tools, always on + 7 write tools, gated OFF by default behind DISCORD_MCP_ENABLE_WRITE=1 -- see "Write tools" below.

Quickstart (60 seconds)

bash
python -m venv .venv.venv/Scripts/python.exe -m pip install -e ".[test]"

Add to your Claude Desktop/Code MCP config:

json
{  "mcpServers": {    "discord-mcp": {      "command": "python",      "args": ["C:/path/to/discord-mcp/run_server.py"],      "env": { "DISCORD_BOT_TOKEN": "your-bot-token-here" }    }  }}

Without DISCORD_BOT_TOKEN set, every tool call still returns a clean structured error (Discord's own 401) instead of crashing -- see "Typed errors" below. Write tools also need DISCORD_MCP_ENABLE_WRITE=1 in the same env block, or they refuse locally with policy_refusal -- see "Write tools" below.

What this is / is not

This is a reference portfolio implementation demonstrating an MCP server over a real external SaaS API (Discord) -- it is NOT an official Discord MCP server, and it is not affiliated with Discord Inc. It started from an earlier, separate sibling project's HttpDiscordClient, a stdlib-urllib Discord transport built and verified against a real live guild for that project's own server-provisioning tooling. This repo hand-adapts that client's request-building, header construction (including its deliberately descriptive User-Agent -- see below), and error handling onto httpx (matching this fleet's other MCP servers) as its own standalone client with no dependency on that sibling project. discord-mcp does not import from or depend on that other package at all.

Started as 5 read-only tools with no write capability at all; now ships a 7-tool write group, off by default, mirroring the *_MCP_ENABLE_WRITE-style gate already shipped in github-mcp/bus-mcp/desktop-mcp in this same portfolio -- see "Write tools" below.

Tools

Read (always on, no gate)

ToolDiscord endpointPurpose
list_channelsGET /guilds/{guild_id}/channelsAll channels (every type) in a guild
list_rolesGET /guilds/{guild_id}/rolesAll roles in a guild
list_categoriesGET /guilds/{guild_id}/channels (filtered)Category channels only (Discord type 4) -- Discord has no dedicated categories-only endpoint, so this filters the same channels payload client-side
get_channel_permission_overwritesGET /channels/{channel_id} (permission_overwrites field)Role/member allow+deny bitfields set on one channel
get_member_rolesGET /guilds/{guild_id}/members/{member_id} (roles field)Role ids currently assigned to one guild member

Write (gated behind DISCORD_MCP_ENABLE_WRITE=1, default OFF)

ToolDiscord endpointPurpose
create_channelPOST /guilds/{guild_id}/channelsCreate a text/voice/category channel
edit_channelPATCH /channels/{channel_id}Rename/re-topic/re-parent/reorder an existing channel
create_rolePOST /guilds/{guild_id}/rolesCreate a role
edit_rolePATCH /guilds/{guild_id}/roles/{role_id}Edit an existing role
edit_guildPATCH /guilds/{guild_id}Update guild-level identity (name/icon/banner/description)
delete_channelDELETE /channels/{channel_id}Destructive. Delete a channel
create_messagePOST /channels/{channel_id}/messagesPost a message to a text channel (content: non-empty, <= 2000 chars)

Write tools

Seven write tools were added on top of the original 5 read-only tools, mirroring the exact write-gate pattern already shipped in this portfolio (github-mcp's GITHUB_MCP_ENABLE_WRITE, desktop-mcp's DESKTOP_MCP_ENABLE_*, and most closely bus-mcp's BUS_MCP_ENABLE_WRITE + gated_write decorator, copied as the reference template). create_message was added a night later than the other 6, once it became clear that none of create_channel/edit_channel/create_role/edit_role/edit_guild/delete_channel can actually post content to a channel -- it reuses the exact same gate and the post() helper create_channel/create_role already added to client.py, no new HTTP plumbing.

  • Off by default. Set DISCORD_MCP_ENABLE_WRITE=1 (or true/yes/on) in the server's environment to enable the write group. Unset (or any other value) means every write tool call refuses locally, with zero Discord API calls attempted, returning a structured policy_refusal error:
    json
    {"ok": false, "error": {"type": "policy_refusal", "message": "Tool group 'write' is disabled. Set DISCORD_MCP_ENABLE_WRITE=1 in the server's environment to enable it.", "group": "write", "tool": "create_channel", "required_env": "DISCORD_MCP_ENABLE_WRITE"}}
  • Enforced at the route layer, not just the MCP-tool layer. The @config.gated_write decorator wraps each write function directly in discord_mcp/routes.py (not merely the @mcp.tool wrapper in server.py), so the gate is unit-testable without spinning up fastmcp or a real transport, and can't be bypassed by any alternate calling path into routes.py.
  • Read fresh from the environment on every call, never cached at import time -- an operator can arm/disarm the write group without restarting the server process, and tests can monkeypatch it per-test.
  • delete_channel gets no separate or lower bar. Despite being genuinely destructive/irreversible against a real guild, it is gated behind the exact same DISCORD_MCP_ENABLE_WRITE env var as the other write tools -- confirmed by dedicated tests in test_routes.py (setting plausible-but-wrong var names like DISCORD_MCP_ENABLE_DELETE does not arm it).
  • Same auth path as the read tools. Write calls reuse the exact same _headers()/pooled httpx.AsyncClient construction in client.py as every read tool -- there is only one auth code path in this repo.
  • create_message validates content locally, not just via Discord's own 400. content must be a non-empty string of at most 2000 characters (Discord's real message-length limit) -- checked by client.validate_message_content before any request is built, same "never even attempt the call" discipline as the snowflake-id checks. A violation returns validation_error, not a raw Discord 400.

icon_base64/banner_base64 on edit_guild -- a flagged assumption

Discord's docs describe PATCH /guilds/{id}'s icon/banner fields as an "image data" string -- a full data URI (data:image/png;base64,<base64>), not a bare base64 payload. edit_guild accepts either:

  • a full data:...;base64,... URI, passed through unchanged, or
  • raw base64 bytes, which are assumed to be PNG and wrapped as data:image/png;base64,<value>.

This PNG assumption is not verified against a real Discord response -- this repo has not confirmed whether Discord accepts, rejects, or silently mis-renders a non-PNG image (JPEG, animated GIF for boosted-server icons, etc.) sent under an image/png label. If you have a non-PNG image, pass a full data:image/...;base64,... URI yourself rather than relying on the default. Flagged here rather than guessed silently.

Typed errors, never a raw crash

Every tool returns {"ok": true, ...} on success or {"ok": false, "error": {...}} on failure -- never an unhandled exception or stack trace.

  • network_error -- connection refused, timeout, DNS failure, or a malformed request URL. Discord's API wasn't reachable at all.
  • auth_error (401) -- missing or invalid bot token.
  • permission_error (403, with Discord's own JSON error body, e.g. {"message": "Missing Access", "code": 50001}) -- the bot lacks the permission/scope for this call, or isn't in the guild.
  • cloudflare_blocked (403, with no JSON error body) -- Discord's API docs ask for a descriptive User-Agent; without one, a client's default UA is a well-known bot fingerprint that Discord's Cloudflare edge can reject with a bare 403 before the request ever reaches route-level permission checks. This is otherwise indistinguishable from a real permission_error 403 -- Discord's real permission-denied responses always carry a JSON body, so absence of a JSON body on a 403 is the signal this type is built on. This is a best-effort heuristic, not a certainty: a proxy, load balancer, or future Discord change that strips the body on a different kind of 403 would also land here. discord-mcp sends the same descriptive User-Agent this heuristic exists to explain (see client.py's _headers()), so in practice this type should rarely fire from this server's own calls -- see "Honest limitations" below.
  • not_found (404) -- bad/unknown guild, channel, role, or member id.
  • rate_limited (429) -- Discord's rate limit. Carries retry_after_s, parsed straight from Discord's own JSON body (retry_after, in seconds). This server does not auto-retry -- it surfaces the limit as a structured error immediately and leaves any backoff/retry decision to the caller.
  • decode_error -- a 2xx response whose body isn't valid JSON (should not happen against the real API; guards against a malformed proxy/mock).
  • discord_api_error -- any other 4xx/5xx not covered above.
  • policy_refusal (write tools only) -- the write group is disabled; see "Write tools" above. No Discord API call is attempted.
  • invalid_id -- a guild/channel/role/member id failed snowflake-shape validation before any request was built (defense against path injection).
  • validation_error (create_message only) -- content was empty or exceeded Discord's real 2000-character message limit, caught before any request was built.

Internally, discord_mcp/client.py raises typed DiscordUnreachable / DiscordApiError (with a DiscordDecodeError subclass for the 2xx-non-JSON case) exceptions; discord_mcp/routes.py catches both and normalizes to the dict shape above before a tool ever returns. Tests exercise both layers for every error type, across every HTTP verb (GET/POST/PATCH/DELETE).

Honest limitations

  • The cloudflare_blocked vs. permission_error split is a heuristic (JSON-body-present-or-not), not something Discord documents or guarantees. It is accurate for the specific failure mode it was written to explain (an edge reject due to a missing/generic User-Agent) but a 403 with a stripped body from an unrelated cause (e.g. a misbehaving proxy in between) would also be classified as cloudflare_blocked.
  • get_channel_permission_overwrites and get_member_roles were not exercised against every possible real-world edge case (e.g. a member with zero roles, a channel with zero overwrites) via the live smoke test -- only via respx-mocked unit tests. The live smoke test only calls list_channels/list_roles; it does not exercise any write tool.
  • No pagination is implemented anywhere. list_channels/list_roles are single-request, unpaginated Discord endpoints (Discord doesn't paginate either of these), so this is a non-issue for the tools in scope -- but a guild large enough to need member-list pagination is out of scope entirely (there is no list_members tool here).
  • The icon_base64/banner_base64 "assumed PNG" default on edit_guild has not been verified against a real Discord response -- see "Write tools" above.
  • No write tool has been exercised against the real Discord API at all (by design -- this task's constraints require zero real network calls; the live smoke test remains read-only-only). This includes create_message -- it has never posted a real message to a real live guild as part of this repo's own build/test process; that remains a deliberate, separate operator action.

Env vars

VarDefaultPurpose
DISCORD_BOT_TOKENunsetBot token, sent as Authorization: Bot <token>. Read-only tools still work without one in the unit test suite (fully mocked); against the real API, a missing token surfaces as a normal auth_error (401) from Discord itself.
DISCORD_MCP_ENABLE_WRITEunset (OFF)Set to 1/true/yes/on to enable the 7 write tools. See "Write tools" above.
DISCORD_MCP_TIMEOUT_S10.0Per-request timeout (seconds)
DISCORD_MCP_LIVEunsetSet to 1 to run the real-network smoke test (see Testing)
DISCORD_MCP_SMOKE_GUILD_IDunsetGuild id the live smoke test targets. Test skips cleanly if unset.
DISCORD_MCP_SMOKE_ENV_PATHunsetPath to an external .env file to source DISCORD_BOT_TOKEN from for the live smoke test, if not already in the environment.

DISCORD_API_BASE (https://discord.com/api/v10) is a fixed constant, not env-overridable -- unlike bus-mcp's self-hosted BUS_MCP_BASE_URL, Discord's REST API has exactly one real base URL.

Usage examples

Once connected in a Claude session, an agent can:

list_channels(guild_id="123456789012345678")list_roles(guild_id="123456789012345678")list_categories(guild_id="123456789012345678")get_channel_permission_overwrites(channel_id="...")get_member_roles(guild_id="123456789012345678", member_id="...")

With DISCORD_MCP_ENABLE_WRITE=1 set:

create_channel(guild_id="...", name="general", type=0)edit_channel(channel_id="...", name="renamed", position=3)create_role(guild_id="...", name="Mod", color=1752220, hoist=True)edit_role(guild_id="...", role_id="...", name="Senior Mod", permissions="8")edit_guild(guild_id="...", name="New Server Name")delete_channel(channel_id="...")create_message(channel_id="...", content="hello from discord-mcp")

Testing

bash
.venv/Scripts/python.exe -m pytest -q

163 tests: 162 unit tests (respx-mocked, zero real network) + 1 live smoke (gated, see below). All 12 tools' happy paths are covered, every error type above is exercised at both the client layer (test_client.py) and the routes-normalization layer (test_routes.py), and test_server.py actually asyncio.run()s each @mcp.tool async wrapper function against a monkeypatched routes module -- not just introspects list_tools() -- so an arg-name mismatch or dropped kwarg between server.py and routes.py would be caught.

Write-tool coverage specifically (test_config.py + test_routes.py): gate-off refusal for each of the 7 write tools with len(respx.calls) == 0 asserted (proving no Discord call is even attempted), gate-on success paths with mocked Discord responses (including request-body assertions), gate-on Discord-API-error passthrough (permission/not-found/rate-limit/network/5xx), and delete_channel specifically double-checked -- both that it refuses identically to the other write tools, and that no plausible-but-wrong env var name (DISCORD_MCP_ENABLE_DELETE, etc.) accidentally arms it. create_message additionally gets dedicated content-length boundary tests (test_client.py/test_routes.py): exactly 2000 characters is accepted (1 respx call), 2001 characters and an empty string are both rejected locally as validation_error with zero respx calls.

Live smoke test

tests/test_live_smoke.py::test_live_list_channels_and_roles_against_a_real_guild is gated behind DISCORD_MCP_LIVE=1 and DISCORD_MCP_SMOKE_GUILD_ID (unset by default -- both must be set to run) and calls the real Discord API's list_channels/list_roles against that guild. It sources DISCORD_BOT_TOKEN from this process's own environment if already set, otherwise, if DISCORD_MCP_SMOKE_ENV_PATH points at an external .env file, loads it from there (once, without ever printing or logging the value) -- this repo has no .env of its own and never will; the real token lives in a separate operator-controlled location. It remains read-only-only -- no write tool has a live smoke test.

bash
DISCORD_MCP_LIVE=1 DISCORD_MCP_SMOKE_GUILD_ID=<your-guild-id> \DISCORD_MCP_SMOKE_ENV_PATH=<path-to-.env-with-token> \.venv/Scripts/python.exe -m pytest tests/test_live_smoke.py -v

Verified passing against a real guild at the time of writing: real channels, roles, and categories all returned as non-empty lists.

Install / connect

bash
python -m venv .venv.venv/Scripts/python.exe -m pip install -e ".[test]"

Register in ~/.claude.json under mcpServers.discord-mcp as a stdio server invoking run_server.py by absolute path (no cwd needed -- the entrypoint adds its own directory to sys.path). env: {} in the real registration -- the real bot token is never baked into ~/.claude.json; set DISCORD_BOT_TOKEN in whatever process actually launches the server if you want authenticated calls, and DISCORD_MCP_ENABLE_WRITE=1 if you want the write group armed. Arming the write gate in the real registration is a deliberate, separate operator action -- not part of this repo's default config.

Handshake check

bash
.venv/Scripts/python.exe scripts/list_tools.py

Prints the twelve registered tool names with no transport started -- pure introspection, useful for verifying the server wires up cleanly after any change.

Out of scope

  • Pagination / list_members (see "Honest limitations" above).
  • Retrying rate-limited (429) requests -- surfaced as a structured error, left to the caller.
  • Distinguishing every possible cause of a body-less 403 with certainty (see the cloudflare_blocked heuristic's honest limitation above).
  • Any write operation beyond the 7 tools above (e.g. member role assignment/kick/ban, message edit/delete, embeds/attachments/reactions, webhook management) -- create_message covers plain-text content only, no embeds/files/reactions. A broader, differently shaped write-capable project exists elsewhere in this author's portfolio with an apply_server_structure-style tool, out of scope here.

Commercial support

Maintained by Jaimen Bell. For production MCP integrations, custom servers, or agent-reliability work, see jaimenbell.dev.

Building your own MCP server? The MCP Starter Kit has templates, a build playbook, and packaging war-stories from shipping this one.

mcp-name: io.github.jaimenbell/discord-mcp

來源:README.md,提交 3fb0bec

工具

0
SourceWeft 尚未在沙箱中啟動過該軟體包,因此其工具未知。

版本歷史

1
  1. v0.1.1最新Sep 16, 2026

評分

已滿足適用檢查項的 50%

該評分衡量此收錄條目的完整程度,依據註冊中心條目、端點與公開來源計算得出。它不是對伺服器本身的評價,也不表示伺服器安全或目前可用:可連通性是帶日期的檢查結果,評分也不包含安裝量或使用者評價。

  • 近 30 天內可連通25 分

    請保持端點能夠回應 MCP 握手;SourceWeft 每 30 天探測一次。

  • 已宣告認證方式10 分

    請在註冊中心條目中宣告憑證請求標頭,或保持端點能夠回應探測,以便判定其認證方式。

  • 已列出工具15 分

    請讓伺服器在無憑證時回應 tools/list,或發布可在沙箱中無憑證啟動的軟體包。

  • 描述必需15 分
  • 圖示10 分
  • 文件15 分
  • 授權條款10 分