UNPKG

@fukayatti0/discord-mcp-server

Version:

Discord integration for Model Context Protocol

1,677 lines (1,613 loc) 85.3 kB
#!/usr/bin/env node import { Client, GatewayIntentBits, ChannelType, EmbedBuilder, AttachmentBuilder, PermissionFlagsBits, } from "discord.js"; import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; import { z } from "zod"; // Discord bot setup const DISCORD_TOKEN = process.env.DISCORD_TOKEN; if (!DISCORD_TOKEN) { throw new Error("DISCORD_TOKEN environment variable is required"); } // Initialize Discord client with necessary intents const client = new Client({ intents: [ GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent, GatewayIntentBits.GuildMembers, GatewayIntentBits.GuildMessageReactions, GatewayIntentBits.GuildVoiceStates, GatewayIntentBits.GuildEmojisAndStickers, GatewayIntentBits.GuildWebhooks, GatewayIntentBits.GuildInvites, GatewayIntentBits.GuildScheduledEvents, GatewayIntentBits.GuildModeration, ], }); // Store Discord client reference let discordClient = null; client.once("ready", () => { discordClient = client; console.log(`Logged in as ${client.user.tag}`); }); // Helper function to ensure Discord client is ready function requireDiscordClient() { if (!discordClient) { throw new Error("Discord client not ready"); } return discordClient; } // Helper function to find a guild by name or ID async function findGuild(guildIdentifier) { const client = requireDiscordClient(); if (!guildIdentifier) { // If no guild specified and bot is only in one guild, use that if (client.guilds.cache.size === 1) { return client.guilds.cache.first(); } // List available guilds const guildList = Array.from(client.guilds.cache.values()) .map((g) => `"${g.name}"`) .join(", "); throw new Error( `I'm in multiple Discord servers! Please tell me which one you want to use. Available servers: ${guildList}` ); } // Try to fetch by ID first try { const guild = await client.guilds.fetch(guildIdentifier); if (guild) return guild; } catch { // If ID fetch fails, search by name const guilds = client.guilds.cache.filter( (g) => g.name.toLowerCase() === guildIdentifier.toLowerCase() ); if (guilds.size === 0) { const availableGuilds = Array.from(client.guilds.cache.values()) .map((g) => `"${g.name}"`) .join(", "); throw new Error( `I can't find a server called "${guildIdentifier}". Here are the servers I'm in: ${availableGuilds}` ); } if (guilds.size > 1) { const guildList = guilds.map((g) => `${g.name} (ID: ${g.id})`).join(", "); throw new Error( `Multiple servers found with name "${guildIdentifier}": ${guildList}. Please specify the server ID.` ); } return guilds.first(); } throw new Error(`Server "${guildIdentifier}" not found`); } // Helper function to find a channel by name or ID within a specific guild (includes threads) async function findChannel(channelIdentifier, guildIdentifier) { const client = requireDiscordClient(); // If guildIdentifier is provided, find within that guild if (guildIdentifier) { const guild = await findGuild(guildIdentifier); // First try to fetch by ID try { const channel = await client.channels.fetch(channelIdentifier); if ( channel && channel.guild && channel.guild.id === guild.id && channel.isTextBased() ) { return channel; } } catch { // If fetching by ID fails, search by name in the specified guild const channels = guild.channels.cache.filter( (channel) => channel.isTextBased() && (channel.name.toLowerCase() === channelIdentifier.toLowerCase() || channel.name.toLowerCase() === channelIdentifier.toLowerCase().replace("#", "")) ); if (channels.size === 0) { const availableChannels = guild.channels.cache .filter((c) => c.isTextBased()) .map((c) => `"#${c.name}"`) .join(", "); throw new Error( `I can't find a channel called "${channelIdentifier}" in ${guild.name}. Here are the channels I can see: ${availableChannels}` ); } if (channels.size > 1) { const channelList = channels .map((c) => `#${c.name} (${c.id})`) .join(", "); throw new Error( `Multiple channels found with name "${channelIdentifier}" in server "${guild.name}": ${channelList}. Please specify the channel ID.` ); } return channels.first(); } throw new Error( `Channel "${channelIdentifier}" is not a text channel or not found in server "${guild.name}"` ); } // If no guild specified, try to fetch by ID directly try { const channel = await client.channels.fetch(channelIdentifier); if (channel && channel.isTextBased()) { return channel; } throw new Error(`Channel "${channelIdentifier}" is not a text channel`); } catch { throw new Error( `Channel "${channelIdentifier}" not found. When using channel names, please also specify the server.` ); } } // Helper function to find a category by name or ID within a specific guild async function findCategory(categoryIdentifier, guildIdentifier) { const client = requireDiscordClient(); const guild = await findGuild(guildIdentifier); // First try to fetch by ID try { const category = await client.channels.fetch(categoryIdentifier); if ( category && category.guild && category.guild.id === guild.id && category.type === ChannelType.GuildCategory ) { return category; } } catch { // If fetching by ID fails, search by name in the specified guild const categories = guild.channels.cache.filter( (channel) => channel.type === ChannelType.GuildCategory && channel.name.toLowerCase() === categoryIdentifier.toLowerCase() ); if (categories.size === 0) { const availableCategories = guild.channels.cache .filter((c) => c.type === ChannelType.GuildCategory) .map((c) => `"${c.name}"`) .join(", "); throw new Error( `Category "${categoryIdentifier}" not found in server "${guild.name}". Available categories: ${availableCategories}` ); } if (categories.size > 1) { const categoryList = categories .map((c) => `${c.name} (${c.id})`) .join(", "); throw new Error( `Multiple categories found with name "${categoryIdentifier}" in server "${guild.name}": ${categoryList}. Please specify the category ID.` ); } return categories.first(); } throw new Error( `Category "${categoryIdentifier}" not found in server "${guild.name}"` ); } // Validation schemas const SendMessageSchema = z.object({ server: z .string() .optional() .describe("Server name or ID (optional if bot is only in one server)"), channel: z .string() .describe('Channel name (e.g., "general") or ID (including threads)'), content: z.string().describe("Message content"), }); const ReadMessagesSchema = z.object({ server: z .string() .optional() .describe("Server name or ID (optional if bot is only in one server)"), channel: z .string() .describe('Channel name (e.g., "general") or ID (including threads)'), limit: z.number().min(1).default(50), before: z .string() .optional() .describe("Message ID to fetch messages before (for pagination)"), after: z .string() .optional() .describe("Message ID to fetch messages after (for pagination)"), around: z .string() .optional() .describe("Message ID to fetch messages around (for pagination)"), }); const ReadMessagesBulkSchema = z.object({ server: z .string() .optional() .describe("Server name or ID (optional if bot is only in one server)"), channel: z .string() .describe('Channel name (e.g., "general") or ID (including threads)'), total_limit: z .number() .min(1) .default(200) .describe( "Total number of messages to fetch (will be fetched in batches of 100). Set to -1 for unlimited" ), unlimited: z .boolean() .optional() .describe( "Set to true to fetch all available messages (ignores total_limit)" ), }); const ReactionSchema = z.object({ server: z .string() .optional() .describe("Server name or ID (optional if bot is only in one server)"), channel: z .string() .describe('Channel name (e.g., "general") or ID (including threads)'), message_id: z.string().describe("Message ID"), emoji: z.string().describe("Emoji to react with"), }); const MultipleReactionSchema = z.object({ server: z .string() .optional() .describe("Server name or ID (optional if bot is only in one server)"), channel: z .string() .describe('Channel name (e.g., "general") or ID (including threads)'), message_id: z.string().describe("Message ID"), emojis: z.array(z.string()).describe("Array of emojis to react with"), }); const ModerateMessageSchema = z.object({ server: z .string() .optional() .describe("Server name or ID (optional if bot is only in one server)"), channel: z .string() .describe('Channel name (e.g., "general") or ID (including threads)'), message_id: z.string().describe("Message ID"), reason: z.string().describe("Reason for moderation"), timeout_minutes: z .number() .min(0) .max(40320) .optional() .describe("Optional timeout duration in minutes"), }); const CategoryChannelsSchema = z.object({ server: z .string() .optional() .describe("Server name or ID (optional if bot is only in one server)"), category: z.string().describe("Category name or ID"), limit: z.number().min(1).default(10).optional(), }); // New schemas for additional functionality const CreateVoiceChannelSchema = z.object({ server: z .string() .optional() .describe("Server name or ID (optional if bot is only in one server)"), name: z.string().describe("Voice channel name"), category_id: z .string() .optional() .describe("Optional category ID to place channel in"), user_limit: z .number() .min(0) .max(99) .optional() .describe("User limit (0 for unlimited)"), bitrate: z .number() .min(8000) .max(384000) .optional() .describe("Bitrate in bps"), }); const CreateThreadSchema = z.object({ server: z .string() .optional() .describe("Server name or ID (optional if bot is only in one server)"), channel: z.string().describe("Parent channel name or ID"), name: z.string().describe("Thread name"), message_id: z .string() .optional() .describe("Message ID to create thread from (for message threads)"), auto_archive_duration: z .number() .optional() .describe("Auto archive duration in minutes (60, 1440, 4320, 10080)"), private: z .boolean() .optional() .describe("Create private thread (requires permissions)"), }); const CreateRoleSchema = z.object({ server: z .string() .optional() .describe("Server name or ID (optional if bot is only in one server)"), name: z.string().describe("Role name"), color: z .string() .optional() .describe("Role color (hex color code like #FF0000)"), hoist: z .boolean() .optional() .describe("Display role separately in member list"), mentionable: z .boolean() .optional() .describe("Allow role to be mentioned by everyone"), permissions: z .array(z.string()) .optional() .describe("Array of permission names"), }); const SendEmbedSchema = z.object({ server: z .string() .optional() .describe("Server name or ID (optional if bot is only in one server)"), channel: z.string().describe("Channel name or ID"), title: z.string().optional().describe("Embed title"), description: z.string().optional().describe("Embed description"), color: z.string().optional().describe("Embed color (hex color code)"), thumbnail: z.string().optional().describe("Thumbnail image URL"), image: z.string().optional().describe("Main image URL"), author_name: z.string().optional().describe("Author name"), author_icon: z.string().optional().describe("Author icon URL"), footer_text: z.string().optional().describe("Footer text"), footer_icon: z.string().optional().describe("Footer icon URL"), fields: z .array( z.object({ name: z.string(), value: z.string(), inline: z.boolean().optional(), }) ) .optional() .describe("Embed fields"), }); const EditMessageSchema = z.object({ server: z .string() .optional() .describe("Server name or ID (optional if bot is only in one server)"), channel: z.string().describe("Channel name or ID"), message_id: z.string().describe("Message ID to edit"), content: z.string().optional().describe("New message content"), embed: z .object({ title: z.string().optional(), description: z.string().optional(), color: z.string().optional(), }) .optional() .describe("Embed data to add/update"), }); const DeleteMessageSchema = z.object({ server: z .string() .optional() .describe("Server name or ID (optional if bot is only in one server)"), channel: z.string().describe("Channel name or ID"), message_id: z.string().describe("Message ID to delete"), reason: z.string().optional().describe("Reason for deletion"), }); const CreateInviteSchema = z.object({ server: z .string() .optional() .describe("Server name or ID (optional if bot is only in one server)"), channel: z .string() .optional() .describe("Channel for invite (defaults to system channel)"), max_age: z .number() .optional() .describe("Invite expiration in seconds (0 for never)"), max_uses: z.number().optional().describe("Maximum uses (0 for unlimited)"), temporary: z.boolean().optional().describe("Grant temporary membership"), unique: z.boolean().optional().describe("Create unique invite"), }); const ManageEmojiSchema = z.object({ server: z .string() .optional() .describe("Server name or ID (optional if bot is only in one server)"), name: z.string().describe("Emoji name"), image_url: z.string().optional().describe("Image URL for creating emoji"), emoji_id: z.string().optional().describe("Emoji ID for deleting emoji"), }); const CreateWebhookSchema = z.object({ server: z .string() .optional() .describe("Server name or ID (optional if bot is only in one server)"), channel: z.string().describe("Channel name or ID"), name: z.string().describe("Webhook name"), avatar_url: z.string().optional().describe("Webhook avatar URL"), }); const KickMemberSchema = z.object({ server: z .string() .optional() .describe("Server name or ID (optional if bot is only in one server)"), user_id: z.string().describe("User ID to kick"), reason: z.string().optional().describe("Reason for kick"), }); const BanMemberSchema = z.object({ server: z .string() .optional() .describe("Server name or ID (optional if bot is only in one server)"), user_id: z.string().describe("User ID to ban"), reason: z.string().optional().describe("Reason for ban"), delete_days: z .number() .min(0) .max(7) .optional() .describe("Days of messages to delete (0-7)"), }); const SendFileSchema = z.object({ server: z .string() .optional() .describe("Server name or ID (optional if bot is only in one server)"), channel: z.string().describe("Channel name or ID"), file_url: z.string().describe("File URL to send"), filename: z.string().optional().describe("Custom filename"), content: z.string().optional().describe("Message content to send with file"), }); // Initialize MCP server const server = new Server( { name: "discord-server", version: "0.1.0", }, { capabilities: { tools: {}, }, } ); // List available Discord tools server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ // Server Information Tools { name: "get_server_info", description: "Show me information about this Discord server including member count, creation date, and settings", inputSchema: { type: "object", properties: { server: { type: "string", description: "Which Discord server? (leave empty if I'm only in one server)", }, }, }, }, { name: "list_members", description: "Show me all the members in this Discord server with their roles and join dates", inputSchema: { type: "object", properties: { server: { type: "string", description: "Server name or ID (optional if bot is only in one server)", }, limit: { type: "number", description: "Maximum number of members to fetch", minimum: 1, maximum: 1000, }, }, }, }, // Category Tools { name: "list_category_channels", description: "Show me all the channels in a specific category, like 'General' or 'Gaming'", inputSchema: { type: "object", properties: { server: { type: "string", description: "Server name or ID (optional if bot is only in one server)", }, category: { type: "string", description: "Category name or ID", }, }, required: ["category"], }, }, { name: "read_category_channels", description: "Read recent messages from all channels in a category like 'Support' or 'Development'", inputSchema: { type: "object", properties: { server: { type: "string", description: "Server name or ID (optional if bot is only in one server)", }, category: { type: "string", description: "Category name or ID", }, limit: { type: "number", description: "Number of messages to fetch per channel", minimum: 1, default: 10, }, }, required: ["category"], }, }, // Role Management Tools { name: "add_role", description: "Give a role like 'Moderator' or 'VIP' to a specific user", inputSchema: { type: "object", properties: { server: { type: "string", description: "Server name or ID (optional if bot is only in one server)", }, user_id: { type: "string", description: "User to add role to", }, role_id: { type: "string", description: "Role ID to add", }, }, required: ["user_id", "role_id"], }, }, { name: "remove_role", description: "Take away a role like 'Member' or 'Helper' from a specific user", inputSchema: { type: "object", properties: { server: { type: "string", description: "Server name or ID (optional if bot is only in one server)", }, user_id: { type: "string", description: "User to remove role from", }, role_id: { type: "string", description: "Role ID to remove", }, }, required: ["user_id", "role_id"], }, }, // Channel Management Tools { name: "create_text_channel", description: "Create a new text channel with a custom name and optional topic", inputSchema: { type: "object", properties: { server: { type: "string", description: "Server name or ID (optional if bot is only in one server)", }, name: { type: "string", description: "Channel name", }, category_id: { type: "string", description: "Optional category ID to place channel in", }, topic: { type: "string", description: "Optional channel topic", }, }, required: ["name"], }, }, { name: "delete_channel", description: "Delete a channel permanently (be careful - this cannot be undone)", inputSchema: { type: "object", properties: { server: { type: "string", description: "Server name or ID (optional if bot is only in one server)", }, channel: { type: "string", description: "Channel name (e.g., 'general') or ID (including threads)", }, reason: { type: "string", description: "Reason for deletion", }, }, required: ["channel"], }, }, // Message Reaction Tools { name: "add_reaction", description: "Add an emoji reaction like 👍, ❤️, or 😂 to a specific message", inputSchema: { type: "object", properties: { server: { type: "string", description: "Server name or ID (optional if bot is only in one server)", }, channel: { type: "string", description: "Channel name (e.g., 'general') or ID (including threads)", }, message_id: { type: "string", description: "Message to react to", }, emoji: { type: "string", description: "Emoji to react with (Unicode or custom emoji ID)", }, }, required: ["channel", "message_id", "emoji"], }, }, { name: "add_multiple_reactions", description: "Add several emoji reactions at once to a message, like 👍❤️😂", inputSchema: { type: "object", properties: { server: { type: "string", description: "Server name or ID (optional if bot is only in one server)", }, channel: { type: "string", description: "Channel name (e.g., 'general') or ID (including threads)", }, message_id: { type: "string", description: "Message to react to", }, emojis: { type: "array", items: { type: "string", description: "Emoji to react with (Unicode or custom emoji ID)", }, description: "List of emojis to add as reactions", }, }, required: ["channel", "message_id", "emojis"], }, }, { name: "remove_reaction", description: "Remove my emoji reaction from a message (like undoing a 👍)", inputSchema: { type: "object", properties: { server: { type: "string", description: "Server name or ID (optional if bot is only in one server)", }, channel: { type: "string", description: "Channel name (e.g., 'general') or ID (including threads)", }, message_id: { type: "string", description: "Message to remove reaction from", }, emoji: { type: "string", description: "Emoji to remove (Unicode or custom emoji ID)", }, }, required: ["channel", "message_id", "emoji"], }, }, { name: "send_message", description: "Send a text message to any channel like #general, #announcements, or a thread", inputSchema: { type: "object", properties: { server: { type: "string", description: "Server name or ID (optional if bot is only in one server)", }, channel: { type: "string", description: "Which channel? (like 'general', 'announcements', or a thread name)", }, content: { type: "string", description: "What message do you want to send?", }, }, required: ["channel", "content"], }, }, { name: "read_messages", description: "Read recent messages from any channel or thread - specify how many you want to see", inputSchema: { type: "object", properties: { server: { type: "string", description: "Server name or ID (optional if bot is only in one server)", }, channel: { type: "string", description: "Channel name (e.g., 'general') or ID (including threads)", }, limit: { type: "number", description: "How many messages do you want to read? (e.g., 10, 50, 100)", minimum: 1, }, before: { type: "string", description: "Message ID to fetch messages before (for pagination)", }, after: { type: "string", description: "Message ID to fetch messages after (for pagination)", }, around: { type: "string", description: "Message ID to fetch messages around (for pagination)", }, }, required: ["channel"], }, }, { name: "read_messages_bulk", description: "Read ALL messages from a channel or thread - can fetch hundreds or thousands of messages automatically", inputSchema: { type: "object", properties: { server: { type: "string", description: "Server name or ID (optional if bot is only in one server)", }, channel: { type: "string", description: "Channel name (e.g., 'general') or ID (including threads)", }, total_limit: { type: "number", description: "How many messages in total? (e.g., 500, 1000, or -1 for ALL messages)", minimum: 1, default: 200, }, unlimited: { type: "boolean", description: "Want ALL messages ever sent in this channel? Set this to true", }, }, required: ["channel"], }, }, { name: "get_user_info", description: "Look up detailed information about any Discord user by their ID", inputSchema: { type: "object", properties: { user_id: { type: "string", description: "Discord user ID", }, }, required: ["user_id"], }, }, { name: "moderate_message", description: "Delete inappropriate messages and optionally put the user in timeout", inputSchema: { type: "object", properties: { server: { type: "string", description: "Server name or ID (optional if bot is only in one server)", }, channel: { type: "string", description: "Channel name (e.g., 'general') or ID (including threads)", }, message_id: { type: "string", description: "ID of message to moderate", }, reason: { type: "string", description: "Reason for moderation", }, timeout_minutes: { type: "number", description: "Optional timeout duration in minutes", minimum: 0, maximum: 40320, // Max 4 weeks }, }, required: ["channel", "message_id", "reason"], }, }, // Voice Channel Management { name: "create_voice_channel", description: "Create a new voice channel with custom settings like user limit and bitrate", inputSchema: { type: "object", properties: { server: { type: "string", description: "Server name or ID (optional if bot is only in one server)", }, name: { type: "string", description: "Voice channel name", }, category_id: { type: "string", description: "Optional category ID to place channel in", }, user_limit: { type: "number", description: "User limit (0 for unlimited)", minimum: 0, maximum: 99, }, bitrate: { type: "number", description: "Bitrate in bps", minimum: 8000, maximum: 384000, }, }, required: ["name"], }, }, // Thread Management { name: "create_thread", description: "Create a new thread in a channel, either standalone or from a message", inputSchema: { type: "object", properties: { server: { type: "string", description: "Server name or ID (optional if bot is only in one server)", }, channel: { type: "string", description: "Parent channel name or ID", }, name: { type: "string", description: "Thread name", }, message_id: { type: "string", description: "Message ID to create thread from (for message threads)", }, auto_archive_duration: { type: "number", description: "Auto archive duration in minutes (60, 1440, 4320, 10080)", }, private: { type: "boolean", description: "Create private thread (requires permissions)", }, }, required: ["channel", "name"], }, }, // Enhanced Role Management { name: "create_role", description: "Create a new role with custom permissions, color, and settings", inputSchema: { type: "object", properties: { server: { type: "string", description: "Server name or ID (optional if bot is only in one server)", }, name: { type: "string", description: "Role name", }, color: { type: "string", description: "Role color (hex color code like #FF0000)", }, hoist: { type: "boolean", description: "Display role separately in member list", }, mentionable: { type: "boolean", description: "Allow role to be mentioned by everyone", }, permissions: { type: "array", items: { type: "string", }, description: "Array of permission names", }, }, required: ["name"], }, }, { name: "delete_role", description: "Delete a role from the server", inputSchema: { type: "object", properties: { server: { type: "string", description: "Server name or ID (optional if bot is only in one server)", }, role_id: { type: "string", description: "Role ID to delete", }, reason: { type: "string", description: "Reason for deletion", }, }, required: ["role_id"], }, }, { name: "list_roles", description: "List all roles in the server with their permissions", inputSchema: { type: "object", properties: { server: { type: "string", description: "Server name or ID (optional if bot is only in one server)", }, }, }, }, // Enhanced Message Features { name: "send_embed", description: "Send a rich embed message with title, description, fields, images, and more", inputSchema: { type: "object", properties: { server: { type: "string", description: "Server name or ID (optional if bot is only in one server)", }, channel: { type: "string", description: "Channel name or ID", }, title: { type: "string", description: "Embed title", }, description: { type: "string", description: "Embed description", }, color: { type: "string", description: "Embed color (hex color code)", }, thumbnail: { type: "string", description: "Thumbnail image URL", }, image: { type: "string", description: "Main image URL", }, author_name: { type: "string", description: "Author name", }, author_icon: { type: "string", description: "Author icon URL", }, footer_text: { type: "string", description: "Footer text", }, footer_icon: { type: "string", description: "Footer icon URL", }, fields: { type: "array", items: { type: "object", properties: { name: { type: "string" }, value: { type: "string" }, inline: { type: "boolean" }, }, required: ["name", "value"], }, description: "Embed fields", }, }, required: ["channel"], }, }, { name: "edit_message", description: "Edit an existing message content or embed", inputSchema: { type: "object", properties: { server: { type: "string", description: "Server name or ID (optional if bot is only in one server)", }, channel: { type: "string", description: "Channel name or ID", }, message_id: { type: "string", description: "Message ID to edit", }, content: { type: "string", description: "New message content", }, embed: { type: "object", properties: { title: { type: "string" }, description: { type: "string" }, color: { type: "string" }, }, description: "Embed data to add/update", }, }, required: ["channel", "message_id"], }, }, { name: "delete_message", description: "Delete a specific message from a channel", inputSchema: { type: "object", properties: { server: { type: "string", description: "Server name or ID (optional if bot is only in one server)", }, channel: { type: "string", description: "Channel name or ID", }, message_id: { type: "string", description: "Message ID to delete", }, reason: { type: "string", description: "Reason for deletion", }, }, required: ["channel", "message_id"], }, }, { name: "send_file", description: "Send a file attachment to a channel", inputSchema: { type: "object", properties: { server: { type: "string", description: "Server name or ID (optional if bot is only in one server)", }, channel: { type: "string", description: "Channel name or ID", }, file_url: { type: "string", description: "File URL to send", }, filename: { type: "string", description: "Custom filename", }, content: { type: "string", description: "Message content to send with file", }, }, required: ["channel", "file_url"], }, }, // Invite Management { name: "create_invite", description: "Create an invite link for the server with custom settings", inputSchema: { type: "object", properties: { server: { type: "string", description: "Server name or ID (optional if bot is only in one server)", }, channel: { type: "string", description: "Channel for invite (defaults to system channel)", }, max_age: { type: "number", description: "Invite expiration in seconds (0 for never)", }, max_uses: { type: "number", description: "Maximum uses (0 for unlimited)", }, temporary: { type: "boolean", description: "Grant temporary membership", }, unique: { type: "boolean", description: "Create unique invite", }, }, }, }, { name: "list_invites", description: "List all active invites for the server", inputSchema: { type: "object", properties: { server: { type: "string", description: "Server name or ID (optional if bot is only in one server)", }, }, }, }, // Emoji Management { name: "create_emoji", description: "Create a custom emoji for the server", inputSchema: { type: "object", properties: { server: { type: "string", description: "Server name or ID (optional if bot is only in one server)", }, name: { type: "string", description: "Emoji name", }, image_url: { type: "string", description: "Image URL for creating emoji", }, }, required: ["name", "image_url"], }, }, { name: "delete_emoji", description: "Delete a custom emoji from the server", inputSchema: { type: "object", properties: { server: { type: "string", description: "Server name or ID (optional if bot is only in one server)", }, emoji_id: { type: "string", description: "Emoji ID to delete", }, reason: { type: "string", description: "Reason for deletion", }, }, required: ["emoji_id"], }, }, { name: "list_emojis", description: "List all custom emojis in the server", inputSchema: { type: "object", properties: { server: { type: "string", description: "Server name or ID (optional if bot is only in one server)", }, }, }, }, // Webhook Management { name: "create_webhook", description: "Create a webhook for a channel", inputSchema: { type: "object", properties: { server: { type: "string", description: "Server name or ID (optional if bot is only in one server)", }, channel: { type: "string", description: "Channel name or ID", }, name: { type: "string", description: "Webhook name", }, avatar_url: { type: "string", description: "Webhook avatar URL", }, }, required: ["channel", "name"], }, }, { name: "list_webhooks", description: "List all webhooks in the server", inputSchema: { type: "object", properties: { server: { type: "string", description: "Server name or ID (optional if bot is only in one server)", }, }, }, }, // Advanced Moderation { name: "kick_member", description: "Kick a member from the server", inputSchema: { type: "object", properties: { server: { type: "string", description: "Server name or ID (optional if bot is only in one server)", }, user_id: { type: "string", description: "User ID to kick", }, reason: { type: "string", description: "Reason for kick", }, }, required: ["user_id"], }, }, { name: "ban_member", description: "Ban a member from the server", inputSchema: { type: "object", properties: { server: { type: "string", description: "Server name or ID (optional if bot is only in one server)", }, user_id: { type: "string", description: "User ID to ban", }, reason: { type: "string", description: "Reason for ban", }, delete_days: { type: "number", description: "Days of messages to delete (0-7)", minimum: 0, maximum: 7, }, }, required: ["user_id"], }, }, { name: "unban_member", description: "Unban a member from the server", inputSchema: { type: "object", properties: { server: { type: "string", description: "Server name or ID (optional if bot is only in one server)", }, user_id: { type: "string", description: "User ID to unban", }, reason: { type: "string", description: "Reason for unban", }, }, required: ["user_id"], }, }, { name: "timeout_member", description: "Put a member in timeout for a specified duration", inputSchema: { type: "object", properties: { server: { type: "string", description: "Server name or ID (optional if bot is only in one server)", }, user_id: { type: "string", description: "User ID to timeout", }, duration_minutes: { type: "number", description: "Timeout duration in minutes", minimum: 1, maximum: 40320, }, reason: { type: "string", description: "Reason for timeout", }, }, required: ["user_id", "duration_minutes"], }, }, // Channel Information { name: "list_channels", description: "List all channels in the server organized by category", inputSchema: { type: "object", properties: { server: { type: "string", description: "Server name or ID (optional if bot is only in one server)", }, include_threads: { type: "boolean", description: "Include active threads in the list", }, }, }, }, { name: "get_channel_info", description: "Get detailed information about a specific channel", inputSchema: { type: "object", properties: { server: { type: "string", description: "Server name or ID (optional if bot is only in one server)", }, channel: { type: "string", description: "Channel name or ID", }, }, required: ["channel"], }, }, ], }; }); // Handle Discord tool calls server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; const client = requireDiscordClient(); try { switch (name) { case "send_message": { const { server, channel, content } = SendMessageSchema.parse(args); const channelObj = await findChannel(channel, server); const message = await channelObj.send(content); return { content: [ { type: "text", text: `✅ Your message has been sent to #${channelObj.name} in ${channelObj.guild.name}!`, }, ], }; } case "read_messages": { const { server, channel, limit, before, after, around } = ReadMessagesSchema.parse(args); const channelObj = await findChannel(channel, server); // Build fetch options with pagination parameters const fetchOptions = { limit }; if (before) fetchOptions.before = before; if (after) fetchOptions.after = after; if (around) fetchOptions.around = around; const messages = await channelObj.messages.fetch(fetchOptions); const messageList = Array.from(messages.values()).map((message) => { const reactionData = message.reactions.cache.map((reaction) => ({ emoji: reaction.emoji.name || reaction.emoji.id || reaction.emoji.toString(), count: reaction.count, })); return { id: message.id, author: message.author.tag, content: message.content, timestamp: message.createdAt.toISOString(), reactions: reactionData, }; }); const formatReaction = (r) => `${r.emoji}(${r.count})`; // Get the oldest and newest message IDs for pagination info const oldestMessage = messageList.length > 0 ? messageList[messageList.length - 1] : null; const newestMessage = messageList.length > 0 ? messageList[0] : null; let paginationInfo = ""; if (messageList.length > 0) { paginationInfo = `\n\nPagination Info:\n` + `- To get older messages, use: before=${oldestMessage.id}\n` + `- To get newer messages, use: after=${newestMessage.id}`; } return { content: [ { type: "text", text: `Retrieved ${messageList.length} messages from #${channelObj.name} in ${channelObj.guild.name}:\n\n` + messageList .map(