
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.
概覽
一個以 Discord REST API 為基礎的本機 MCP 伺服器,讓助理讀取伺服器頻道、角色與權限,並可選擇建立或編輯頻道、角色、伺服器資訊及發送訊息。
- 可以做什麼
- 讀取伺服器結構:列出頻道、分類和角色,取得某個頻道的權限覆寫或某成員已指派角色,這些工具一律可用且唯讀。管理頻道:啟用寫入群組後可建立、重新命名、修改主題、調整上層、排序或刪除頻道。管理角色與伺服器資訊:建立和編輯角色,更新伺服器名稱、圖示、橫幅或描述。發送訊息:對文字頻道發送不超過 2000 字元的純文字內容。每次呼叫都回傳結構化的成功結果或分類錯誤物件,而不是直接崩潰。
- 適用情境
- 適合讓助理檢視 Discord 伺服器的頻道、角色與權限配置,或透過 Discord API 進行小規模的伺服器結構調整與純文字訊息發送,而不必自行撰寫帶驗證的請求。它是作品集裡的參考實作,並非 Discord 官方伺服器。
- 執行需求
- 以本機 Python 程序(stdio)執行,從 PyPI 套件安裝,並以入口腳本的絕對路徑註冊。實際呼叫需要 DISCORD_BOT_TOKEN 中的機器人權杖,且機器人須在目標伺服器內並具備相應權限。寫入工具另需設定 DISCORD_MCP_ENABLE_WRITE=1。另有選用變數用於請求逾時與實網冒煙測試。
安裝
在 SourceWeft 中
- 開啟 儀表板中的 Discord Mcp,將其新增到工作區。
- 為需要使用其工具的對話啟用該服務。
Desktop only,透過 STDIO。 STDIO 服務會啟動本機處理程序,因此需要 SourceWeft 桌面主機。
其他 MCP 客戶端
參照 儲存庫 中的啟動說明。
README
discord-mcp
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)
Add to your Claude Desktop/Code MCP config:
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)
Write (gated behind DISCORD_MCP_ENABLE_WRITE=1, default OFF)
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(ortrue/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 structuredpolicy_refusalerror: - Enforced at the route layer, not just the MCP-tool layer. The
@config.gated_writedecorator wraps each write function directly indiscord_mcp/routes.py(not merely the@mcp.toolwrapper inserver.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 intoroutes.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_channelgets no separate or lower bar. Despite being genuinely destructive/irreversible against a real guild, it is gated behind the exact sameDISCORD_MCP_ENABLE_WRITEenv var as the other write tools -- confirmed by dedicated tests intest_routes.py(setting plausible-but-wrong var names likeDISCORD_MCP_ENABLE_DELETEdoes not arm it).- Same auth path as the read tools. Write calls reuse the exact same
_headers()/pooledhttpx.AsyncClientconstruction inclient.pyas every read tool -- there is only one auth code path in this repo. create_messagevalidates content locally, not just via Discord's own 400.contentmust be a non-empty string of at most 2000 characters (Discord's real message-length limit) -- checked byclient.validate_message_contentbefore any request is built, same "never even attempt the call" discipline as the snowflake-id checks. A violation returnsvalidation_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 descriptiveUser-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 realpermission_error403 -- 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 descriptiveUser-Agentthis heuristic exists to explain (seeclient.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. Carriesretry_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_messageonly) --contentwas 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_blockedvs.permission_errorsplit 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 ascloudflare_blocked. get_channel_permission_overwritesandget_member_roleswere 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 callslist_channels/list_roles; it does not exercise any write tool.- No pagination is implemented anywhere.
list_channels/list_rolesare 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 nolist_memberstool here). - The
icon_base64/banner_base64"assumed PNG" default onedit_guildhas 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
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:
With DISCORD_MCP_ENABLE_WRITE=1 set:
Testing
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.
Verified passing against a real guild at the time of writing: real channels, roles, and categories all returned as non-empty lists.
Install / connect
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
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_blockedheuristic'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_messagecovers plain-textcontentonly, no embeds/files/reactions. A broader, differently shaped write-capable project exists elsewhere in this author's portfolio with anapply_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版本歷史
1- v0.1.1最新Sep 16, 2026
評分
已滿足適用檢查項的 50%
該評分衡量此收錄條目的完整程度,依據註冊中心條目、端點與公開來源計算得出。它不是對伺服器本身的評價,也不表示伺服器安全或目前可用:可連通性是帶日期的檢查結果,評分也不包含安裝量或使用者評價。
- 近 30 天內可連通25 分
請保持端點能夠回應 MCP 握手;SourceWeft 每 30 天探測一次。
- 已宣告認證方式10 分
請在註冊中心條目中宣告憑證請求標頭,或保持端點能夠回應探測,以便判定其認證方式。
- 已列出工具15 分
請讓伺服器在無憑證時回應 tools/list,或發布可在沙箱中無憑證啟動的軟體包。
- 描述必需15 分
- 圖示10 分
- 文件15 分
- 授權條款10 分