Skip to main content
Glama

Discord MCP Server

index.ts29.8 kB
#!/usr/bin/env node import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from "@modelcontextprotocol/sdk/types.js"; import { Client, GatewayIntentBits, TextChannel, ChannelType, Collection, PermissionsBitField, Message, Partials, Events, MessageReaction, User, GuildMember, ChannelManager, } from "discord.js"; // Constants and configuration const DISCORD_TOKEN = process.env.DISCORD_TOKEN; const MAX_RETRY_ATTEMPTS = 3; const CHANNEL_CACHE_TTL = 60 * 60 * 1000; // 1 hour in milliseconds if (!DISCORD_TOKEN) { throw new Error("DISCORD_TOKEN environment variable is required"); } // Request type interfaces interface SendMessageArgs { channel: string; message: string; } interface ReadMessagesArgs { channel: string; limit?: number; } interface ListChannelsArgs { server?: string; } interface GetUserInfoArgs { user: string; } interface ReactToMessageArgs { channel: string; messageId: string; emoji: string; } interface RequestParams { name: string; arguments?: Record<string, unknown>; _meta?: unknown; } interface McpRequest { params: RequestParams; method?: string; } // Channel cache interface interface ChannelCacheEntry { channel: TextChannel; timestamp: number; } // Validation functions const isValidSendMessageArgs = (args: unknown): args is SendMessageArgs => typeof args === "object" && args !== null && typeof (args as SendMessageArgs).channel === "string" && (args as SendMessageArgs).channel.trim().length > 0 && typeof (args as SendMessageArgs).message === "string" && (args as SendMessageArgs).message.trim().length > 0; const isValidReadMessagesArgs = (args: unknown): args is ReadMessagesArgs => { if (typeof args !== "object" || args === null) { return false; } const typedArgs = args as ReadMessagesArgs; if (typeof typedArgs.channel !== "string" || typedArgs.channel.trim().length === 0) { return false; } if (typedArgs.limit !== undefined && (typeof typedArgs.limit !== "number" || typedArgs.limit <= 0)) { return false; } return true; }; const isValidListChannelsArgs = (args: unknown): args is ListChannelsArgs => typeof args === "object" && args !== null && ((args as ListChannelsArgs).server === undefined || typeof (args as ListChannelsArgs).server === "string"); const isValidGetUserInfoArgs = (args: unknown): args is GetUserInfoArgs => typeof args === "object" && args !== null && typeof (args as GetUserInfoArgs).user === "string" && (args as GetUserInfoArgs).user.trim().length > 0; const isValidReactToMessageArgs = (args: unknown): args is ReactToMessageArgs => typeof args === "object" && args !== null && typeof (args as ReactToMessageArgs).channel === "string" && (args as ReactToMessageArgs).channel.trim().length > 0 && typeof (args as ReactToMessageArgs).messageId === "string" && (args as ReactToMessageArgs).messageId.trim().length > 0 && typeof (args as ReactToMessageArgs).emoji === "string" && (args as ReactToMessageArgs).emoji.trim().length > 0; class DiscordServer { private server: Server; private discordClient: Client; private ready: boolean = false; private channelCache: Map<string, ChannelCacheEntry> = new Map(); private rateLimitTimestamp: number = 0; private requestCount: number = 0; constructor() { this.server = new Server( { name: "discord-mcp-server", version: "1.0.0", }, { capabilities: { tools: {}, }, } ); // Initialize Discord client with all needed intents and partials this.discordClient = new Client({ intents: [ GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent, GatewayIntentBits.GuildMembers, GatewayIntentBits.GuildMessageReactions, ], partials: [ Partials.Message, Partials.Channel, Partials.Reaction, Partials.User, ], }); this.setupDiscordClient(); this.setupToolHandlers(); // Error handling this.server.onerror = (error) => console.error("[MCP Error]", error); process.on("SIGINT", async () => { await this.discordClient.destroy(); await this.server.close(); process.exit(0); }); } private setupDiscordClient() { this.discordClient.once(Events.ClientReady, () => { this.ready = true; console.error( `Discord MCP server connected as ${this.discordClient.user?.tag}` ); console.error( `Connected to ${this.discordClient.guilds.cache.size} servers` ); }); this.discordClient.on(Events.Error, (error) => { console.error("[Discord Error]", error); }); // Debug events for troubleshooting if (process.env.DEBUG) { this.discordClient.on(Events.Debug, (info) => { console.error(`[Discord Debug] ${info}`); }); } // Setup handlers for partials this.discordClient.on(Events.MessageReactionAdd, async (reaction, user) => { // Handle partial reactions if (reaction.partial) { try { await reaction.fetch(); } catch (error) { console.error('Error fetching reaction:', error); return; } } // This is where you'd handle reaction events if needed }); } private setupToolHandlers() { this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: "send-message", description: "Send a message to a Discord channel", inputSchema: { type: "object", properties: { channel: { type: "string", description: "Channel name or ID (use # for channel names, e.g. '#general')", }, message: { type: "string", description: "The message content to send", }, }, required: ["channel", "message"], }, }, { name: "read-messages", description: "Read recent messages from a Discord channel", inputSchema: { type: "object", properties: { channel: { type: "string", description: "Channel name or ID (use # for channel names, e.g. '#general')", }, limit: { type: "number", description: "Number of recent messages to retrieve (default: 10, max: 100)", }, }, required: ["channel"], }, }, { name: "list-channels", description: "List available channels in a Discord server", inputSchema: { type: "object", properties: { server: { type: "string", description: "Server name or ID (optional, will list channels from all servers if not provided)", }, }, }, }, { name: "list-servers", description: "List servers the bot has access to", inputSchema: { type: "object", properties: {}, }, }, { name: "get-user-info", description: "Get information about a Discord user", inputSchema: { type: "object", properties: { user: { type: "string", description: "Username or user ID (use @ for usernames, e.g. '@username')", }, }, required: ["user"], }, }, { name: "react-to-message", description: "Add a reaction emoji to a message", inputSchema: { type: "object", properties: { channel: { type: "string", description: "Channel name or ID where the message is located", }, messageId: { type: "string", description: "ID of the message to react to", }, emoji: { type: "string", description: "Emoji to react with (Unicode emoji or custom emoji name)", }, }, required: ["channel", "messageId", "emoji"], }, }, ], })); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { // Ensure Discord client is ready if (!this.ready) { await new Promise<void>((resolve) => { const checkReady = () => { if (this.ready) { resolve(); } else { setTimeout(checkReady, 100); } }; checkReady(); }); } // Rate limit protection this.enforceRateLimit(); try { switch (request.params.name) { case "send-message": return await this.handleSendMessage(request); case "read-messages": return await this.handleReadMessages(request); case "list-channels": return await this.handleListChannels(request); case "list-servers": return await this.handleListServers(); case "get-user-info": return await this.handleGetUserInfo(request); case "react-to-message": return await this.handleReactToMessage(request); default: throw new McpError( ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}` ); } } catch (error) { console.error(`Error handling ${request.params.name}:`, error); return { content: [ { type: "text", text: `Error processing request: ${(error as Error).message || "Unknown error"}`, }, ], isError: true, }; } }); } private enforceRateLimit() { const now = Date.now(); const resetInterval = 1000; // 1 second const maxRequestsPerInterval = 5; if (now - this.rateLimitTimestamp > resetInterval) { // Reset counter if interval passed this.rateLimitTimestamp = now; this.requestCount = 1; } else { // Increment counter within current interval this.requestCount++; // Enforce rate limit if (this.requestCount > maxRequestsPerInterval) { throw new McpError( ErrorCode.InternalError, `Rate limit exceeded. Maximum ${maxRequestsPerInterval} requests per second.` ); } } } private async handleSendMessage(request: McpRequest) { if (!isValidSendMessageArgs(request.params.arguments)) { throw new McpError( ErrorCode.InvalidParams, "Invalid arguments for send-message. Channel and message must be non-empty strings." ); } const { channel: channelInput, message } = request.params .arguments as SendMessageArgs; try { const channel = await this.findChannel(channelInput); if (!channel) { return { content: [ { type: "text", text: `Channel not found: ${channelInput}`, }, ], isError: true, }; } // Check if bot has permission to send messages if ( !channel.permissionsFor(this.discordClient.user?.id || "")?.has( PermissionsBitField.Flags.SendMessages ) ) { return { content: [ { type: "text", text: `Bot does not have permission to send messages in ${channel.name}`, }, ], isError: true, }; } // Use retry logic for API calls that might fail let attempt = 0; let sentMessage: Message | undefined = undefined; let lastError: Error | undefined = undefined; while (attempt < MAX_RETRY_ATTEMPTS && !sentMessage) { try { sentMessage = await channel.send(message); break; } catch (error) { lastError = error as Error; attempt++; if (attempt < MAX_RETRY_ATTEMPTS) { // Wait before retrying with exponential backoff await new Promise(resolve => setTimeout(resolve, 1000 * Math.pow(2, attempt - 1))); } } } if (!sentMessage) { throw lastError || new Error("Failed to send message after multiple attempts"); } return { content: [ { type: "text", text: `Message sent to #${channel.name} in server ${channel.guild.name}`, }, ], }; } catch (error) { console.error("Error sending message:", error); return { content: [ { type: "text", text: `Error sending message: ${(error as Error).message}`, }, ], isError: true, }; } } private async handleReadMessages(request: McpRequest) { if (!isValidReadMessagesArgs(request.params.arguments)) { throw new McpError( ErrorCode.InvalidParams, "Invalid arguments for read-messages. Channel must be a non-empty string and limit must be a positive number." ); } const { channel: channelInput, limit = 10 } = request.params .arguments as ReadMessagesArgs; try { const channel = await this.findChannel(channelInput); if (!channel) { return { content: [ { type: "text", text: `Channel not found: ${channelInput}`, }, ], isError: true, }; } // Check if bot has permission to read message history if ( !channel.permissionsFor(this.discordClient.user?.id || "")?.has( PermissionsBitField.Flags.ReadMessageHistory ) ) { return { content: [ { type: "text", text: `Bot does not have permission to read message history in ${channel.name}`, }, ], isError: true, }; } // Limit to max 100 messages const actualLimit = Math.min(limit, 100); const messages = await channel.messages.fetch({ limit: actualLimit }); if (messages.size === 0) { return { content: [ { type: "text", text: `No messages found in #${channel.name}`, }, ], }; } // Format messages from oldest to newest const formattedMessages = [...messages.values()] .reverse() .map((msg) => this.formatMessage(msg)) .join("\n\n"); return { content: [ { type: "text", text: `Last ${messages.size} messages from #${channel.name} in server ${channel.guild.name}:\n\n${formattedMessages}`, }, ], }; } catch (error) { console.error("Error reading messages:", error); return { content: [ { type: "text", text: `Error reading messages: ${(error as Error).message}`, }, ], isError: true, }; } } private async handleListChannels(request: McpRequest) { if (!isValidListChannelsArgs(request.params.arguments)) { throw new McpError( ErrorCode.InvalidParams, "Invalid arguments for list-channels. Server must be a string or undefined." ); } try { const { server: serverInput } = (request.params.arguments || {}) as ListChannelsArgs; // If server is specified, find it if (serverInput) { const guild = this.findServer(serverInput); if (!guild) { return { content: [ { type: "text", text: `Server not found: ${serverInput}`, }, ], isError: true, }; } // Get readable channels const textChannels = guild.channels.cache.filter( (c) => c.type === ChannelType.GuildText && c .permissionsFor(this.discordClient.user?.id || "") ?.has(PermissionsBitField.Flags.ViewChannel) ); if (textChannels.size === 0) { return { content: [ { type: "text", text: `No readable text channels found in server ${guild.name}`, }, ], }; } const channelList = textChannels .map((c) => `- #${c.name} (ID: ${c.id})`) .join("\n"); return { content: [ { type: "text", text: `Available channels in server ${guild.name}:\n\n${channelList}`, }, ], }; } // List channels from all servers const allChannels: string[] = []; this.discordClient.guilds.cache.forEach((guild) => { const serverChannels = guild.channels.cache .filter( (c) => c.type === ChannelType.GuildText && c .permissionsFor(this.discordClient.user?.id || "") ?.has(PermissionsBitField.Flags.ViewChannel) ) .map((c) => `- ${guild.name} / #${c.name} (ID: ${c.id})`); allChannels.push(...serverChannels); }); if (allChannels.length === 0) { return { content: [ { type: "text", text: "No readable text channels found in any server", }, ], }; } return { content: [ { type: "text", text: `Available channels in all servers:\n\n${allChannels.join( "\n" )}`, }, ], }; } catch (error) { console.error("Error listing channels:", error); return { content: [ { type: "text", text: `Error listing channels: ${(error as Error).message}`, }, ], isError: true, }; } } private async handleListServers() { try { if (this.discordClient.guilds.cache.size === 0) { return { content: [ { type: "text", text: "The bot is not a member of any servers", }, ], }; } const serverList = this.discordClient.guilds.cache .map((guild) => `- ${guild.name} (ID: ${guild.id})`) .join("\n"); return { content: [ { type: "text", text: `Available servers:\n\n${serverList}`, }, ], }; } catch (error) { console.error("Error listing servers:", error); return { content: [ { type: "text", text: `Error listing servers: ${(error as Error).message}`, }, ], isError: true, }; } } private async handleGetUserInfo(request: McpRequest) { if (!isValidGetUserInfoArgs(request.params.arguments)) { throw new McpError( ErrorCode.InvalidParams, "Invalid arguments for get-user-info. User must be a non-empty string." ); } const { user: userInput } = request.params.arguments as GetUserInfoArgs; try { // Remove @ prefix if present const cleanUserInput = userInput.startsWith("@") ? userInput.substring(1) : userInput; // Try to find user by ID first let fetchedUser: User | undefined = undefined; try { if (/^\d+$/.test(cleanUserInput)) { fetchedUser = await this.discordClient.users.fetch(cleanUserInput); } } catch (error) { // Not a valid ID or user not found } // If not found by ID, search by username if (!fetchedUser) { for (const guild of this.discordClient.guilds.cache.values()) { try { const members = await guild.members.fetch(); const member = members.find( (m) => m.user.username.toLowerCase() === cleanUserInput.toLowerCase() || m.displayName.toLowerCase() === cleanUserInput.toLowerCase() ); if (member) { fetchedUser = member.user; break; } } catch (error) { console.error(`Error fetching members for guild ${guild.name}:`, error); } } } if (!fetchedUser) { return { content: [ { type: "text", text: `User not found: ${userInput}`, }, ], isError: true, }; } // Get user presence across all shared servers const userPresence: Record<string, string> = {}; for (const guild of this.discordClient.guilds.cache.values()) { try { const member = await guild.members.fetch(fetchedUser.id); if (member) { userPresence[guild.name] = member.displayName; } } catch (error) { // Member not in this guild } } const serverList = Object.keys(userPresence).length > 0 ? "Shared servers:\n" + Object.entries(userPresence) .map( ([server, displayName]) => `- ${server}${ displayName !== fetchedUser?.username ? ` (as ${displayName})` : "" }` ) .join("\n") : "No shared servers with this user"; return { content: [ { type: "text", text: `User Information: Username: ${fetchedUser.username} ID: ${fetchedUser.id} Account Created: ${fetchedUser.createdAt.toISOString().split("T")[0]} Bot: ${fetchedUser.bot ? "Yes" : "No"} ${serverList}`, }, ], }; } catch (error) { console.error("Error getting user info:", error); return { content: [ { type: "text", text: `Error getting user info: ${(error as Error).message}`, }, ], isError: true, }; } } private async handleReactToMessage(request: McpRequest) { if (!isValidReactToMessageArgs(request.params.arguments)) { throw new McpError( ErrorCode.InvalidParams, "Invalid arguments for react-to-message. Channel, messageId, and emoji must be non-empty strings." ); } const { channel: channelInput, messageId, emoji } = request.params.arguments as ReactToMessageArgs; try { const channel = await this.findChannel(channelInput); if (!channel) { return { content: [ { type: "text", text: `Channel not found: ${channelInput}`, }, ], isError: true, }; } // Check permissions if ( !channel.permissionsFor(this.discordClient.user?.id || "")?.has( PermissionsBitField.Flags.AddReactions ) ) { return { content: [ { type: "text", text: `Bot does not have permission to add reactions in ${channel.name}`, }, ], isError: true, }; } // Fetch the message let message; try { message = await channel.messages.fetch(messageId); } catch (error) { return { content: [ { type: "text", text: `Message not found with ID: ${messageId}`, }, ], isError: true, }; } // Add the reaction await message.react(emoji); return { content: [ { type: "text", text: `Added reaction ${emoji} to message in ${channel.name}`, }, ], }; } catch (error) { console.error("Error adding reaction:", error); return { content: [ { type: "text", text: `Error adding reaction: ${(error as Error).message}`, }, ], isError: true, }; } } // Helper methods private async findChannel( channelInput: string ): Promise<TextChannel | null> { // Check cache first const cacheKey = channelInput.toLowerCase(); const cachedEntry = this.channelCache.get(cacheKey); if (cachedEntry && Date.now() - cachedEntry.timestamp < CHANNEL_CACHE_TTL) { return cachedEntry.channel; } // Remove # prefix if present const cleanChannelInput = channelInput.startsWith("#") ? channelInput.substring(1) : channelInput; // Try to find channel by ID first if (/^\d+$/.test(cleanChannelInput)) { try { const channel = await this.discordClient.channels.fetch( cleanChannelInput ); if (channel && channel.type === ChannelType.GuildText) { // Cache the result this.channelCache.set(cacheKey, { channel: channel as TextChannel, timestamp: Date.now() }); return channel as TextChannel; } } catch (error) { // Not a valid ID or channel not found } } // Search by name across all servers for (const guild of this.discordClient.guilds.cache.values()) { const channel = guild.channels.cache.find( (c) => c.type === ChannelType.GuildText && c.name.toLowerCase() === cleanChannelInput.toLowerCase() ); if (channel) { // Cache the result this.channelCache.set(cacheKey, { channel: channel as TextChannel, timestamp: Date.now() }); return channel as TextChannel; } } return null; } private findServer(serverInput: string) { // Try to find server by ID first if (/^\d+$/.test(serverInput)) { const guild = this.discordClient.guilds.cache.get(serverInput); if (guild) return guild; } // Search by name return this.discordClient.guilds.cache.find( (g) => g.name.toLowerCase() === serverInput.toLowerCase() ); } private formatMessage(message: Message) { const timestamp = message.createdAt.toISOString().split("T").join(" ").substring(0, 19); let content = message.content; // Add embeds content if present if (message.embeds.length > 0) { const embedsContent = message.embeds .map(embed => { let embedText = ""; if (embed.title) embedText += `[Title: ${embed.title}]\n`; if (embed.description) embedText += embed.description; return embedText; }) .filter(text => text.length > 0) .join("\n"); if (embedsContent) { content += content ? `\n\n${embedsContent}` : embedsContent; } } // Add attachment info if present if (message.attachments.size > 0) { const attachmentInfo = message.attachments.map(a => `[Attachment: ${a.name || "file"}]`).join(", "); content += content ? `\n${attachmentInfo}` : attachmentInfo; } // Add reaction info if present if (message.reactions.cache.size > 0) { const reactionInfo = message.reactions.cache .map(r => `${r.emoji.name || r.emoji.toString()}: ${r.count}`) .join(", "); content += content ? `\n[Reactions: ${reactionInfo}]` : `[Reactions: ${reactionInfo}]`; } return `**${message.author.username}** (${timestamp}):\n${content || "[No text content]"}`; } async run() { try { // Connect to Discord first await this.discordClient.login(DISCORD_TOKEN); // Then connect the MCP server const transport = new StdioServerTransport(); await this.server.connect(transport); console.error(`Discord MCP server running on stdio`); } catch (error) { console.error("Error starting server:", error); process.exit(1); } } } const server = new DiscordServer(); server.run().catch(console.error);

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/RossH121/discord-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server