smithery.ai

discord-bot

Build Discord bots with modern frameworks. Use when: creating Discord bot, slash commands, moderation bot.

First seen Mar 10, 2026

Installation

$ npx skills add https://smithery.ai

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from smithery.ai · top by installs.

npx skills add https://smithery.ai

Browse all from smithery.ai

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 12,577 B
  • docs SUMMARY.md 203 B

History

  1. First seen on skills.sh
  2. First recorded snapshot · 3 installs

SKILL.md

Discord Bot Development

Project Protection Setup

MANDATORY before writing any code:

# 1. Create .gitignore
cat >> .gitignore << 'EOF'
# Build
target/
node_modules/
__pycache__/
dist/

# Secrets - CRITICAL for bots!
.env
.env.*
!.env.example
bot_token.txt
config.json  # If contains token

# IDE
.idea/
.vscode/
.DS_Store
EOF

# 2. Setup pre-commit hooks
cat > .pre-commit-config.yaml << 'EOF'
repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v5.0.0
    hooks:
      - id: detect-private-key
      - id: check-added-large-files
  - repo: https://github.com/gitleaks/gitleaks
    rev: v8.21.2
    hooks:
      - id: gitleaks
EOF

pre-commit install

Why critical: Discord bot tokens give FULL access. Leaked token = bot compromised, can spam users.


Stack Options

Language Framework Best For
Rust serenity + poise Performance, type safety
Python discord.py / nextcord Rapid development
Node discord.js JS ecosystem, largest community

Quick Start

Rust (serenity + poise)

# Cargo.toml
[dependencies]
serenity = { version = "0.12", features = ["framework"] }
poise = "0.6"
tokio = { version = "1", features = ["full"] }
use poise::serenity_prelude as serenity;

struct Data {}
type Error = Box<dyn std::error::Error + Send + Sync>;
type Context<'a> = poise::Context<'a, Data, Error>;

/// Say hello
#[poise::command(slash_command)]
async fn hello(ctx: Context<'_>) -> Result<(), Error> {
    ctx.say("Hello!").await?;
    Ok(())
}

#[tokio::main]
async fn main() {
    let framework = poise::Framework::builder()
        .options(poise::FrameworkOptions {
            commands: vec![hello()],
            ..Default::default()
        })
        .setup(|ctx, _ready, framework| {
            Box::pin(async move {
                poise::builtins::register_globally(ctx, &framework.options().commands).await?;
                Ok(Data {})
            })
        })
        .build();

    let token = std::env::var("DISCORD_TOKEN").unwrap();
    let intents = serenity::GatewayIntents::non_privileged();

    let client = serenity::ClientBuilder::new(token, intents)
        .framework(framework)
        .await
        .unwrap();

    client.start().await.unwrap();
}

Python (discord.py)

# requirements.txt
discord.py>=2.0
import discord
from discord import app_commands

intents = discord.Intents.default()
client = discord.Client(intents=intents)
tree = app_commands.CommandTree(client)

@tree.command(name="hello", description="Say hello")
async def hello(interaction: discord.Interaction):
    await interaction.response.send_message("Hello!")

@client.event
async def on_ready():
    await tree.sync()
    print(f"Logged in as {client.user}")

client.run("BOT_TOKEN")

Node (discord.js)

// package.json: "discord.js": "^14.14"
import { Client, GatewayIntentBits, SlashCommandBuilder, REST, Routes } from 'discord.js';

const client = new Client({ intents: [GatewayIntentBits.Guilds] });

const commands = [
  new SlashCommandBuilder().setName('hello').setDescription('Say hello'),
].map(cmd => cmd.toJSON());

client.once('ready', async () => {
  const rest = new REST().setToken(process.env.DISCORD_TOKEN!);
  await rest.put(Routes.applicationCommands(client.user!.id), { body: commands });
  console.log(`Logged in as ${client.user?.tag}`);
});

client.on('interactionCreate', async (interaction) => {
  if (!interaction.isChatInputCommand()) return;
  if (interaction.commandName === 'hello') {
    await interaction.reply('Hello!');
  }
});

client.login(process.env.DISCORD_TOKEN);

