discord-ops
README.md
# discord-ops
Agency-grade Discord MCP server with multi-guild project routing.
[](https://github.com/bookedsolidtech/discord-ops/actions/workflows/ci.yml)
[](https://www.npmjs.com/package/discord-ops)
[](LICENSE)
## Features
- **72 MCP tools** — messaging, personas, polls, forums, notes, channels, moderation, roles, webhooks, botops, audit log, threads, guilds, invites, permissions, search, 23 templates, OG embed unfurling, project introspection
- **Project note board** — a durable, directed-note log per project; agents leave hand-offs and read them on startup, aware of other concurrent sessions
- **Webhook personas** — one bot, unlimited posting identities via per-message username/avatar overrides; never create another bot application
- **Native polls** — structured agent/human consensus with per-answer voter reads and bot flags
- **Forum work queues** — posts as tasks, tags as status labels, archive to close
- **Real-time event feed** — optional `discord-ops listen` sidecar streams messages/reactions to a local sink read by `get_events`, zero API calls per poll
- **Multi-guild project routing** — `send_message({ project: "my-app", channel: "builds" })` instead of raw channel IDs
- **Agent-to-agent coordination** — agents post tasks, peers reply and react, originators read results back via `get_replies` / `get_reactions` — Discord as a durable coordination bus
- **Notification routing** — map notification types (`ci_build`, `deploy`, `error`) to channels per project
- **Owner pings** — configure project owners so releases, errors, and alerts auto-mention the right people
- **Bot personas** — named bots with identity metadata, per-channel bot assignment, and per-bot tool profiles
- **Multi-bot support** — manage multiple Discord bots from a single MCP server with per-project tokens
- **Tool profiles** — load only the tools an agent needs; cut schema overhead by 85% with slim profiles
- **Smart channel resolution** — channel params accept channel name or snowflake ID, with 4-layer fuzzy fallback
- **Auto-embed for send_message** — every message gets a branded embed by default; `raw: true` for plain text
- **OG metadata unfurling** — `send_embed` fetches Open Graph metadata server-side and renders rich link previews
- **Flexible token configuration** — configurable default token env var, optional default when all projects use per-project tokens
- **Config validation** — `discord-ops validate` detects duplicate guilds, missing tokens, invalid channel refs without connecting to Discord
- **HTTP/SSE + stdio transports** — stdio for Claude Code, HTTP/SSE for remote MCP clients
- **HTTP transport auth** — bearer token auth via `DISCORD_OPS_HTTP_TOKEN` with constant-time comparison
- **Dry-run mode** — simulate destructive operations without calling Discord API
- **Interactive setup wizard** — `discord-ops setup` discovers guilds/channels live, configures single/multi-bot routing and the coordination board, and writes a project `.mcp.json` for one-command install
- **Security hardening** — rate limiting, permission pre-flight checks, snowflake ID validation, self-protection guards
- **Lazy login** — tools enumerate before Discord connects; first tool call triggers login
- **Zod validation** — all inputs validated before execution
- **Error sanitization** — tokens, webhook URLs, and snowflake IDs stripped from error output
- **Audit logging** — every tool call logged to stderr
## Quick Start
```bash
# Install
npm install -g discord-ops
# Interactive setup — connects to your bot, discovers guilds/channels, and
# writes ~/.discord-ops.json + a project .mcp.json (so Claude Code loads it)
discord-ops setup
# Or manual setup
export DISCORD_TOKEN="your-bot-token"
discord-ops health
# Start MCP server (stdio)
discord-ops
# Start MCP server (HTTP/SSE)
discord-ops serve --port 3000
```
## Claude Code Integration
Add to your project's `.mcp.json`. Use `npx` with `@latest` so every session automatically uses the latest published release — without it, npx may serve a stale cached version indefinitely:
```json
{
"mcpServers": {
"discord-ops": {
"command": "npx",
"args": ["-y", "discord-ops@latest"],
"env": {
"DISCORD_TOKEN": "${DISCORD_TOKEN}"
}
}
}
}
```
The `${VAR}` syntax is Claude Code's native env var interpolation — it reads the value from your shell environment at startup. Export your bot token in `~/.zshrc` (or `.bashrc`) and it will be available to all projects without hardcoding it in any file.
### Multi-org setup (per-project tokens)
When each project has its own bot, pass all token env vars and let `~/.discord-ops.json` handle which project uses which:
```json
{
"mcpServers": {
"discord-ops": {
"command": "npx",
"args": ["-y", "discord-ops@latest"],
"env": {
"ORG_A_DISCORD_TOKEN": "${ORG_A_DISCORD_TOKEN}",
"ORG_B_DISCORD_TOKEN": "${ORG_B_DISCORD_TOKEN}"
}
}
}
}
```
Each project in `~/.discord-ops.json` declares `"token_env": "ORG_A_DISCORD_TOKEN"` and discord-ops routes automatically. No default `DISCORD_TOKEN` needed when all projects have `token_env` set.
### Single-org shorthand
If all your projects share one bot, just pass that token:
```json
{
"mcpServers": {
"discord-ops": {
"command": "npx",
"args": ["-y", "discord-ops@latest"],
"env": {
"DISCORD_TOKEN": "${DISCORD_TOKEN}"
}
}
}
}
```
### Custom token env var name
If another tool already claims `DISCORD_TOKEN`, use `DISCORD_OPS_TOKEN_ENV` to point at a different name:
```json
{
"mcpServers": {
"discord-ops": {
"command": "npx",
"args": ["-y", "discord-ops@latest"],
"env": {
"DISCORD_OPS_TOKEN_ENV": "MY_BOT_TOKEN",
"MY_BOT_TOKEN": "${MY_BOT_TOKEN}"
}
}
}
}
```
## Project Routing
The killer feature: route messages by project name and channel alias instead of raw IDs.
### Global config (`~/.discord-ops.json`)
```json
{
"projects": {
"my-app": {
"guild_id": "123456789012345678",
"channels": {
"dev": "CHANNEL_ID",
"builds": "CHANNEL_ID",
"alerts": "CHANNEL_ID",
"releases": "CHANNEL_ID"
},
"default_channel": "dev"
}
},
"default_project": "my-app",
"notification_routing": {
"ci_build": "builds",
"deploy": "builds",
"release": "releases",
"error": "alerts",
"dev": "dev"
}
}
```
### Per-project bot tokens
Projects can specify their own bot token via `token_env`:
```json
{
"projects": {
"org-a": {
"guild_id": "111111111111111111",
"channels": { "dev": "CHANNEL_ID" },
"default_channel": "dev",
"token_env": "ORG_A_DISCORD_TOKEN"
},
"org-b": {
"guild_id": "222222222222222222",
"channels": { "dev": "CHANNEL_ID" },
"default_channel": "dev",
"token_env": "ORG_B_DISCORD_TOKEN"
}
}
}
```
When all projects have `token_env`, the default `DISCORD_TOKEN` is optional. Each project connects with its own bot.
### Bot personas
Give bots names, roles, and per-channel assignment. This is ideal when your Discord server runs multiple bots with distinct personas (e.g., a community helper vs. a tech ops bot).
```json
{
"bots": {
"claire": {
"name": "Claire",
"role": "Community helper",
"description": "Handles support and community channels",
"token_env": "CLAIRE_TOKEN",
"default_profile": "messaging"
},
"courier": {
"name": "Clarity Courier",
"role": "Technical operations",
"token_env": "COURIER_TOKEN",
"default_profile": "full",
"profile_add": ["send_template"],
"profile_remove": []
}
},
"projects": {
"clarity-house": {
"guild_id": "123456789012345678",
"bot": "courier",
"channels": {
"general": "111111111111111111",
"support": { "id": "222222222222222222", "bot": "claire" },
"dev-ops": "333333333333333333",
"ai-testing": { "id": "444444444444444444", "bot": "claire" }
},
"default_channel": "dev-ops",
"tool_profile": "full"
}
}
}
```
**How it works:**
- **`bots`** — named bot definitions with identity metadata and `token_env`
- **`project.bot`** — default bot for the project (all channels use this bot unless overridden)
- **Channel `bot` override** — individual channels can use a different bot: `{ "id": "...", "bot": "claire" }`
- **`default_profile`** — per-bot tool profile (restricts which tools a bot can use at runtime)
- **`profile_add`** — additional tools to load on top of the bot's `default_profile`
- **`profile_remove`** — tools to exclude from the bot's `default_profile`
- **Token resolution** — channel bot → project bot → project `token_env` → default `DISCORD_TOKEN`
- **Bot persona in routing** — resolved targets include `bot: { name, role }` metadata for agent context
- **Backwards compatible** — `bots` is optional; channels accept both `"ID"` and `{ "id": "ID", "bot": "name" }` formats
Use `list_bots` to see all configured bots, their project assignments, channel overrides, and connection status.
### Owner pings
Configure project owners so that releases, errors, and alerts automatically prepend `@mentions`. This ensures the right people are always paged for critical events without hardcoding mentions in every message.
```json
{
"projects": {
"my-app": {
"guild_id": "123456789012345678",
"channels": { "releases": "CHANNEL_ID", "alerts": "CHANNEL_ID" },
"owners": ["820027414902079548"],
"notify_owners_on": ["release", "error", "alert"]
}
}
}
```
**`notify_owners_on` values:** any notification type (`release`, `error`, `alert`, `ci_build`, `deploy`, etc.)
**Safety:** `"dev"` is hardcoded to never trigger owner pings regardless of config — dev noise stays quiet.
When a `send_message` or `send_embed` call uses a matching `notification_type`, the owner mentions are automatically prepended to the message. No other changes needed.
### Smart channel resolution
The `channel` param accepts a channel name or snowflake ID anywhere a channel is needed. Resolution happens in four layers:
1. **Exact alias match** — `"builds"` hits the `builds` alias in your project config
2. **Fuzzy alias match** — `"build"` or `"blds"` resolves to the closest alias
3. **Live Discord API lookup** — `"general"` resolves even with no configured alias
4. **Error** — if none of the above find a match
This means you can pass `channel: "general"` and it will work even for channels that aren't in your config. You can also pass a raw snowflake ID directly — `channel: "1234567890"` bypasses alias resolution entirely.
### Per-project config (`.discord-ops.json` in repo root)
```json
{
"project": "my-app",
"notification_routing": {
"ci_build": "builds",
"deploy": "builds"
}
}
```
### Usage
```
# By project + channel alias
send_message({ project: "my-app", channel: "builds", content: "Build passed!" })
# By notification type (auto-routed to channel, owner pinged if configured)
send_message({ project: "my-app", notification_type: "release", content: "v1.0.0 shipped" })
# Direct channel ID (always works)
send_message({ channel_id: "123456789", content: "Hello" })
# Channel by name (live lookup — no alias needed)
send_message({ project: "my-app", channel: "general", content: "Hello" })
```
## Messaging
### Auto-embed
`send_message` automatically wraps messages in a polished embed with a color bar, description, and timestamp. Pass `raw: true` to send plain text instead.
```
# Branded embed (default)
send_message({ project: "my-app", channel: "dev", content: "Deploy complete" })
# Plain text
send_message({ project: "my-app", channel: "dev", content: "pong", raw: true })
```
### send_embed — OG metadata unfurling
`send_embed` fetches Open Graph metadata server-side from any URL and renders a rich preview embed. All OG fields can be overridden.
```
send_embed({
url: "https://www.npmjs.com/package/discord-ops/v/0.23.0",
project: "my-app",
channel: "releases",
title: "discord-ops v0.23.0",
description: "Owner pings, smart channel resolution, category editing",
color: 5763719,
footer: "Released April 3, 2026"
})
```
Useful for sharing GitHub PRs, npm releases, blog posts, or any URL with rich previews — the bot fetches the metadata so Discord's CDN doesn't cache-bust client-side unfurls.
## Agent-to-Agent Coordination
discord-ops doubles as a coordination bus for multi-agent systems. One agent posts a task as a message, peer agents (or humans) reply and react, and the originating agent reads the replies and reactions back. Because the exchange lives in Discord, it's durable across sessions, works between agent processes that share no memory, and every step is observable by the humans in the server.
The loop is four tool calls:
```
# Agent A posts a task and records the returned message ID
send_message({ project: "my-app", channel: "engineering", content: "TASK: verify the staging deploy", raw: true })
→ { "id": "333333333333333333" }
# Agent B claims it with a reaction, then replies
add_reaction({ project: "my-app", channel: "engineering", message_id: "333333333333333333", emoji: "👀" })
send_message({ project: "my-app", channel: "engineering", reply_to: "333333333333333333", content: "RESULT: verified, all checks green", raw: true })
# Agent A collects results at its next task boundary
get_replies({ project: "my-app", channel: "engineering", message_id: "333333333333333333" })
get_reactions({ project: "my-app", channel: "engineering", message_id: "333333333333333333" })
```
Three read-side tools close the loop: `get_message` fetches a single message with its reply, thread, and reaction state; `get_reactions` reports who reacted with what; `get_replies` collects replies to a specific message. `get_messages` includes `reply_to` on every message, so scanning agents can reconstruct conversation structure. For exchanges longer than one round trip, `create_thread` from the original message keeps the channel clean.
See [docs/agent-coordination.md](docs/agent-coordination.md) for the full protocol — reaction vocabulary, threads, polling guidance, multi-project setups, and anti-patterns. New to discord-ops? [docs/project-onboarding.md](docs/project-onboarding.md) is the start-to-finish walkthrough for adopting it as a team's coordination bus — project setup, the shared note board, personas, polls and forums, and least-privilege profiles.
## Tools
### Messaging (16 tools)
| Tool | Description |
| ----------------- | ----------------------------------------------------------- |
| `send_message` | Send a message with project routing (auto-embed by default) |
| `send_embed` | Fetch OG metadata from a URL and post a rich embed |
| `get_messages` | Fetch recent messages (supports ISO 8601 timestamps) |
| `get_message` | Fetch one message with reply, thread, and reaction state |
| `get_reactions` | List reactions on a message with per-user detail |
| `get_replies` | Collect replies to a specific message |
| `edit_message` | Edit a bot message |
| `delete_message` | Delete a message |
| `add_reaction` | React to a message |
| `pin_message` | Pin a message in a channel |
| `unpin_message` | Unpin a message |
| `search_messages` | Search messages by content, author, or date range |
| `send_template` | Send a styled embed using a built-in template |
| `list_templates` | List available templates with required variables |
| `notify_owners` | Ping project owners based on notification type |
| `forward_message` | Forward a message snapshot to another channel |
### Personas (3 tools)
One bot, unlimited posting identities — persona identity is a per-message webhook username/avatar override, so no new Discord bots or developer-portal work are ever needed.
| Tool | Description |
| ---------------- | -------------------------------------------------------------------- |
| `create_persona` | Ensure a channel has a persona-capable webhook (token never exposed) |
| `send_as` | Post as any persona name/avatar; returns the message id |
| `list_personas` | List persona webhooks per channel or guild |
### Polls (3 tools)
| Tool | Description |
| ------------------ | --------------------------------------------------------------- |
| `send_poll` | Create a native poll (2-10 answers, multiselect, up to 32 days) |
| `get_poll_results` | Read tallies and per-answer voters with bot flags |
| `end_poll` | Immediately finalize a poll (idempotent) |
### Forums (3 tools)
Forum channels as durable work queues: a post is a task, tags are status labels, archiving closes it.
| Tool | Description |
| ------------------- | -------------------------------------------------- |
| `create_forum_post` | Create a tagged forum post with an initial message |
| `list_forum_posts` | List posts, filterable by tag name |
| `update_forum_post` | Replace tags and/or archive (close) a post |
### Botops (5 tools)
Manage the bot application itself via API — no developer portal round-trips. (Creating _new_ bot applications remains portal-only by Discord's design; use personas instead.)
| Tool | Description |
| -------------------- | ------------------------------------------------------------------- |
| `set_bot_nick` | Set or clear the bot's per-guild nickname |
| `update_application` | Edit description, icon, tags, and install params of the current app |
| `list_app_emojis` | List application-owned emojis |
| `create_app_emoji` | Upload an app emoji; returns an `add_reaction`-ready identifier |
| `delete_app_emoji` | Delete an app emoji |
### Channels (9 tools)
| Tool | Description |
| ----------------- | ----------------------------------------------------------------------------- |
| `list_channels` | List guild channels |
| `get_channel` | Get channel details |
| `create_channel` | Create a channel |
| `edit_channel` | Edit channel name, topic, category, or position (text, voice, and categories) |
| `delete_channel` | Delete a channel |
| `purge_messages` | Bulk-delete messages (max 100, < 14 days old) |
| `set_slowmode` | Set or disable slowmode |
| `move_channel` | Move a channel to a different category or position |
| `set_permissions` | Set channel permission overrides for a role or member |
### Moderation (4 tools)
| Tool | Description |
| ---------------- | -------------------------- |
| `kick_member` | Kick a member from a guild |
| `ban_member` | Ban a user from a guild |
| `unban_member` | Unban a user |
| `timeout_member` | Timeout (mute) a member |
### Roles (5 tools)
| Tool | Description |
| ------------- | ---------------------------------- |
| `list_roles` | List guild roles |
| `create_role` | Create a new role |
| `edit_role` | Edit role properties |
| `delete_role` | Delete a role |
| `assign_role` | Add or remove a role from a member |
### Webhooks (6 tools)
| Tool | Description |
| ----------------- | ------------------------------------ |
| `create_webhook` | Create a webhook on a channel |
| `get_webhook` | Get webhook details |
| `list_webhooks` | List webhooks for a guild or channel |
| `edit_webhook` | Edit webhook properties |
| `delete_webhook` | Delete a webhook |
| `execute_webhook` | Send a message via webhook |
### Audit (1 tool)
| Tool | Description |
| ----------------- | ---------------------------------- |
| `query_audit_log` | Query guild audit log with filters |
### Guilds & Members (6 tools)
| Tool | Description |
| --------------- | ----------------------------------- |
| `list_guilds` | List bot's guilds |
| `get_guild` | Get guild details |
| `get_invites` | Get all active invites for a guild |
| `create_invite` | Create an invite link for a channel |
| `list_members` | List guild members |
| `get_member` | Get member details |
### Threads (3 tools)
| Tool | Description |
| ---------------- | -------------------------------------- |
| `create_thread` | Create a thread |
| `list_threads` | List active threads |
| `archive_thread` | Archive (and optionally lock) a thread |
### Notes (4 tools)
A durable, directed-note log — one board channel per project — that agents read on startup and use for hand-offs across concurrent sessions. See [docs/project-onboarding.md](docs/project-onboarding.md).
| Tool | Description |
| --------------- | ----------------------------------------------------------------- |
| `leave_note` | Leave a directed note (to a session, role, or `all`) on the board |
| `get_notes` | Read the board, filtered by recipient, sender, tag, or unresolved |
| `resolve_note` | Mark a note handled (✅) with an optional reply |
| `list_sessions` | See which sessions are active on a project, from note activity |
### System (4 tools)
| Tool | Description |
| --------------- | -------------------------------------------------------------------- |
| `health_check` | Bot status, version, connected guilds, and permission audit |
| `list_projects` | List all projects with guild mappings, token status, and validation |
| `list_bots` | List all bot personas with project assignments and channel overrides |
| `get_events` | Read the local real-time event feed written by `discord-ops listen` |
## Tool Profiles
Load only the tools an agent needs. Reduces schema token overhead by up to 85% for narrow use cases.
### Built-in profiles
| Profile | Tools | Description |
| ------------ | ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `full` | 72 | All tools (default) |
| `monitoring` | 17 | get_messages, get_message, get_replies, get_reactions, send_message, send_as, add_reaction, create_thread, send_poll, get_poll_results, leave_note, get_notes, resolve_note, list_sessions, health_check, list_projects, list_bots |
| `readonly` | 13 | get_messages, get_message, get_replies, get_reactions, get_poll_results, list_personas, list_forum_posts, list_channels, list_members, get_guild, health_check, list_projects, list_bots |
| `moderation` | 7 | get_messages, kick_member, ban_member, timeout_member, delete_message, purge_messages, query_audit_log |
| `messaging` | 10 | add_reaction, delete_message, edit_message, forward_message, get_messages, get_message, get_replies, get_reactions, send_as, send_message |
| `channels` | 7 | create_channel, delete_channel, edit_channel, get_channel, list_channels, purge_messages, set_slowmode |
| `webhooks` | 6 | create_webhook, delete_webhook, edit_webhook, execute_webhook, get_webhook, list_webhooks |
### Using profiles
```bash
# Via CLI flag
discord-ops --profile monitoring
# Load specific tools
discord-ops --tools "send_message,send_template,health_check"
# Combined (profile as base + add tools)
discord-ops --profile readonly --tools "send_message"
```
### Per-project profile config
Profiles can be set per project in `~/.discord-ops.json` so each agent gets only what it needs:
```json
{
"projects": {
"my-app": {
"guild_id": "123456789012345678",
"channels": { "dev": "CHANNEL_ID", "alerts": "CHANNEL_ID" },
"tool_profile": "monitoring",
"profile_add": ["send_message"],
"profile_remove": ["list_members"]
}
}
}
```
- **`tool_profile`** — base profile to use for this project
- **`profile_add`** — add tools not included in the base profile
- **`profile_remove`** — remove tools from the base profile
## CLI
```
discord-ops Start MCP server (stdio transport)
discord-ops serve Start MCP server (HTTP/SSE transport)
discord-ops run <tool> --args '{…}' Run any tool directly (no AI/MCP required)
discord-ops init Scaffold a per-project .discord-ops.json
discord-ops setup Interactive setup wizard (single + multi-bot)
discord-ops health Run health check + permission audit
discord-ops validate Validate config without connecting to Discord
discord-ops --profile Load a built-in tool profile (monitoring/readonly/moderation/full)
discord-ops --tools Load specific tools by name (comma-separated)
discord-ops --dry-run Simulate destructive operations
discord-ops --help Show help
discord-ops --version Show version
```
### `run` — call any tool without an AI agent
The `run` subcommand executes any discord-ops tool directly from the shell — no MCP client, no AI. Pass all tool input as a single JSON string via `--args`.
```bash
# Send a plain message
npx discord-ops@latest run send_message \
--args '{"project":"my-app","channel":"general","content":"Deployment complete."}'
# Send a rich release announcement
npx discord-ops@latest run send_template \
--args '{
"project": "my-app",
"channel": "releases",
"template": "release",
"vars": {
"name": "my-app",
"version": "v1.2.0",
"author_name": "My Org",
"highlights": "• New feature A\n• Bug fix B",
"npm": "my-app@latest",
"npm_url": "https://www.npmjs.com/package/my-app",
"link": "https://github.com/my-org/my-app/releases/tag/v1.2.0",
"footer": "Published 2026-04-03"
}
}'
```
Any tool name accepted by the MCP server works here — `send_message`, `send_template`, `send_embed`, `list_channels`, etc. The same input schema applies; validation errors are printed with field paths and exit code 1.
### `init` — scaffold a per-project config
The `init` subcommand creates a `.discord-ops.json` file in the current directory for per-project configuration.
```bash
discord-ops init --project my-app --guild-id 123456789012345678
# With channels and custom token env var
discord-ops init --project my-app --guild-id 123456789012345678 \
--token-env MY_APP_TOKEN \
--channel builds=987654321098765432 \
--channel releases=111222333444555666
# Overwrite existing and mark as default project
discord-ops init --project my-app --guild-id 123456789012345678 --force --default
```
**Flags:**
| Flag | Required | Description |
| ------------------------ | -------- | ---------------------------------------------------- |
| `--project <name>` | Yes | Project name |
| `--guild-id <snowflake>` | Yes | Discord guild/server snowflake ID |
| `--token-env <VAR>` | No | Env var for bot token (default: `DISCORD_TOKEN`) |
| `--channel <alias>=<id>` | No | Channel alias mapping, repeatable |
| `--force` | No | Overwrite existing `.discord-ops.json` |
| `--default` | No | Mark this project as `default_project` in the config |
### `serve` flags
The `serve` subcommand accepts additional flags for HTTP transport configuration:
| Flag | Description |
| --------------------------- | ---------------------------------------------------------------------------- |
| `--port <port>` | HTTP port (default: 3000) |
| `--allowed-origin <origin>` | Allowed CORS origin (default: `http://localhost`) |
| `--allow-unauthenticated` | Start without requiring `DISCORD_OPS_HTTP_TOKEN` (insecure, shows a warning) |
## Environment Variables
| Variable | Required | Description |
| ------------------------ | -------- | ---------------------------------------------------------------------------------- |
| `DISCORD_TOKEN` | No\* | Default Discord bot token (\*required unless all projects have `token_env`) |
| `DISCORD_OPS_TOKEN_ENV` | No | Override which env var holds the default token (default: `DISCORD_TOKEN`) |
| `<PROJECT>_TOKEN` | No | Per-project bot tokens (configured via `token_env` in project config) |
| `DISCORD_OPS_CONFIG` | No | Path to global config file, or inline JSON string (default: `~/.discord-ops.json`) |
| `DISCORD_OPS_LOG_LEVEL` | No | `debug`, `info`, `warn`, `error` (default: `info`) |
| `DISCORD_OPS_DRY_RUN` | No | Enable dry-run mode (any truthy value) |
| `DRY_RUN` | No | Enable dry-run mode (any truthy value, alias) |
| `DISCORD_OPS_HTTP_TOKEN` | No | Bearer token for HTTP transport authentication (strongly recommended) |
### Token resolution
1. **Channel-level bot** — if the channel has a `bot` override, use that bot's `token_env`
2. **Project-level bot** — if the project has a `bot`, use that bot's `token_env`
3. **Project-level `token_env`** — project's own token env var
4. **Default token** — `DISCORD_TOKEN` (or custom via `DISCORD_OPS_TOKEN_ENV`)
If `DISCORD_OPS_TOKEN_ENV` is set, its value names the env var holding the default token (e.g., `DISCORD_OPS_TOKEN_ENV=MY_BOT_TOKEN` reads `MY_BOT_TOKEN`). If all projects have `token_env` or `bot` set, no default token is needed.
## CI/CD Integration
Use `discord-ops run` in GitHub Actions (or any CI) to post rich Discord notifications after a publish, deploy, or build — no AI agent required.
### Config shape for CI
In CI you typically have one bot token and one project. Pass a minimal config as an inline JSON string via `DISCORD_OPS_CONFIG`. No file writing needed.
**When storing as a GitHub secret, minify to a single line** — multiline strings break secret injection. The shape (pretty-printed for readability):
```json
{
"projects": {
"my-app": {
"guild_id": "123456789012345678",
"channels": {
"releases": "987654321098765432",
"builds": "111222333444555666"
},
"default_channel": "releases"
}
},
"default_project": "my-app"
}
```
- No `token_env` needed — omitting it means the project uses `DISCORD_TOKEN` (the default).
- `owners` and `notify_owners_on` are optional — include them if you want owner pings on errors.
- Channel values are Discord snowflake IDs. Channel names (aliases) resolve to these IDs.
### GitHub Actions example
Store two secrets in your repo:
- `BOOKED_DISCORD_BOT_TOKEN` — your bot token
- `DISCORD_OPS_CONFIG` — the config JSON **minified to a single line** (multiline strings break GitHub secrets)
```
{"projects":{"my-app":{"guild_id":"123456789012345678","channels":{"releases":"987654321098765432"},"default_channel":"releases"}},"default_project":"my-app"}
```
```yaml
- name: Notify Discord
run: |
npx discord-ops@latest run send_template --args '{
"project": "my-app",
"channel": "releases",
"template": "release",
"vars": {
"name": "my-app",
"version": "${{ steps.version.outputs.version }}",
"author_name": "My Org",
"highlights": "${{ steps.changelog.outputs.highlights }}",
"npm": "my-app@latest",
"npm_url": "https://www.npmjs.com/package/my-app",
"link": "${{ steps.release.outputs.url }}",
"footer": "Published ${{ steps.date.outputs.date }}"
}
}'
env:
DISCORD_TOKEN: ${{ secrets.BOOKED_DISCORD_BOT_TOKEN }}
DISCORD_OPS_CONFIG: ${{ secrets.DISCORD_OPS_CONFIG }}
```
`DISCORD_TOKEN` is the default token variable — no additional configuration needed. The bot token from your secret is used directly.
## HTTP Transport Security
When running `discord-ops serve`, the HTTP endpoint is unauthenticated by default with a loud startup warning. Set `DISCORD_OPS_HTTP_TOKEN` to require bearer auth:
```bash
DISCORD_OPS_HTTP_TOKEN=your-secret-token discord-ops serve --port 3000
```
All requests must include:
```
Authorization: Bearer your-secret-token
```
The health endpoint (`GET /health`) is always exempt from auth — load balancers and Docker healthchecks can reach it without a token.
Token comparison uses constant-time comparison to prevent timing attacks.
## Dry-Run Mode
Enable dry-run to simulate destructive operations (delete, ban, kick, etc.) without actually calling the Discord API:
```bash
# Via CLI flag
discord-ops --dry-run
# Via environment variable
DISCORD_OPS_DRY_RUN=1 discord-ops
# Via env alias
DRY_RUN=true discord-ops
```
In dry-run mode, destructive tools return a simulated success response showing what would have happened.
## Message Templates
23 built-in templates with cutting-edge Discord features. Use `send_template` with project routing.
**Features across all templates:**
- **Author branding** — every template has a configurable `author_name` + `author_icon` at the top
- **Link buttons** — clickable buttons below embeds (View Logs, Open PR, Runbook, etc.)
- **Discord timestamps** — dates auto-convert to each user's timezone with live countdowns
- **Native polls** — real Discord polls with progress bars and vote tracking
- **Multi-embed dashboards** — up to 10 embeds per message for service status boards
- **Footer icons** — status indicator icons (green/red) next to footer text
- **Clickable titles** — embed titles link directly to URLs
- **Syntax-highlighted code** — code examples with language-specific highlighting
- **Progress bars** — visual Unicode block progress indicators
### DevOps Templates (11)
| Template | Description | Key Features |
| ------------------- | ---------------------------------------------- | -------------------------------------------- |
| `release` | Version release with install + link buttons | Author, link buttons, clickable title |
| `deploy` | Deploy success/failure with logs button | Footer icon, view/logs buttons |
| `ci_build` | CI result with build link button | Footer icon, clickable title |
| `incident` | Incident alert with severity colors | Discord timestamps, status page button |
| `incident_resolved` | Resolution with postmortem button | Discord timestamps, postmortem link |
| `maintenance` | Maintenance with live timezone countdowns | Discord timestamps, countdown, status button |
| `status_update` | Service status (operational/degraded/outage) | Footer icon, dashboard button |
| `review` | PR review with diff stats + PR button | Clickable title, additions/deletions |
| `dashboard` | Multi-embed service status board (up to 9 svc) | Multi-embed, per-service color cards |
| `oncall` | On-call handoff with shift timestamps | Discord timestamps, runbook button |
| `alert` | Configurable alert (info/warn/error/critical) | Level-based colors, metric thresholds |
### Team & Community Templates (12)
| Template | Description | Key Features |
| -------------- | --------------------------------------- | --------------------------------------------- |
| `celebration` | Celebrate wins with images | Author, thumbnail, image |
| `welcome` | Welcome members with onboarding buttons | Discord timestamps, handbook/onboarding links |
| `shoutout` | Recognize work with avatar thumbnail | Thumbnail, nomination attribution |
| `quote` | Block-quoted inspirational text | Block quote formatting, author avatar |
| `announcement` | Announcement with deadline countdown | Discord timestamps, countdown, link button |
| `changelog` | Changelog with 7 section types | Deprecated, performance, security sections |
| `milestone` | Milestone with target date countdown | Discord timestamps, progress tracking |
| `tip` | Pro tip with syntax-highlighted code | Language-specific code blocks, doc button |
| `poll` | Native Discord poll with vote tracking | Native poll API, multiselect, duration |
| `progress` | Visual progress bar with deadline | Unicode progress bar, countdown |
| `standup` | Daily standup summary | Yesterday/today/blockers sections |
| `retro` | Sprint retrospective | Went-well/improve/actions, velocity |
### Examples
**Release announcement:**
```
send_template({
template: "release",
vars: {
version: "v0.23.0",
name: "discord-ops",
highlights: "• Owner pings\n• Smart channel resolution\n• Category channel editing",
npm: "npm install discord-ops@0.23.0",
npm_url: "https://www.npmjs.com/package/discord-ops/v/0.23.0",
link: "https://github.com/bookedsolidtech/discord-ops/pull/20",
footer: "Released April 3, 2026",
author_name: "Booked Solid Technology"
},
project: "my-app",
channel: "releases"
})
```
**Native Discord Poll:**
```
send_template({
template: "poll",
vars: {
question: "Best language for MCP servers?",
options: "TypeScript|Rust|Go|Python",
duration: "48",
multiselect: "true"
},
project: "my-app",
channel: "dev"
})
```
**Multi-Embed Status Dashboard:**
```
send_template({
template: "dashboard",
vars: {
services: "API|Database|CDN|Auth|Queue",
statuses: "operational|operational|degraded|operational|outage",
title: "Production Status",
url: "https://status.example.com"
},
project: "my-app",
channel: "alerts"
})
```
**On-call handoff:**
```
send_template({
template: "oncall",
vars: {
outgoing: "alice",
incoming: "bob",
shift_start: "2026-04-04T09:00:00Z",
notes: "Payment service latency elevated — watch grafana/d/payments",
active_incidents: "INC-342: elevated error rate on /checkout",
runbook_url: "https://wiki.example.com/oncall",
mention: "<@BOB_USER_ID>"
},
project: "my-app",
channel: "team-chat"
})
```
All templates support project routing (`project`, `channel`, `notification_type`, `channel_id`) and author branding (`author_name`, `author_icon`).
## Advanced Config Reference
Full `~/.discord-ops.json` schema with all options:
```json
{
"bots": {
"my-bot": {
"name": "My Bot",
"role": "General purpose",
"description": "Handles all operations",
"token_env": "MY_BOT_TOKEN",
"default_profile": "full",
"profile_add": [],
"profile_remove": []
}
},
"projects": {
"my-app": {
"guild_id": "123456789012345678",
"token_env": "MY_APP_DISCORD_TOKEN",
"bot": "my-bot",
"channels": {
"dev": "CHANNEL_ID",
"builds": "CHANNEL_ID",
"releases": "CHANNEL_ID",
"alerts": "CHANNEL_ID",
"support": { "id": "CHANNEL_ID", "bot": "my-bot" }
},
"default_channel": "dev",
"owners": ["USER_SNOWFLAKE_ID"],
"notify_owners_on": ["release", "error", "alert"],
"tool_profile": "full",
"profile_add": [],
"profile_remove": []
}
},
"default_project": "my-app",
"notification_routing": {
"ci_build": "builds",
"deploy": "builds",
"release": "releases",
"error": "alerts",
"dev": "dev"
}
}
```
**Global fields:**
| Field | Description |
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `bots` | Named bot personas with `name`, `role`, `description`, `token_env`, `default_profile`, `profile_add`, `profile_remove` |
| `default_project` | Project used when no `project` param is provided |
| `notification_routing` | Global notification type → channel alias routing |
**Project fields:**
| Field | Description |
| ---------------------- | ------------------------------------------------------------------------------------- |
| `guild_id` | Discord server (guild) snowflake ID |
| `token_env` | Env var name for this project's bot token |
| `bot` | Default bot persona for this project (references a key in `bots`) |
| `channels` | Alias → channel ID or `{ id, bot }` map; `channel: "builds"` resolves here first |
| `default_channel` | Channel used when no `channel` param is provided |
| `owners` | User snowflake IDs to mention on matching notification types |
| `notify_owners_on` | Notification types that trigger owner pings (`"dev"` never pings) |
| `tool_profile` | Base tool profile for this project (`full`, `monitoring`, etc.) — enforced at runtime |
| `profile_add` | Additional tools to load on top of the base profile |
| `profile_remove` | Tools to exclude from the base profile |
| `notification_routing` | Per-repo config only (`.discord-ops.json`), not a project field in global config |
Per-project profiles are enforced at runtime — all tools stay registered on the MCP server, but tool calls are filtered when the resolved project or bot has a profile set. This means agents can discover all tools via MCP schema, but per-project restrictions are applied on each call.
## Multi-Organization Troubleshooting
### Validating your config
Run `discord-ops validate` to check your config without connecting to Discord. It detects:
- Missing `token_env` values (env var not set)
- Bot references (`project.bot`, channel `bot`) pointing to undefined bots
- Invalid `default_profile` or `tool_profile` values
- Missing bot `token_env` environment variables
- Duplicate guild IDs across projects with different tokens
- `default_channel` referencing a nonexistent alias
- `default_project` pointing to a nonexistent project
- Notification routing to nonexistent channel aliases
### Common issues
**"No token available for project X"**
The project needs a token. Either:
- Set its `token_env` env var (e.g., `export ORG_A_TOKEN=...`)
- Set a default token via `DISCORD_TOKEN`
- Use `DISCORD_OPS_TOKEN_ENV` to point at a custom env var
**Bot can't access a guild**
If a project uses `token_env` for a different bot, that bot must be invited to the project's guild. Run `discord-ops health` to see which guilds each bot can access.
**Migrating from single-bot to multi-bot**
1. Add `token_env` to projects that need their own bot
2. Set the corresponding env vars
3. Run `discord-ops validate` to verify
4. Run `discord-ops health` to test connections
**Token rotation**
Update the env var value and restart the MCP server. No config changes needed — `token_env` reads from the environment at runtime.
**Channel not found**
Channel resolution tries 4 layers in order: exact alias → fuzzy alias → live Discord name lookup → error. If a channel is still not found, verify the bot has access to the channel and `list_channels` returns it.
## Development
```bash
git clone https://github.com/bookedsolidtech/discord-ops.git
cd discord-ops
npm install
npm run build
npm test
# Local CI
./scripts/act-ci.sh --local
```
## License
MIT
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessUnresponsive