Smithery Logo
MCPsSkillsDocsPricing
Login
Smithery Logo

Give agents more agency

Resources

DocumentationPrivacy PolicySystem Status

Company

PricingAboutBlog

Connect

© 2026 Smithery. All rights reserved.

    timequity

    discord-bot

    timequity/discord-bot
    Coding
    3

    About

    SKILL.md

    Install

    • Claude Code
      Claude Code
    • Codex
      Codex
    • OpenClaw
      OpenClaw
    • Cursor
      Cursor
    • Amp
      Amp
    • GitHub Copilot
      GitHub Copilot
    • Gemini CLI
      Gemini CLI
    • Kilo Code
      Kilo Code
    • Junie
      Junie
    • Replit
      Replit
    • Windsurf
      Windsurf
    • Cline
      Cline
    • Continue
      Continue
    • OpenCode
      OpenCode
    • OpenHands
      OpenHands
    • Roo Code
      Roo Code
    • Augment
      Augment
    • Goose
      Goose
    • Trae
      Trae
    • Zencoder
      Zencoder
    • Antigravity
      Antigravity
    • Download skill
    ├─
    ├─
    └─

    About

    Build Discord bots with modern frameworks. Use when: creating Discord bot, slash commands, moderation bot. Triggers: "discord", "discord bot", "serenity", "discord.js", "discord.py".

    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
    
    Recommended Servers
    Hostsmith
    Hostsmith
    Thoughtbox
    Thoughtbox
    Bright Data
    Bright Data
    Repository
    timequity/plugins
    Files