Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

MCP Server

The bot embeds a Model Context Protocol (MCP) server that exposes Discord server-management as 51 tools any MCP client can call. The intended workflow is to point an AI coding assistant — Claude Code, Cursor, etc. — at the bot and let it manage your Discord server programmatically.

What it does

When the bot starts, it spins up an HTTP server on a configurable port (default 9090) and registers the tool catalog with the standard MCP streamable-HTTP transport. An MCP client connects, lists the available tools, and can then invoke them. Every tool runs against the same Discord HTTP client the bot itself uses, so the actions execute as the bot user with the bot’s permissions.

The point of this is that you can talk to your Discord server in natural language from inside an AI assistant:

“Find the channel category called Archive and move all read-only channels into it.”

“List every member with the @Verified role who hasn’t sent a message in 30 days.”

“Create a new role ‘Beta Testers’, colour green, no permissions, and assign it to these five users.”

Instead of writing Discord API code or clicking through the Discord UI, your AI assistant calls the matching MCP tools.

Why this exists

Discord administration is full of repetitive multi-step operations: auditing roles, cleaning up old channels, mass-renaming, bulk permission changes. The Discord client doesn’t have a scripting interface, and writing one-off API scripts for every cleanup task is tedious. An LLM with the right tools can plan the operation, ask for confirmation, and execute it in a couple of turns.

The bot is also a natural place to put this server: it is already a long-running process holding the Discord HTTP client, with the right intents and a privileged token. Adding an MCP endpoint costs almost nothing in startup time and gives you a remote-control interface for free.

The protocol

Model Context Protocol is an open JSON-RPC-based protocol from Anthropic that lets clients (LLMs and IDE extensions) discover and call tools exposed by servers. The bot uses the Streamable HTTP transport, which is a simple HTTP-and-SSE flavour of MCP (no stdio handshakes, no daemon-spawning).

The server is built on the rmcp Rust SDK. Tool definitions are written as decorated async fns on a single DiscordTools struct in src/mcp/tools.rs; the #[tool(description = "...")] attribute generates the JSON schema from each function’s parameter type.

Connecting Claude Code

The simplest way to test the server is to add it to your local Claude Code config (~/.claude.json):

{
  "mcpServers": {
    "discord": {
      "type": "http",
      "url": "http://localhost:9090/mcp"
    }
  }
}

Then restart Claude Code. The next time you start a session, the discord server should appear in your tool list and you can call any of the 51 tools by name.

If you’ve set MCP_AUTH_TOKEN (see below), add the bearer token to the same entry:

{
  "mcpServers": {
    "discord": {
      "type": "http",
      "url": "http://localhost:9090/mcp",
      "headers": {
        "Authorization": "Bearer your-token-here"
      }
    }
  }
}

Other MCP-capable clients (Cursor, Continue, custom code) follow the same pattern: point them at http://<host>:<port>/mcp over HTTP and optionally pass a bearer token.

Tool catalog

The 51 tools are grouped into five categories. The full reference, including parameter schemas, lives in Reference: MCP Tool Catalog. The table below is the one-line summary.

Guilds

ToolWhat it does
list_guildsList every Discord server the bot is a member of, with names and IDs.

Server

ToolWhat it does
get_guild_infoName, owner, approximate member count, channel/role counts.
send_messagePost a message to a channel. Privileged.
delete_messagesBulk-delete the most recent N messages from a channel (1–100).
get_recent_messagesRead recent messages from a channel, newest first; supports pagination via before.
search_messagesSearch a channel by author, content substring, and time range (ISO date or snowflake). Filters compose.
add_reactionAdd a reaction (unicode or custom emoji) to a specific message.
remove_reactionRemove the bot’s own reaction from a specific message.

Channels

ToolWhat it does
list_channelsAll channels in the server with IDs, types, and positions.
create_channelCreate a text, voice, category, forum, or stage channel.
delete_channelDelete a channel.
edit_channelEdit name, topic, NSFW flag, slowmode, parent category.
move_channelMove a channel to a new position or category.
set_channel_permissionsApply permission overrides for a role or member.
create_voice_channelCreate a voice channel with optional bitrate / user_limit.
create_stage_channelCreate a stage channel (speaker/audience-separated voice).
edit_voice_channelEdit voice-specific channel fields (bitrate, user_limit, region).

Roles

ToolWhat it does
list_rolesAll roles with IDs, colours, positions, permissions.
create_roleCreate a new role.
delete_roleDelete a role.
edit_roleEdit name, colour, permissions, hoist, mentionable.

