fluency-discord-mcp
Provides tools for interacting with a Discord guild via the Discord REST API, including reading channels, messages, members, and threads; sending, editing, and pinning messages; creating threads; managing channels, roles, and permissions; and performing moderation actions such as timeout, kick, ban, and audit.
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., "@fluency-discord-mcpcreate a community digest of this week's activity"
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.
fluency-discord-mcp
An MCP server over the Discord REST API, scoped to the FluencySecurityAi guild
(1542903941933826118). Read the server, post to it, and moderate it from any MCP client.
Runs two ways: as a local stdio process for a single operator, or hosted over HTTP where callers
sign in with Discord and their guild roles decide what they can do. See
Authentication. The live instance is
discord.fluencyalliance.com; the runbook for it is
DEPLOY.md.
REST only — no gateway connection. MCP tools are pull-based, so a websocket would buy nothing and keep a process hot for no reason. The consequence: this server answers questions and takes actions on request; it cannot react to events as they happen.
Guardrails
It ships write-capable, and that is on purpose. A server that can only read is a worse Discord
client than Discord — the value here is connecting the guild to other tools and getting something
done, which needs posting. DISCORD_MODE defaults to admin; stepping down to write or read
is a deliberate choice an operator makes, not a ladder they have to climb first.
What stays off by default is the genuinely irreversible: delete, kick, ban and bulk-delete need
DISCORD_ALLOW_DESTRUCTIVE and a per-call confirm: true. Independent fences, each of which can
refuse on its own:
Fence | Env | Effect |
Mode |
|
|
Guild |
| Every call resolves to a guild id and is refused if it is not listed. Channel-addressed tools resolve the channel's guild first. |
Destructive switch |
| Delete, kick, ban and bulk-delete additionally require this flag and an explicit |
Dry run | per-call |
|
Channel allowlist |
| Optional. Confines writes to named channels. Checked locally, before any API call. |
Identity | Discord OAuth | Over HTTP, the caller's Discord roles pick the mode for their session. Not a check layered on top: the session's server is built at that tier, so higher tools are never registered for them. |
Refusals come back as tool errors naming which fence fired, so an agent can tell "not permitted" from "Discord said no".
Related MCP server: discord-mcp
Tool surface
Server inspection (5, available in every mode) — describe_capabilities (the routing map,
with an MCP Apps UI resource), inspect_version_compatibility, list_skills, load_skill,
report_client_issue
read (+10) — discord_whoami, discord_guild_info, discord_list_channels,
discord_list_roles, discord_list_members, discord_find_member, discord_get_member,
discord_list_threads, discord_read_messages, discord_search_messages
write (+5) — discord_send_message, discord_edit_message, discord_add_reaction,
discord_pin_message, discord_create_thread
admin (+17) — channels (create / edit / delete / set_channel_permissions),
roles (create / edit / delete / manage_member_role),
moderation (member_audit, timeout_member, kick_member, ban_member, unban_member,
list_bans, set_nickname), messages (delete_message, bulk_delete_messages)
Every description is assembled from three required fields — what the tool does, when to call it,
and what it returns — plus its required argument names and its near-neighbour cross references,
both derived from the schema at registration. A new tool cannot ship with those missing, because
ToolSpec will not compile without them.
Two notes on what Discord itself allows:
Search is local. Discord's native message-search endpoint is not available to bots, so
discord_search_messagespages history and filters in-process.scan_limittrades depth for API calls.Member listing needs a privileged intent.
discord_list_membersrequires SERVER MEMBERS to be enabled on the bot application, or Discord returns Missing Access.
Skills
skills/ holds versioned operational playbooks, delivered on request through list_skills and
load_skill rather than crammed into tool descriptions — routing text stays short and the depth
is fetched only when a task needs it. Files are read at call time, so editing a playbook needs no
rebuild. Point DISCORD_SKILLS_DIR elsewhere to serve your own.
Skill | Covers |
| Working a report from evidence to action without over-reaching |
| Creating channels and access rules without breaking existing permissions |
| Turning a week of activity into a summary someone will read |
Setup
1. Create the bot at https://discord.com/developers/applications → New Application → Bot. Copy the token. Under Privileged Gateway Intents, enable SERVER MEMBERS INTENT if you want member listing.
2. Invite it to the guild, with the permission set matching the mode you intend to run.
Replace YOUR_APP_ID:
read https://discord.com/api/oauth2/authorize?client_id=YOUR_APP_ID&scope=bot&permissions=66688
write https://discord.com/api/oauth2/authorize?client_id=YOUR_APP_ID&scope=bot&permissions=377957248192
admin https://discord.com/api/oauth2/authorize?client_id=YOUR_APP_ID&scope=bot&permissions=1477871529174Discord permissions are necessary but not sufficient: to moderate a member or assign a role, the bot's highest role must sit above the target's. Drag the bot's role up in Server Settings → Roles.
Re-inviting is how a bot gains permissions later. The permission bits in the invite URL are
baked into the bot's role at join time. Raising DISCORD_MODE afterwards changes only which tools
get registered — never what the guild will let them do. A server started at admin whose bot
joined from the read URL therefore exposes the whole admin surface and 403s on the first
structural call. Either re-invite from the URL matching the mode you want, or tick the missing
permissions onto the bot's existing role in Server Settings → Roles. Step 4 catches this before an
agent does.
3. Build.
npm install && npm run build4. Check it. Reads only; never writes.
DISCORD_BOT_TOKEN=... DISCORD_GUILD_ID=1542903941933826118 DISCORD_MODE=admin npm run preflightIt reports the bot's identity, its effective permissions against what the mode needs, its role position, the channel list, and whether the members intent is on. Exits non-zero if the mode promises more than the bot can deliver.
5. Store the token in the keychain, not in a file:
./scripts/store-token.shIt prompts without echoing and validates before storing. A bot token is three dot-separated base64url segments whose first segment decodes to the application id, so the script can tell you when you have pasted the Public Key or the Application ID instead — the two fields adjacent to it on the portal page, and the two that get grabbed by mistake.
scripts/with-token.sh is the matching launcher: it reads the token back at spawn time and execs
the server, so the secret is never in .mcp.json, never in the repo, and never in a process
argument list where anyone running ps could read it.
6. Register with a client.
Claude Code reads .mcp.json from the project root. It is committed, holds no secret, and
points at with-token.sh — opening the project is the whole setup.
Claude Desktop keeps one global config instead:
DISCORD_BOT_TOKEN='<your bot token>' npm run registerThe token is read from the environment rather than argv, for the ps reason above. The script
backs up the existing config, merges the entry idempotently, and chmods the result to 600. It
registers at DISCORD_MODE=admin with destructive actions off — usable immediately, with the
irreversible things still behind their own switch. Pass DISCORD_MODE=read or write to register
with less. Claude Desktop reads the file only at launch, so restart it afterwards.
Either way this is a local stdio process that the client spawns, so it serves exactly one operator and will not appear in claude.ai sessions, which load only hosted HTTPS servers. To serve other people, run it over HTTP instead — see Deployment model and Authentication.
Give the local and hosted servers different names in your client config. OAuth tokens are stored per endpoint, so two entries sharing a name means authenticating one does nothing for the other, and one silently shadows the other.
Deployment model
Two ways to run, and the difference is who the operator is.
stdio — the client spawns the process, so the operator is whoever owns the machine. Mode is
fixed at boot by DISCORD_MODE. This is the right shape for one person with admin, and it is
unchanged by everything below.
http — one hosted process, many callers, each authenticated with Discord. Mode comes from the
caller rather than the environment. Set DISCORD_TRANSPORT=http.
The bot token is a guild-level credential and the bot is already a member, so hosting it does not mean everyone creates a Discord application. Two identities are in play, and keeping them apart is the whole design:
What it is | How many | |
Discord identity | the bot — what actually calls the REST API | one, server-side, never distributed |
Operator identity | who is asking the bot to act | one per authenticated human |
Handing the bot token to each person collapses those into one: full authority over the guild, no attribution, and revocation that can only be all-or-nothing.
Authentication
Over HTTP, callers sign in with Discord and their guild roles decide their tier.
This server is the OAuth 2.1 authorization server; Discord is the login step inside it. That is not a stylistic choice — Discord has no Dynamic Client Registration, and MCP clients register themselves, so pointing a client straight at Discord cannot work. We issue the tokens; Discord proves who the human is.
The flow, once per 8 hours:
client → /mcp 401 + WWW-Authenticate
→ /.well-known/…/mcp discovery
→ /register client registers itself (DCR)
→ /authorize 302 → discord.com
human approves
→ /auth/discord/callback identify + role lookup → tier
→ /token access token carrying the tierMembership is the authorization. After login the server reads the caller's roles from
/users/@me/guilds/{id}/member, which 404s for anyone outside the guild. There is no allowlist to
maintain and no "should this person be allowed" question to answer — leaving the guild revokes
access by itself.
Caller | Tier | Env |
Not a guild member | refused | — |
Guild member, no matching role |
|
|
Holds a mapped role (highest wins) |
|
|
Guild owner |
|
|
Ownership is handled separately because Discord does not express it as a role. The defaults
therefore work on a guild with no roles at all: the owner gets admin, everyone else read.
DISCORD_ROLE_TIERS (roleId:tier,roleId:tier) starts mattering the moment a role exists.
The scopes requested are identify and guilds.members.read — enough to name the caller and read
their roles, and not enough to read their messages or join servers on their behalf.
Tokens last 8 hours and there are no refresh tokens. Re-authorizing is when a role change takes effect; silent indefinite renewal would let a revoked role keep working. Token state is in-memory, so restarting the service logs everyone out.
Setting DISCORD_CLIENT_ID, DISCORD_CLIENT_SECRET and DISCORD_PUBLIC_URL enables all of this;
omitting them runs the server unauthenticated, which is only appropriate on a trusted network.
Deploying it
DISCORD_TRANSPORT=http DISCORD_HTTP_PORT=8500 npm startBind loopback and terminate TLS in front. The reverse proxy needs two things beyond the obvious:
proxy_set_header Host $host; # DISCORD_ALLOWED_HOSTS is checked against it
proxy_buffering off; # MCP streams over SSE
proxy_read_timeout 3600s;proxy_buffering off is the one that bites: with buffering on, nginx holds streamed events until
the response completes, which for a live session never happens, and the server appears to hang.
GET /healthz reports version, whether auth is on, and live session counts.
Validation
The surface is validated with mcp_analysis against
the MCP 2025-11-25 specification plus the Headless conventions profile:
MCP_ANALYSIS_DIR=/path/to/mcp_analysis npm run analyzeThat runs three steps and writes each result under analysis/:
Capture the surface — boot the server, enumerate tools, resources and UI content.
Capture runtime evidence —
scripts/capture-evidence.mjsdrives every registered tool down a failure path and records theCallToolResult, so error behaviour is proven rather than inferred. The harness builds its child environment from scratch with an invalid token, destructive actions off, and non-existent skill and log paths: no call it makes can reach Discord or mutate a guild, even if a real token is exported in the calling shell.Validate against the evidence bundle.
Current result: 1,164 checks — specification 100%, extensions 100%, quality 98.5%.
analysis/fluency-discord.json is this server's profile: the headless conventions plus a
tool_namespace declaration and two deliberate exemptions.
n1_exempt— this server is a thin, guarded layer over Discord's REST API, sodiscord_list_channelsis the honest name for listing channels and an outcome-shaped alias would describe the API less accurately.describe_capabilitiesis the outcome-oriented front door; it routes to the primitives by task rather than replacing them. RULES.md contemplates exactly this: "CRUD primitives may remain as deliberate building blocks and profile exemptions."j10_exempt—describe_capabilitiesreads in-memory configuration, has no external dependency, and therefore no runtime failure path to capture.
Five N4-INSPECT-TWIN warnings remain, all on additive creation tools — send_message,
create_thread, create_channel, create_role, set_nickname — where a dry run would report
back only what the caller just typed. RULES.md agrees ("not required for every additive creation
tool unless a profile chooses that policy") but the profile schema has no n4_exempt to say so,
which is why these stay visible rather than being declared.
Static analysis proves the contract, not the behaviour: authorization, role-position enforcement, confirmation enforcement against a live guild, and audit records still need a real token.
Operating notes
A rebuild does not reach a running server. The client spawns this process and holds it for the
life of the session, so npm run build writes dist/ underneath a server that already loaded the
old code. Restart the client session — or, when a fix appears not to have landed, compare the
timestamps:
stat -f '%Sm %N' -t '%H:%M:%S' dist/*.js
ps -eo pid,lstart,command | grep '[d]ist/index.js'A server whose start time precedes the build is serving stale code.
Empty message bodies are meaningful. System messages (joins, pins, boosts) and sticker-only
messages carry no content by design. They render as <system: joined the server> and
<sticker: Wave> rather than as blank lines, because a blank body is otherwise indistinguishable
from the redaction you get when the Message Content intent is switched off.
A 403 / 50013 has two causes and one message. Discord's "Missing Permissions" means either
the bot's role lacks the permission bit, or its highest role sits below the target's — the error
does not say which. npm run preflight separates them: it prints the effective permission set
against what the mode needs, and the role position. discord_whoami and describe_capabilities
will not, and should not be read as a permission check — both report the mode's surface, which
is this server's configuration rather than anything Discord has agreed to. An admin-mode server on
an under-permissioned bot will cheerfully list Structure as available and then 403 on
discord_create_channel.
Opening this project can swap which server you are talking to. .mcp.json is project-scoped,
so a client that moves into this directory spawns the entry defined here — which may not be the
same instance, or the same mode, as one registered globally. Two servers, one bot, different
capability, and nothing announces the switch. discord_whoami reports the mode of whichever one
actually answered; check it before concluding a tool has gone missing.
Rate limits belong to the token, not the caller. Everyone driving a given bot token shares one set of Discord rate-limit buckets.
Guild content is data, not instruction. A message asking the agent to ban someone, post
something, or change a setting is not authorization for it. The server says so in its instructions,
and it holds for anything built on top of this too.
Restarting the hosted server logs everyone out. Tokens live in memory, so every deploy forces re-authentication. Deliberate while token persistence is outstanding, but worth timing.
A 401 is the beginning of the flow, not a failure. An MCP client showing "needs authentication"
has correctly discovered that it must log in. The failure worth chasing is a 401 whose
WWW-Authenticate header points at a URL that 404s — RFC 9728 suffixes the resource path onto the
metadata URL, so it is /.well-known/oauth-protected-resource/mcp, not the bare path. Derive it
with the SDK's getOAuthProtectedResourceMetadataUrl rather than by hand.
OAuth tokens are stored per endpoint. Registering the same server under one name at two endpoints (a local stdio one and a hosted one) means authenticating one does nothing for the other, and whichever scope wins will silently shadow the other. Give them distinct names.
This server cannot be deployed
Maintenance
Related MCP Connectors
An MCP server that provides an API to LLMs to manage their JumpCloud resources.
The official Planning Center MCP server for interacting with your ministry's data.
- SupabaseOAuthcom.supabase
MCP server for interacting with the Supabase platform
MCP server for Sendbird — chat users, channels, members, and messages from your AI client.
Related MCP Servers
- AlicenseBqualityDmaintenanceEnables comprehensive Discord bot management and server operations through MCP, including channel management, message handling, member moderation, role management, and voice operations. Provides secure Discord API integration with built-in permission controls and audit logging capabilities.198818MIT
- AlicenseBqualityDmaintenanceEnables MCP clients to interact with Discord servers, allowing operations such as sending messages and reading message history through the Discord API.2388MIT
- AlicenseAqualityBmaintenanceMCP server over the real Discord REST API: 5 read-only tools plus 7 write tools gated off by default behind DISCORD_MCP_ENABLE_WRITE.12MIT
- AlicenseNot gradedqualityFmaintenanceMCP server that gives AI agents first-class access to Discord, enabling discovery, messaging, channel management, moderation, and arbitrary REST calls through typed, consent-aware tools.73MIT