Skip to main content
Glama

Discord Agent MCP

by aj-geddes
messaging.ts30.2 kB
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { DiscordClientManager } from "../discord/client.js"; import { Logger } from "../utils/logger.js"; import { z } from "zod"; import { PermissionDeniedError, ChannelNotFoundError, MessageNotFoundError, } from "../errors/discord.js"; import { embedSchema, messageSchema } from "../types/schemas.js"; import { PermissionFlagsBits } from "discord.js"; export function registerMessagingTools( server: McpServer, discordManager: DiscordClientManager, logger: Logger, ) { // Send Message Tool server.registerTool( "send_message", { title: "Send Discord Message", description: "Send a message to a Discord channel", inputSchema: { channelId: z.string().describe("Channel ID (snowflake) or name"), content: z.string().max(2000).describe("Message content"), embeds: z .array(embedSchema) .max(10) .optional() .describe("Optional embeds (max 10)"), }, outputSchema: { success: z.boolean(), messageId: z.string().optional(), channelId: z.string().optional(), timestamp: z.string().optional(), error: z.string().optional(), }, }, async ({ channelId, content, embeds }) => { try { const client = discordManager.getClient(); // Resolve channel const channel = await client.channels .fetch(channelId) .catch(() => null); if (!channel || !channel.isTextBased()) { throw new ChannelNotFoundError(channelId); } // Check permissions if ("guild" in channel && channel.guild) { const permissions = channel.permissionsFor(client.user!); if (!permissions?.has(PermissionFlagsBits.SendMessages)) { throw new PermissionDeniedError("SendMessages", channelId); } if ( channel.isThread() && !permissions?.has(PermissionFlagsBits.SendMessagesInThreads) ) { throw new PermissionDeniedError("SendMessagesInThreads", channelId); } } // Send message (type narrowing for send method) if (!("send" in channel)) { throw new ChannelNotFoundError(channelId); } const message = await channel.send({ content, embeds: embeds || [], }); const output = { success: true, messageId: message.id, channelId: channel.id, timestamp: message.createdAt.toISOString(), }; logger.info("Message sent", { channelId, messageId: message.id }); return { content: [ { type: "text" as const, text: `Message sent successfully to <#${channel.id}>`, }, ], structuredContent: output, }; } catch (error: any) { logger.error("Failed to send message", { error: error.message, channelId, }); const output = { success: false, error: error.message, }; return { content: [ { type: "text" as const, text: `Failed to send message: ${error.message}`, }, ], structuredContent: output, isError: true, }; } }, ); // Read Messages Tool server.registerTool( "read_messages", { title: "Read Discord Messages", description: "Retrieve recent message history from a channel", inputSchema: { channelId: z.string().describe("Channel ID or name"), limit: z .number() .int() .min(1) .max(100) .default(50) .describe("Number of messages to retrieve"), before: z .string() .optional() .describe("Get messages before this message ID"), after: z .string() .optional() .describe("Get messages after this message ID"), }, outputSchema: { success: z.boolean(), messages: z.array(messageSchema).optional(), hasMore: z.boolean().optional(), error: z.string().optional(), }, }, async ({ channelId, limit, before, after }) => { try { const client = discordManager.getClient(); const channel = await client.channels .fetch(channelId) .catch(() => null); if (!channel || !channel.isTextBased()) { throw new ChannelNotFoundError(channelId); } // Check permissions if ("guild" in channel && channel.guild) { const permissions = channel.permissionsFor(client.user!); if (!permissions?.has(PermissionFlagsBits.ViewChannel)) { throw new PermissionDeniedError("ViewChannel", channelId); } if (!permissions?.has(PermissionFlagsBits.ReadMessageHistory)) { throw new PermissionDeniedError("ReadMessageHistory", channelId); } } // Fetch messages const messages = await channel.messages.fetch({ limit, before, after, }); const formattedMessages = messages.map((msg) => ({ id: msg.id, content: msg.content, author: { id: msg.author.id, username: msg.author.username, discriminator: msg.author.discriminator, bot: msg.author.bot, avatar: msg.author.avatar, }, timestamp: msg.createdAt.toISOString(), editedTimestamp: msg.editedAt?.toISOString() || null, attachments: msg.attachments.map((att) => ({ id: att.id, filename: att.name, size: att.size, url: att.url, contentType: att.contentType || undefined, })), embeds: msg.embeds.map((embed) => ({ title: embed.title || undefined, description: embed.description || undefined, url: embed.url || undefined, color: embed.color || undefined, timestamp: embed.timestamp ? new Date(embed.timestamp).toISOString() : undefined, })), reactions: msg.reactions.cache.map((reaction) => ({ emoji: reaction.emoji.name || reaction.emoji.id || "", count: reaction.count, me: reaction.me, })), })); const output = { success: true, messages: formattedMessages, hasMore: messages.size === limit, }; logger.info("Messages retrieved", { channelId, count: messages.size }); return { content: [ { type: "text" as const, text: `Retrieved ${messages.size} message(s) from <#${channel.id}>`, }, ], structuredContent: output, }; } catch (error: any) { logger.error("Failed to read messages", { error: error.message, channelId, }); const output = { success: false, error: error.message, }; return { content: [ { type: "text" as const, text: `Failed to read messages: ${error.message}`, }, ], structuredContent: output, isError: true, }; } }, ); // Delete Message Tool server.registerTool( "delete_message", { title: "Delete Discord Message", description: "Delete a specific message from a channel", inputSchema: { channelId: z.string().describe("Channel ID"), messageId: z.string().describe("Message ID to delete"), reason: z .string() .optional() .describe("Reason for deletion (audit log)"), }, outputSchema: { success: z.boolean(), deletedMessageId: z.string().optional(), error: z.string().optional(), }, }, async ({ channelId, messageId, reason }) => { try { const client = discordManager.getClient(); const channel = await client.channels .fetch(channelId) .catch(() => null); if (!channel || !channel.isTextBased()) { throw new ChannelNotFoundError(channelId); } const message = await channel.messages .fetch(messageId) .catch(() => null); if (!message) { throw new MessageNotFoundError(messageId); } // Check permissions (bot can always delete own messages) if (message.author.id !== client.user!.id) { if ("guild" in channel && channel.guild) { const permissions = channel.permissionsFor(client.user!); if (!permissions?.has(PermissionFlagsBits.ManageMessages)) { throw new PermissionDeniedError("ManageMessages", channelId); } } } await message.delete(); const output = { success: true, deletedMessageId: messageId, }; logger.info("Message deleted", { channelId, messageId, reason }); return { content: [ { type: "text" as const, text: `Message ${messageId} deleted successfully`, }, ], structuredContent: output, }; } catch (error: any) { logger.error("Failed to delete message", { error: error.message, channelId, messageId, }); const output = { success: false, error: error.message, }; return { content: [ { type: "text" as const, text: `Failed to delete message: ${error.message}`, }, ], structuredContent: output, isError: true, }; } }, ); // Add Reaction Tool server.registerTool( "add_reaction", { title: "Add Reaction to Message", description: "Add an emoji reaction to a message", inputSchema: { channelId: z.string().describe("Channel ID"), messageId: z.string().describe("Message ID"), emoji: z .string() .describe("Unicode emoji or custom emoji format (name:id)"), }, outputSchema: { success: z.boolean(), error: z.string().optional(), }, }, async ({ channelId, messageId, emoji }) => { try { const client = discordManager.getClient(); const channel = await client.channels .fetch(channelId) .catch(() => null); if (!channel || !channel.isTextBased()) { throw new ChannelNotFoundError(channelId); } const message = await channel.messages .fetch(messageId) .catch(() => null); if (!message) { throw new MessageNotFoundError(messageId); } // Check permissions if ("guild" in channel && channel.guild) { const permissions = channel.permissionsFor(client.user!); if (!permissions?.has(PermissionFlagsBits.AddReactions)) { throw new PermissionDeniedError("AddReactions", channelId); } } await message.react(emoji); const output = { success: true }; logger.info("Reaction added", { channelId, messageId, emoji }); return { content: [ { type: "text" as const, text: `Reaction ${emoji} added to message`, }, ], structuredContent: output, }; } catch (error: any) { logger.error("Failed to add reaction", { error: error.message, channelId, messageId, emoji, }); const output = { success: false, error: error.message, }; return { content: [ { type: "text" as const, text: `Failed to add reaction: ${error.message}`, }, ], structuredContent: output, isError: true, }; } }, ); // Edit Message Tool server.registerTool( "edit_message", { title: "Edit Discord Message", description: "Edit an existing message sent by the bot", inputSchema: { channelId: z.string().describe("Channel ID"), messageId: z.string().describe("Message ID to edit"), content: z .string() .max(2000) .optional() .describe("New message content"), embeds: z .array(embedSchema) .max(10) .optional() .describe("New embeds (max 10)"), }, outputSchema: { success: z.boolean(), messageId: z.string().optional(), editedTimestamp: z.string().optional(), error: z.string().optional(), }, }, async ({ channelId, messageId, content, embeds }) => { try { const client = discordManager.getClient(); const channel = await client.channels .fetch(channelId) .catch(() => null); if (!channel || !channel.isTextBased()) { throw new ChannelNotFoundError(channelId); } const message = await channel.messages .fetch(messageId) .catch(() => null); if (!message) { throw new MessageNotFoundError(messageId); } // Check if message was sent by the bot if (message.author.id !== client.user!.id) { throw new Error( "Can only edit messages sent by the bot. This message belongs to another user.", ); } // Edit message const editedMessage = await message.edit({ content: content || message.content, embeds: embeds || message.embeds, }); const output = { success: true, messageId: editedMessage.id, editedTimestamp: editedMessage.editedAt?.toISOString(), }; logger.info("Message edited", { channelId, messageId }); return { content: [ { type: "text" as const, text: `Message ${messageId} edited successfully`, }, ], structuredContent: output, }; } catch (error: any) { logger.error("Failed to edit message", { error: error.message, channelId, messageId, }); const output = { success: false, error: error.message, }; return { content: [ { type: "text" as const, text: `Failed to edit message: ${error.message}`, }, ], structuredContent: output, isError: true, }; } }, ); // Send Message with File Tool server.registerTool( "send_message_with_file", { title: "Send Discord Message with File", description: "Send a message with file attachment to a Discord channel", inputSchema: { channelId: z.string().describe("Channel ID (snowflake) or name"), content: z .string() .max(2000) .optional() .describe("Message content (optional if file provided)"), filePath: z.string().describe("Absolute path to file to attach"), fileName: z .string() .optional() .describe( "Custom filename (optional, uses original if not provided)", ), embeds: z .array(embedSchema) .max(10) .optional() .describe("Optional embeds (max 10)"), }, outputSchema: { success: z.boolean(), messageId: z.string().optional(), channelId: z.string().optional(), timestamp: z.string().optional(), attachmentUrl: z.string().optional(), error: z.string().optional(), }, }, async ({ channelId, content, filePath, fileName, embeds }) => { try { const client = discordManager.getClient(); // Resolve channel const channel = await client.channels .fetch(channelId) .catch(() => null); if (!channel || !channel.isTextBased()) { throw new ChannelNotFoundError(channelId); } // Check permissions if ("guild" in channel && channel.guild) { const permissions = channel.permissionsFor(client.user!); if (!permissions?.has(PermissionFlagsBits.SendMessages)) { throw new PermissionDeniedError("SendMessages", channelId); } if (!permissions?.has(PermissionFlagsBits.AttachFiles)) { throw new PermissionDeniedError("AttachFiles", channelId); } if ( channel.isThread() && !permissions?.has(PermissionFlagsBits.SendMessagesInThreads) ) { throw new PermissionDeniedError("SendMessagesInThreads", channelId); } } // Import fs module dynamically const fs = await import("fs"); const path = await import("path"); // Check if file exists if (!fs.existsSync(filePath)) { throw new Error(`File not found: ${filePath}`); } // Prepare attachment const attachment = { attachment: filePath, name: fileName || path.basename(filePath), }; // Send message (type narrowing for send method) if (!("send" in channel)) { throw new ChannelNotFoundError(channelId); } const message = await channel.send({ content: content || undefined, embeds: embeds || [], files: [attachment], }); const attachmentUrl = message.attachments.first()?.url || "No attachment URL"; const output = { success: true, messageId: message.id, channelId: channel.id, timestamp: message.createdAt.toISOString(), attachmentUrl, }; logger.info("Message with file sent", { channelId, messageId: message.id, filePath, }); return { content: [ { type: "text" as const, text: `Message with file sent successfully to <#${channel.id}>`, }, ], structuredContent: output, }; } catch (error: any) { logger.error("Failed to send message with file", { error: error.message, channelId, filePath, }); const output = { success: false, error: error.message, }; return { content: [ { type: "text" as const, text: `Failed to send message with file: ${error.message}`, }, ], structuredContent: output, isError: true, }; } }, ); // Pin Message Tool server.registerTool( "pin_message", { title: "Pin Discord Message", description: "Pin a message to the channel", inputSchema: { channelId: z.string().describe("Channel ID"), messageId: z.string().describe("Message ID to pin"), reason: z .string() .optional() .describe("Reason for pinning (audit log)"), }, outputSchema: { success: z.boolean(), pinnedMessageId: z.string().optional(), error: z.string().optional(), }, }, async ({ channelId, messageId, reason }) => { try { const client = discordManager.getClient(); const channel = await client.channels .fetch(channelId) .catch(() => null); if (!channel || !channel.isTextBased()) { throw new ChannelNotFoundError(channelId); } const message = await channel.messages .fetch(messageId) .catch(() => null); if (!message) { throw new MessageNotFoundError(messageId); } // Check permissions if ("guild" in channel && channel.guild) { const permissions = channel.permissionsFor(client.user!); if (!permissions?.has(PermissionFlagsBits.ManageMessages)) { throw new PermissionDeniedError("ManageMessages", channelId); } } await message.pin(reason); const output = { success: true, pinnedMessageId: messageId, }; logger.info("Message pinned", { channelId, messageId, reason }); return { content: [ { type: "text" as const, text: `Message ${messageId} pinned successfully`, }, ], structuredContent: output, }; } catch (error: any) { logger.error("Failed to pin message", { error: error.message, channelId, messageId, }); const output = { success: false, error: error.message, }; return { content: [ { type: "text" as const, text: `Failed to pin message: ${error.message}`, }, ], structuredContent: output, isError: true, }; } }, ); // Send Rich Message Tool server.registerTool( "send_rich_message", { title: "Send Rich Discord Message", description: "Send a richly formatted message with embeds, images, and advanced formatting. Supports full Discord markdown and embed features.", inputSchema: { channelId: z.string().describe("Channel ID (snowflake) or name"), content: z .string() .max(2000) .optional() .describe( "Message content with Discord markdown formatting (bold: **text**, italic: *text*, underline: __text__, strikethrough: ~~text~~, code: `code`, code block: ```language\\ncode\\n```)", ), embed: z .object({ title: z.string().max(256).optional(), description: z.string().max(4096).optional(), url: z.string().url().optional(), color: z .number() .int() .min(0) .max(0xffffff) .optional() .describe("Hex color as integer (e.g., 0x00ff00 for green)"), image: z .string() .url() .optional() .describe("Large image URL at bottom of embed"), thumbnail: z .string() .url() .optional() .describe("Small image URL at top-right of embed"), author: z .object({ name: z.string().max(256), url: z.string().url().optional(), iconURL: z.string().url().optional(), }) .optional(), footer: z .object({ text: z.string().max(2048), iconURL: z.string().url().optional(), }) .optional(), fields: z .array( z.object({ name: z.string().max(256), value: z.string().max(1024), inline: z.boolean().optional(), }), ) .max(25) .optional(), timestamp: z.boolean().optional().describe("Add current timestamp"), }) .optional() .describe("Rich embed object with images and formatting"), }, outputSchema: { success: z.boolean(), messageId: z.string().optional(), channelId: z.string().optional(), timestamp: z.string().optional(), error: z.string().optional(), }, }, async ({ channelId, content, embed }) => { try { const client = discordManager.getClient(); // Resolve channel const channel = await client.channels .fetch(channelId) .catch(() => null); if (!channel || !channel.isTextBased()) { throw new ChannelNotFoundError(channelId); } // Check permissions if ("guild" in channel && channel.guild) { const permissions = channel.permissionsFor(client.user!); if (!permissions?.has(PermissionFlagsBits.SendMessages)) { throw new PermissionDeniedError("SendMessages", channelId); } if (embed && !permissions?.has(PermissionFlagsBits.EmbedLinks)) { throw new PermissionDeniedError("EmbedLinks", channelId); } if ( channel.isThread() && !permissions?.has(PermissionFlagsBits.SendMessagesInThreads) ) { throw new PermissionDeniedError("SendMessagesInThreads", channelId); } } // Build embed if provided let embedPayload: any[] = []; if (embed) { const embedObj: any = {}; if (embed.title) embedObj.title = embed.title; if (embed.description) embedObj.description = embed.description; if (embed.url) embedObj.url = embed.url; if (embed.color !== undefined) embedObj.color = embed.color; if (embed.author) { embedObj.author = { name: embed.author.name, url: embed.author.url, icon_url: embed.author.iconURL, }; } if (embed.footer) { embedObj.footer = { text: embed.footer.text, icon_url: embed.footer.iconURL, }; } if (embed.image) { embedObj.image = { url: embed.image }; } if (embed.thumbnail) { embedObj.thumbnail = { url: embed.thumbnail }; } if (embed.fields) { embedObj.fields = embed.fields; } if (embed.timestamp) { embedObj.timestamp = new Date().toISOString(); } embedPayload = [embedObj]; } // Send message (type narrowing for send method) if (!("send" in channel)) { throw new ChannelNotFoundError(channelId); } const message = await channel.send({ content: content || undefined, embeds: embedPayload, }); const output = { success: true, messageId: message.id, channelId: channel.id, timestamp: message.createdAt.toISOString(), }; logger.info("Rich message sent", { channelId, messageId: message.id }); return { content: [ { type: "text" as const, text: `Rich message sent successfully to <#${channel.id}>`, }, ], structuredContent: output, }; } catch (error: any) { logger.error("Failed to send rich message", { error: error.message, channelId, }); const output = { success: false, error: error.message, }; return { content: [ { type: "text" as const, text: `Failed to send rich message: ${error.message}`, }, ], structuredContent: output, isError: true, }; } }, ); // Unpin Message Tool server.registerTool( "unpin_message", { title: "Unpin Discord Message", description: "Unpin a message from the channel", inputSchema: { channelId: z.string().describe("Channel ID"), messageId: z.string().describe("Message ID to unpin"), reason: z .string() .optional() .describe("Reason for unpinning (audit log)"), }, outputSchema: { success: z.boolean(), unpinnedMessageId: z.string().optional(), error: z.string().optional(), }, }, async ({ channelId, messageId, reason }) => { try { const client = discordManager.getClient(); const channel = await client.channels .fetch(channelId) .catch(() => null); if (!channel || !channel.isTextBased()) { throw new ChannelNotFoundError(channelId); } const message = await channel.messages .fetch(messageId) .catch(() => null); if (!message) { throw new MessageNotFoundError(messageId); } // Check permissions if ("guild" in channel && channel.guild) { const permissions = channel.permissionsFor(client.user!); if (!permissions?.has(PermissionFlagsBits.ManageMessages)) { throw new PermissionDeniedError("ManageMessages", channelId); } } await message.unpin(reason); const output = { success: true, unpinnedMessageId: messageId, }; logger.info("Message unpinned", { channelId, messageId, reason }); return { content: [ { type: "text" as const, text: `Message ${messageId} unpinned successfully`, }, ], structuredContent: output, }; } catch (error: any) { logger.error("Failed to unpin message", { error: error.message, channelId, messageId, }); const output = { success: false, error: error.message, }; return { content: [ { type: "text" as const, text: `Failed to unpin message: ${error.message}`, }, ], structuredContent: output, isError: true, }; } }, ); }

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/aj-geddes/discord-agent-mcp'

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