Package Exports
- discord-ops
- discord-ops/client
- discord-ops/config
- discord-ops/tools
Readme
discord-ops
Agency-grade Discord MCP server with multi-guild project routing.
Features
- 46 MCP tools — messaging, channels, moderation, roles, webhooks, audit log, threads, guilds, invites, permissions, search, 23 templates, OG embed unfurling, project introspection
- Multi-guild project routing —
send_message({ project: "my-app", channel: "builds" })instead of raw channel IDs - 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
- 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: truefor plain text - OG metadata unfurling —
send_embedfetches 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 validatedetects 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_TOKENwith constant-time comparison - Dry-run mode — simulate destructive operations without calling Discord API
- Interactive setup wizard —
discord-ops setupsupports single-bot and multi-bot configuration - 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
# Install
npm install -g discord-ops
# Interactive setup (creates ~/.discord-ops.json)
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 3000Claude 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:
{
"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:
{
"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:
{
"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:
{
"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)
{
"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:
{
"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.
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.
{
"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:
- Exact alias match —
"builds"hits thebuildsalias in your project config - Fuzzy alias match —
"build"or"blds"resolves to the closest alias - Live Discord API lookup —
"general"resolves even with no configured alias - 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)
{
"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.14.0",
project: "my-app",
channel: "releases",
title: "discord-ops v0.14.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.
Tools
Messaging (11 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) |
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 |
Channels (8 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 |
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 |
System (2 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 |
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 |
|---|---|---|
monitoring |
6 | health_check, list_guilds, get_guild, get_messages, list_channels, list_members |
readonly |
6 | get_messages, get_channel, get_guild, get_member, list_roles, search_messages |
moderation |
7 | kick_member, ban_member, unban_member, timeout_member, purge_messages, get_member, query_audit_log |
full |
46 | All tools (default) |
Using profiles
# 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:
{
"projects": {
"my-app": {
"guild_id": "123456789012345678",
"channels": { "dev": "CHANNEL_ID", "alerts": "CHANNEL_ID" },
"tool_profile": "monitoring",
"tool_profile_add": ["send_message"],
"tool_profile_remove": ["list_members"]
}
}
}tool_profile— base profile to use for this projecttool_profile_add— add tools not included in the base profiletool_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 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 versionrun — 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.
# 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.
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
- If
DISCORD_OPS_TOKEN_ENVis set, its value names the env var holding the default token (e.g.,DISCORD_OPS_TOKEN_ENV=MY_BOT_TOKENreadsMY_BOT_TOKEN). - Otherwise, the default token comes from
DISCORD_TOKEN. - Per-project tokens override the default: if a project config has
"token_env": "ORG_A_TOKEN", that project's bot usesORG_A_TOKEN. - If all projects have
token_envset with valid values, no default token is needed at all.
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):
{
"projects": {
"my-app": {
"guild_id": "123456789012345678",
"channels": {
"releases": "987654321098765432",
"builds": "111222333444555666"
},
"default_channel": "releases"
}
},
"default_project": "my-app"
}- No
token_envneeded — omitting it means the project usesDISCORD_TOKEN(the default). ownersandnotify_owners_onare 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 tokenDISCORD_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"}- 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:
DISCORD_OPS_HTTP_TOKEN=your-secret-token discord-ops serve --port 3000All requests must include:
Authorization: Bearer your-secret-tokenThe 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:
# Via CLI flag
discord-ops --dry-run
# Via environment variable
DISCORD_OPS_DRY_RUN=1 discord-ops
# Via env alias
DRY_RUN=true discord-opsIn 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_iconat 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.14.0",
name: "discord-ops",
highlights: "• Owner pings\n• Smart channel resolution\n• Category channel editing",
npm: "npm install discord-ops@0.14.0",
npm_url: "https://www.npmjs.com/package/discord-ops/v/0.14.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:
{
"projects": {
"my-app": {
"guild_id": "123456789012345678",
"token_env": "MY_APP_DISCORD_TOKEN",
"channels": {
"dev": "CHANNEL_ID",
"builds": "CHANNEL_ID",
"releases": "CHANNEL_ID",
"alerts": "CHANNEL_ID"
},
"default_channel": "dev",
"owners": ["USER_SNOWFLAKE_ID"],
"notify_owners_on": ["release", "error", "alert"],
"tool_profile": "full",
"tool_profile_add": [],
"tool_profile_remove": [],
"notification_routing": {
"ci_build": "builds",
"deploy": "builds",
"release": "releases",
"error": "alerts"
}
}
},
"default_project": "my-app",
"notification_routing": {
"ci_build": "builds",
"deploy": "builds",
"release": "releases",
"error": "alerts",
"dev": "dev"
}
}| Field | Description |
|---|---|
guild_id |
Discord server (guild) snowflake ID |
token_env |
Env var name for this project's bot token |
channels |
Alias → channel ID 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.) |
tool_profile_add |
Additional tools to load on top of the base profile |
tool_profile_remove |
Tools to exclude from the base profile |
notification_routing |
Per-project override of global notification → channel routing |
Multi-Organization Troubleshooting
Validating your config
Run discord-ops validate to check your config without connecting to Discord. It detects:
- Missing
token_envvalues (env var not set) - Duplicate guild IDs across projects with different tokens
default_channelreferencing a nonexistent aliasdefault_projectpointing 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_envenv var (e.g.,export ORG_A_TOKEN=...) - Set a default token via
DISCORD_TOKEN - Use
DISCORD_OPS_TOKEN_ENVto 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
- Add
token_envto projects that need their own bot - Set the corresponding env vars
- Run
discord-ops validateto verify - Run
discord-ops healthto 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
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 --localLicense
MIT