Members

ToolWhat it does
list_membersList server members (max 1000 per call, paginate with after).
get_memberDetailed info about one member.
assign_roleAdd a role to a member.
remove_roleRemove a role from a member.
ban_memberBan a user, optionally with a reason and message-history purge.
unban_memberUnban a user.
kick_memberKick a member.
timeout_memberTime out a member for a duration like 1h, 30m, 7d.
remove_timeoutLift an active timeout (inverse of timeout_member).
set_nicknameSet or clear a member’s server nickname (1–32 chars).
get_bansList active bans with id/name/reason; paginate with after.
move_voice_memberMove a member to a different voice channel.
disconnect_voice_memberDisconnect a member from voice.
modify_voice_stateServer-mute / server-deafen a member when in voice.

Direct Messages

ToolWhat it does
send_private_messageDM a user. Opens the DM channel automatically. Privileged.
read_private_messagesRead recent DMs between the bot and a user, newest first.
edit_private_messageEdit one of the bot’s previously-sent DMs.
delete_private_messageDelete one of the bot’s previously-sent DMs.

Webhooks

ToolWhat it does
list_webhooksList webhooks on a channel (id, name, token).
create_webhookCreate a webhook on a channel; returns id + token.
delete_webhookDelete a webhook by ID.
send_webhook_messageSend through a webhook with optional username/avatar overrides. Privileged.

Invites

ToolWhat it does
list_invitesList active server invites (code, channel, inviter, uses).
create_inviteCreate a new invite with optional max_age, max_uses, temporary, unique.
delete_inviteDelete an invite by code.
get_invite_detailsLook up an invite (no need for bot to be in the target guild).

Custom Emoji

ToolWhat it does
list_emojisList custom emoji in the server.
create_emojiCreate a custom emoji from an HTTPS image URL (bot fetches + base64).
edit_emojiRename a custom emoji.
delete_emojiDelete a custom emoji.

That’s 51 tools total: 1 + 7 + 9 + 4 + 14 + 4 + 4 + 4 + 4.

Multi-guild support

Almost every tool takes an optional guild_id parameter. When it’s omitted, the tool acts against the bot’s configured guild — the one named in the GUILD_ID env var. When it’s supplied, the tool acts against that guild instead, as long as the bot is a member of it.

This means you can run a single bot across several Discord servers and manage them all through one MCP endpoint. The list_guilds tool is deliberately the one that does not take a guild_id parameter — use it to discover what’s available, then pass the right ID into the follow-up calls.

Port and binding

Two environment variables control the listen address (loaded in src/config.rs):

VariableDefaultNotes
MCP_PORT9090TCP port the server listens on.
MCP_BIND_ADDR127.0.0.1Interface to bind to. The default is localhost-only, deliberately.

The defaults are chosen so that a fresh install is not exposing anything to the public internet. To make the server reachable from other machines on a private network, set MCP_BIND_ADDR=0.0.0.0 and arrange your own network ACLs. To expose it over the public internet at all, see the security section below first.

Authentication

A third environment variable controls authentication:

VariableDefaultNotes
MCP_AUTH_TOKENempty (none)Bearer token. Required unless MCP_BIND_ADDR is a loopback interface (127.0.0.1, ::1).

The middleware in src/mcp/mod.rs is a single from_fn layer:

  • If MCP_AUTH_TOKEN is empty, every request is allowed through.
  • If it is set, every request must carry a matching Authorization: Bearer <token> header, otherwise it is rejected with 401 Unauthorized.
  • The bearer-token comparison is constant-time (via subtle::ConstantTimeEq) so a network attacker can’t use response timing to probe the token byte-by-byte.

Strict startup guard

The bot refuses to start if MCP_AUTH_TOKEN is empty and MCP_BIND_ADDR is not a loopback address. This replaces the older “soft warning” behaviour — the misconfiguration that used to expose an unauthenticated MCP endpoint on the public internet now fails the boot instead, with a pointed error explaining how to fix it. Either set a token or bind to loopback; there is no third option.

The mcp-gateway applies the same rule even more strictly: because it always listens on 0.0.0.0, it refuses to start without MCP_AUTH_TOKEN set, no loopback escape hatch available. The gateway also forwards that same token on every outgoing request to its backends — one shared secret covers both the inbound check and the outbound forward — so operators configure a single value and the bundled docker-compose deploy (where backends bind 0.0.0.0:9090 and therefore require a token themselves) works without extra plumbing.

