Skip to main content
Glama
coretez

fluency-discord-mcp

by coretez

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

DISCORD_MODE

read / write / admin, defaulting to admin. Tools above the mode are never registered — a client cannot call what it cannot see. Set it lower to hand out less.

Guild

DISCORD_GUILD_ID

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

DISCORD_ALLOW_DESTRUCTIVE

Delete, kick, ban and bulk-delete additionally require this flag and an explicit confirm: true argument.

Dry run

per-call dry_run

discord_delete_channel, discord_delete_role, discord_delete_message and discord_set_channel_permissions report exactly what they would destroy and change nothing. A preview needs neither confirmation nor the destructive switch — that is precisely when someone is deciding whether to enable it.

Channel allowlist

DISCORD_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_messages pages history and filters in-process. scan_limit trades depth for API calls.

  • Member listing needs a privileged intent. discord_list_members requires 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

moderation-triage

Working a report from evidence to action without over-reaching

channel-provisioning

Creating channels and access rules without breaking existing permissions

community-digest

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=1477871529174

Discord 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 build

4. Check it. Reads only; never writes.

DISCORD_BOT_TOKEN=... DISCORD_GUILD_ID=1542903941933826118 DISCORD_MODE=admin npm run preflight

It 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.sh

It 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 register

The 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 tier

Membership 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

read

DISCORD_MEMBER_TIER

Holds a mapped role (highest wins)

write / admin

DISCORD_ROLE_TIERS

Guild owner

admin

DISCORD_OWNER_TIER

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 start

Bind 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 analyze

That runs three steps and writes each result under analysis/:

  1. Capture the surface — boot the server, enumerate tools, resources and UI content.

  2. Capture runtime evidencescripts/capture-evidence.mjs drives every registered tool down a failure path and records the CallToolResult, 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.

  3. 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, so discord_list_channels is the honest name for listing channels and an outcome-shaped alias would describe the API less accurately. describe_capabilities is 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_exemptdescribe_capabilities reads 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.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers

  • A
    license
    B
    quality
    D
    maintenance
    Enables 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.
    19
    88
    18
    MIT
  • A
    license
    B
    quality
    D
    maintenance
    Enables MCP clients to interact with Discord servers, allowing operations such as sending messages and reading message history through the Discord API.
    23
    88
    MIT
  • A
    license
    A
    quality
    B
    maintenance
    MCP server over the real Discord REST API: 5 read-only tools plus 7 write tools gated off by default behind DISCORD_MCP_ENABLE_WRITE.
    12
    MIT
  • A
    license
    Not graded
    quality
    F
    maintenance
    MCP 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.
    7
    3
    MIT