Slash Commands

With Options (Rust/poise)

/// Ban a user
#[poise::command(slash_command, required_permissions = "BAN_MEMBERS")]
async fn ban(
    ctx: Context<'_>,
    #[description = "User to ban"] user: serenity::User,
    #[description = "Reason"] reason: Option<String>,
) -> Result<(), Error> {
    let reason = reason.unwrap_or_else(|| "No reason provided".to_string());

    ctx.guild_id()
        .unwrap()
        .ban_with_reason(&ctx.serenity_context().http, user.id, 0, &reason)
        .await?;

    ctx.say(format!("Banned {} for: {}", user.name, reason)).await?;
    Ok(())
}

With Options (Python)

@tree.command(name="ban", description="Ban a user")
@app_commands.describe(user="User to ban", reason="Reason for ban")
@app_commands.default_permissions(ban_members=True)
async def ban(
    interaction: discord.Interaction,
    user: discord.Member,
    reason: str = "No reason provided"
):
    await user.ban(reason=reason)
    await interaction.response.send_message(f"Banned {user.name} for: {reason}")

Embeds

Rust

use serenity::builder::{CreateEmbed, CreateMessage};

let embed = CreateEmbed::new()
    .title("User Info")
    .description("Details about the user")
    .field("Username", &user.name, true)
    .field("ID", user.id.to_string(), true)
    .color(0x00ff00)
    .thumbnail(user.avatar_url().unwrap_or_default());

ctx.send(poise::CreateReply::default().embed(embed)).await?;

Python

embed = discord.Embed(
    title="User Info",
    description="Details about the user",
    color=0x00ff00
)
embed.add_field(name="Username", value=user.name, inline=True)
embed.add_field(name="ID", value=str(user.id), inline=True)
embed.set_thumbnail(url=user.avatar.url if user.avatar else None)

await interaction.response.send_message(embed=embed)

Buttons & Components

Rust

use serenity::builder::{CreateButton, CreateActionRow};

let button = CreateButton::new("confirm")
    .label("Confirm")
    .style(serenity::ButtonStyle::Primary);

let cancel = CreateButton::new("cancel")
    .label("Cancel")
    .style(serenity::ButtonStyle::Danger);

let row = CreateActionRow::Buttons(vec![button, cancel]);

ctx.send(poise::CreateReply::default()
    .content("Are you sure?")
    .components(vec![row])
).await?;

Python

from discord.ui import Button, View

class ConfirmView(View):
    @discord.ui.button(label="Confirm", style=discord.ButtonStyle.primary)
    async def confirm(self, interaction: discord.Interaction, button: Button):
        await interaction.response.send_message("Confirmed!")
        self.stop()

    @discord.ui.button(label="Cancel", style=discord.ButtonStyle.danger)
    async def cancel(self, interaction: discord.Interaction, button: Button):
        await interaction.response.send_message("Cancelled!")
        self.stop()

view = ConfirmView()
await interaction.response.send_message("Are you sure?", view=view)

Event Handlers

Rust

struct Handler;

#[serenity::async_trait]
impl serenity::EventHandler for Handler {
    async fn message(&self, ctx: serenity::Context, msg: serenity::Message) {
        if msg.content == "!ping" {
            msg.channel_id.say(&ctx.http, "Pong!").await.ok();
        }
    }

    async fn guild_member_addition(&self, ctx: serenity::Context, member: serenity::Member) {
        if let Some(channel) = member.guild_id.to_guild_cached(&ctx.cache) {
            // Send welcome message
        }
    }
}

Python

@client.event
async def on_message(message: discord.Message):
    if message.author.bot:
        return
    if message.content == "!ping":
        await message.channel.send("Pong!")

@client.event
async def on_member_join(member: discord.Member):
    channel = member.guild.system_channel
    if channel:
        await channel.send(f"Welcome {member.mention}!")

Permissions

Rust