Request size limit

Every incoming request body is capped at 64 KiB. Oversize bodies are rejected with 413 Payload Too Large. Bodies that fit but aren’t valid JSON-RPC come back with a standards-conformant {"jsonrpc":"2.0","error":{"code":-32700,"message":"Parse error"},"id":null} response rather than a generic 400.

The MCP gateway

If you run multiple bot instances out of the same host (production and staging, or two community bots, etc.), each one binds its own port and exposes its own catalog. Pointing Claude Code at all of them individually means maintaining a list of URLs and switching between them.

The repository ships a separate mcp-gateway service that solves this. It listens on a single port, multiplexes requests to whichever bot’s MCP endpoint they belong to, and presents a unified tool list to clients. Each tool’s schema gains an extra instance parameter (matching a key in the INSTANCES env var, e.g. bot_a or bot_b) which the gateway uses to pick the target bot. A synthetic list_instances tool is appended to the catalog so clients can discover the available instances and their guilds.

The gateway is the recommended entry point in production — see Architecture: MCP Gateway Routing for how it routes and authenticates requests.

Security considerations

The MCP tools are powerful. ban_member permanently removes a user; delete_channel is destructive and unrecoverable; send_message lets the bot post anywhere it has Send Messages permission. There is no in-bot confirmation gate on MCP calls — a misfiring AI agent or a compromised client could do real damage in seconds.

This means the threat model is the MCP endpoint itself is privileged infrastructure. Treat it the same way you’d treat a bare-metal SSH session into your Discord server.

Concrete recommendations:

  • MCP_AUTH_TOKEN is required unless bound to loopback. The bot enforces this at startup: if MCP_BIND_ADDR is anything other than a loopback address and the token is unset, the process refuses to boot. Generate the token with openssl rand -hex 32 or similar.
  • Default to 127.0.0.1. The default MCP_BIND_ADDR is localhost for a reason. Do not change it unless you’re putting something meaningful in front of it.
  • Do not put the port on the public internet. Even with a bearer token, you’re one token leak away from a takeover. Use a WireGuard / Tailscale / SSH tunnel / VPN to reach it remotely.
  • Audit the bot’s Discord permissions. The MCP server can only do what the bot itself can do. If you grant the bot Administrator, you’ve granted Administrator to anyone holding the MCP token.
  • See Deployment: MCP Exposure for the long-form discussion and example reverse-proxy configurations.

Known limitations

  • OAuth 2.1 is not implemented. The MCP spec defines an OAuth-based flow that some clients prefer (Claude Code’s HTTP transport, for instance, expects it). The bot speaks bearer-token auth only, so you’ll see warnings in those clients about the auth mode being unrecognised — they still work, but the OAuth handshake never happens.
  • Rate limiting is Discord’s, not ours. The tools call the Discord HTTP API directly with no extra throttling. Bulk operations on members, roles, or messages are bounded by Discord’s rate limits, and a too-eager AI agent will trigger 429 responses you’ll see surfaced as Discord API error: ... in the tool output.
  • Per-call API timeout is 10 seconds. API_TIMEOUT in src/mcp/tools.rs. Long-running Discord calls (e.g. fetching a large guild’s full member list) may time out; in that case, page through with smaller limit values.
  • Discord permission errors are surfaced as plain strings, not as structured error codes. If a tool says Missing Permissions, the bot itself doesn’t have the permission needed for the operation.
  • The timeout_member tool’s reason argument is currently cosmetic — it’s part of the schema for forward compatibility, but the underlying call doesn’t yet thread it through to Discord’s audit log.

Example use cases

  • Mass channel cleanup. “List every channel in the #archive-2022 category that hasn’t had a message this year and delete them.” The AI calls list_channels, filters mentally, and invokes delete_channel for each one.
  • Role audits. “List every member with the @Donor role and cross-check against the active payment list.” Pair list_members with whatever data source you point the AI at.
  • Permissions sweep. “Make sure every channel under the #mods-only category has the @Moderator role allowed and @everyone denied.” set_channel_permissions per channel.
  • Bulk message cleanup. “Delete the last 50 messages in #spam.” One call to delete_messages.
  • Server bootstrapping. “Create a category ‘Beta’, three text channels under it, and a role with permissions limited to that category.” Several create_channel and create_role calls in sequence.

The general pattern is list-then-act: ask the AI to discover what’s there, then have it execute the change with a clear plan you can approve.

Cross-references