#[poise::command(
    slash_command,
    required_permissions = "MANAGE_MESSAGES",  // Bot needs this
    default_member_permissions = "MANAGE_MESSAGES",  // User needs this
)]
async fn clear(
    ctx: Context<'_>,
    #[description = "Number of messages"] count: u8,
) -> Result<(), Error> {
    let messages = ctx.channel_id()
        .messages(&ctx.serenity_context().http, serenity::GetMessages::new().limit(count))
        .await?;

    ctx.channel_id()
        .delete_messages(&ctx.serenity_context().http, messages)
        .await?;

    ctx.say(format!("Deleted {} messages", count)).await?;
    Ok(())
}

Sharding (for 2500+ servers)

Rust

let client = serenity::ClientBuilder::new(token, intents)
    .framework(framework)
    .await?;

// Auto-sharding
client.start_autosharded().await?;

// Or manual sharding
// client.start_shard(shard_id, total_shards).await?;

Python

from discord import AutoShardedClient

client = AutoShardedClient(intents=intents)

Environment & Security

# .env
DISCORD_TOKEN=your_bot_token
GUILD_ID=123456789  # For development

# .gitignore
.env

Common Pitfalls

Pitfall Solution
Commands not showing Call tree.sync() / register commands
Missing intents Enable in Developer Portal + code
Rate limits Use caching, batch operations
Privileged intents Enable in portal for members/presence
Token exposed Use env vars, never commit

Bot Permissions Calculator

Required intents for common features:

  • Messages: MESSAGE_CONTENT (privileged)
  • Members: GUILD_MEMBERS (privileged)
  • Presence: GUILD_PRESENCES (privileged)

Invite URL format:

https://discord.com/api/oauth2/authorize?client_id=YOUR_ID&permissions=PERMS&scope=bot%20applications.commands

Testing

Rust (serenity/poise)

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_parse_duration() {
        assert_eq!(parse_duration("1h"), Duration::hours(1));
        assert_eq!(parse_duration("30m"), Duration::minutes(30));
    }

    #[tokio::test]
    async fn test_ban_requires_permission() {
        // Test permission checks
        let result = check_ban_permission(user_without_perms).await;
        assert!(result.is_err());
    }
}

Python (discord.py)

import pytest
from unittest.mock import AsyncMock, MagicMock

@pytest.fixture
def mock_interaction():
    interaction = MagicMock()
    interaction.response.send_message = AsyncMock()
    interaction.user.guild_permissions.ban_members = True
    return interaction

@pytest.mark.asyncio
async def test_hello_command(mock_interaction):
    await hello(mock_interaction)
    mock_interaction.response.send_message.assert_called_once()

@pytest.mark.asyncio
async def test_ban_without_permission(mock_interaction):
    mock_interaction.user.guild_permissions.ban_members = False
    with pytest.raises(discord.errors.Forbidden):
        await ban(mock_interaction, MagicMock())

Node (discord.js)

import { describe, it, expect, vi } from 'vitest';

describe('Commands', () => {
  it('hello command replies', async () => {
    const interaction = {
      reply: vi.fn(),
      isChatInputCommand: () => true,
      commandName: 'hello',
    };

    await handleInteraction(interaction);
    expect(interaction.reply).toHaveBeenCalled();
  });
});

TDD Workflow

1. Task[tdd-test-writer]: "Create /ban slash command"
   → Writes test expecting permission check + success
   → cargo test / pytest / npm test → FAILS (RED)

2. Task[rust-developer]: "Implement /ban command"
   → Implements with permission checks
   → Tests PASS (GREEN)

3. Repeat for each command

4. Task[code-reviewer]: "Review bot implementation"
   → Checks security, permissions, rate limits

Security Checklist

  • Token in environment variable (never in code)
  • .env in .gitignore
  • pre-commit hooks with gitleaks
  • Permission checks on all moderation commands
  • Rate limiting for user commands
  • Input sanitization (no injection in embeds)
  • No privileged intents unless needed
  • Audit log for moderation actions

Project Structure

discord-bot/
├── src/
│   ├── main.rs
│   ├── commands/
│   │   ├── mod.rs
│   │   ├── moderation.rs
│   │   └── fun.rs
│   └── events.rs
├── tests/
│   └── commands_test.rs
├── Cargo.toml
├── .env.example
├── .env              # NOT committed
└── .gitignore