diff --git a/package-lock.json b/package-lock.json index c99f74d..f2b9f26 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mcp-discord", - "version": "1.0.0", + "version": "1.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mcp-discord", - "version": "1.0.0", + "version": "1.1.0", "license": "MIT", "dependencies": { "discord.js": "^14.18.0", diff --git a/src/index.ts b/src/index.ts index b154c2d..e339e4c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,11 +1,34 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; -import { Client, GatewayIntentBits, Events, TextChannel, ForumChannel, ChannelType } from "discord.js"; +import { Client, GatewayIntentBits } from "discord.js"; import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; +import { toolList } from './toolList.js'; +import { + createToolContext, + loginHandler, + sendMessageHandler, + getForumChannelsHandler, + createForumPostHandler, + getForumPostHandler, + replyToForumHandler, + deleteForumPostHandler, + createTextChannelHandler, + deleteChannelHandler, + readMessagesHandler, + getServerInfoHandler, + addReactionHandler, + addMultipleReactionsHandler, + removeReactionHandler, + deleteMessageHandler, + createWebhookHandler, + sendWebhookMessageHandler, + editWebhookHandler, + deleteWebhookHandler +} from './tools/tools.js'; // Configuration parsing let config: any = {}; @@ -56,6 +79,11 @@ const client = new Client({ ] }); +// Save token to client for login handler +if (config.DISCORD_TOKEN) { + client.token = config.DISCORD_TOKEN; +} + // Create an MCP server const server = new Server( { @@ -69,1260 +97,98 @@ const server = new Server( } ); -const DiscordLoginSchema = z.object({ - random_string: z.string().optional() -}); - -const SendMessageSchema = z.object({ - channelId: z.string(), - message: z.string() -}); - -const GetForumChannelsSchema = z.object({ - guildId: z.string() -}); - -const CreateForumPostSchema = z.object({ - forumChannelId: z.string(), - title: z.string(), - content: z.string(), - tags: z.array(z.string()).optional() -}); - -const GetForumPostSchema = z.object({ - threadId: z.string() -}); - -const ReplyToForumSchema = z.object({ - threadId: z.string(), - message: z.string() -}); - -const CreateTextChannelSchema = z.object({ - guildId: z.string(), - channelName: z.string(), - topic: z.string().optional() -}); - -const DeleteChannelSchema = z.object({ - channelId: z.string(), - reason: z.string().optional() -}); - -const ReadMessagesSchema = z.object({ - channelId: z.string(), - limit: z.number().min(1).max(100).optional().default(50) -}); - -const GetServerInfoSchema = z.object({ - guildId: z.string() -}); - -const AddReactionSchema = z.object({ - channelId: z.string(), - messageId: z.string(), - emoji: z.string() -}); - -const AddMultipleReactionsSchema = z.object({ - channelId: z.string(), - messageId: z.string(), - emojis: z.array(z.string()) -}); - -const RemoveReactionSchema = z.object({ - channelId: z.string(), - messageId: z.string(), - emoji: z.string(), - userId: z.string().optional() -}); - -const DeleteForumPostSchema = z.object({ - threadId: z.string(), - reason: z.string().optional() -}); - -const DeleteMessageSchema = z.object({ - channelId: z.string(), - messageId: z.string(), - reason: z.string().optional() -}); - -const CreateWebhookSchema = z.object({ - channelId: z.string(), - name: z.string(), - avatar: z.string().optional(), - reason: z.string().optional() -}); - -const SendWebhookMessageSchema = z.object({ - webhookId: z.string(), - webhookToken: z.string(), - content: z.string(), - username: z.string().optional(), - avatarURL: z.string().optional(), - threadId: z.string().optional() -}); - -const EditWebhookSchema = z.object({ - webhookId: z.string(), - webhookToken: z.string().optional(), - name: z.string().optional(), - avatar: z.string().optional(), - channelId: z.string().optional(), - reason: z.string().optional() -}); - -const DeleteWebhookSchema = z.object({ - webhookId: z.string(), - webhookToken: z.string().optional(), - reason: z.string().optional() -}); - // Set up the tool list server.setRequestHandler(ListToolsRequestSchema, async () => { return { - tools: [ - { - name: "test", - description: "A simple test tool to verify the MCP server is working correctly", - inputSchema: { - type: "object" - } - }, - { - name: "discord_login", - description: "Logs in to Discord using the configured token", - inputSchema: { - type: "object", - properties: { - random_string: { type: "string" } - }, - required: [] - } - }, - { - name: "discord_send", - description: "Sends a message to a specified Discord text channel", - inputSchema: { - type: "object", - properties: { - channelId: { type: "string" }, - message: { type: "string" } - }, - required: ["channelId", "message"] - } - }, - { - name: "discord_get_forum_channels", - description: "Lists all forum channels in a specified Discord server (guild)", - inputSchema: { - type: "object", - properties: { - guildId: { type: "string" } - }, - required: ["guildId"] - } - }, - { - name: "discord_create_forum_post", - description: "Creates a new post in a Discord forum channel with optional tags", - inputSchema: { - type: "object", - properties: { - forumChannelId: { type: "string" }, - title: { type: "string" }, - content: { type: "string" }, - tags: { - type: "array", - items: { type: "string" } - } - }, - required: ["forumChannelId", "title", "content"] - } - }, - { - name: "discord_get_forum_post", - description: "Retrieves details about a forum post including its messages", - inputSchema: { - type: "object", - properties: { - threadId: { type: "string" } - }, - required: ["threadId"] - } - }, - { - name: "discord_reply_to_forum", - description: "Adds a reply to an existing forum post or thread", - inputSchema: { - type: "object", - properties: { - threadId: { type: "string" }, - message: { type: "string" } - }, - required: ["threadId", "message"] - } - }, - { - name: "discord_create_text_channel", - description: "Creates a new text channel in a Discord server with an optional topic", - inputSchema: { - type: "object", - properties: { - guildId: { type: "string" }, - channelName: { type: "string" }, - topic: { type: "string" } - }, - required: ["guildId", "channelName"] - } - }, - { - name: "discord_delete_channel", - description: "Deletes a Discord channel with an optional reason", - inputSchema: { - type: "object", - properties: { - channelId: { type: "string" }, - reason: { type: "string" } - }, - required: ["channelId"] - } - }, - { - name: "discord_read_messages", - description: "Retrieves messages from a Discord text channel with a configurable limit", - inputSchema: { - type: "object", - properties: { - channelId: { type: "string" }, - limit: { - type: "number", - minimum: 1, - maximum: 100, - default: 50 - } - }, - required: ["channelId"] - } - }, - { - name: "discord_get_server_info", - description: "Retrieves detailed information about a Discord server including channels and member count", - inputSchema: { - type: "object", - properties: { - guildId: { type: "string" } - }, - required: ["guildId"] - } - }, - { - name: "discord_add_reaction", - description: "Adds an emoji reaction to a specific Discord message", - inputSchema: { - type: "object", - properties: { - channelId: { type: "string" }, - messageId: { type: "string" }, - emoji: { type: "string" } - }, - required: ["channelId", "messageId", "emoji"] - } - }, - { - name: "discord_add_multiple_reactions", - description: "Adds multiple emoji reactions to a Discord message at once", - inputSchema: { - type: "object", - properties: { - channelId: { type: "string" }, - messageId: { type: "string" }, - emojis: { - type: "array", - items: { type: "string" } - } - }, - required: ["channelId", "messageId", "emojis"] - } - }, - { - name: "discord_remove_reaction", - description: "Removes a specific emoji reaction from a Discord message", - inputSchema: { - type: "object", - properties: { - channelId: { type: "string" }, - messageId: { type: "string" }, - emoji: { type: "string" }, - userId: { type: "string" } - }, - required: ["channelId", "messageId", "emoji"] - } - }, - { - name: "discord_delete_forum_post", - description: "Deletes a forum post or thread with an optional reason", - inputSchema: { - type: "object", - properties: { - threadId: { type: "string" }, - reason: { type: "string" } - }, - required: ["threadId"] - } - }, - { - name: "discord_delete_message", - description: "Deletes a specific message from a Discord text channel", - inputSchema: { - type: "object", - properties: { - channelId: { type: "string" }, - messageId: { type: "string" }, - reason: { type: "string" } - }, - required: ["channelId", "messageId"] - } - }, - { - name: "discord_create_webhook", - description: "Creates a new webhook for a Discord channel", - inputSchema: { - type: "object", - properties: { - channelId: { type: "string" }, - name: { type: "string" }, - avatar: { type: "string" }, - reason: { type: "string" } - }, - required: ["channelId", "name"] - } - }, - { - name: "discord_send_webhook_message", - description: "Sends a message to a Discord channel using a webhook", - inputSchema: { - type: "object", - properties: { - webhookId: { type: "string" }, - webhookToken: { type: "string" }, - content: { type: "string" }, - username: { type: "string" }, - avatarURL: { type: "string" }, - threadId: { type: "string" } - }, - required: ["webhookId", "webhookToken", "content"] - } - }, - { - name: "discord_edit_webhook", - description: "Edits an existing webhook for a Discord channel", - inputSchema: { - type: "object", - properties: { - webhookId: { type: "string" }, - webhookToken: { type: "string" }, - name: { type: "string" }, - avatar: { type: "string" }, - channelId: { type: "string" }, - reason: { type: "string" } - }, - required: ["webhookId"] - } - }, - { - name: "discord_delete_webhook", - description: "Deletes an existing webhook for a Discord channel", - inputSchema: { - type: "object", - properties: { - webhookId: { type: "string" }, - webhookToken: { type: "string" }, - reason: { type: "string" } - }, - required: ["webhookId"] - } - } - ] + tools: toolList }; }); +// Create tool context +const toolContext = createToolContext(client); + // Handle tool execution requests server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { + let toolResponse; switch (name) { - case "test": { - return { - content: [{ type: "text", text: `test success` }] - }; - } + case "discord_login": + toolResponse = await loginHandler(args, toolContext); + return toolResponse; - case "discord_login": { - DiscordLoginSchema.parse(args); - try { - const token = config.DISCORD_TOKEN; - if (!token) { - return { - content: [{ type: "text", text: "Discord token not found in config. Make sure the --config parameter is correctly set." }], - isError: true - }; - } - - await client.login(token); - return { - content: [{ type: "text", text: `Successfully logged in to Discord : ${client.user?.tag}` }] - }; - } catch (error) { - return { - content: [{ type: "text", text: `Login failed: ${error}` }], - isError: true - }; - } - } + case "discord_send": + toolResponse = await sendMessageHandler(args, toolContext); + return toolResponse; - case "discord_send": { - const { channelId, message } = SendMessageSchema.parse(args); - try { - if (!client.isReady()) { - return { - content: [{ type: "text", text: "Discord client not logged in. Please use discord_login tool first." }], - isError: true - }; - } + case "discord_get_forum_channels": + toolResponse = await getForumChannelsHandler(args, toolContext); + return toolResponse; - const channel = await client.channels.fetch(channelId); - if (!channel || !channel.isTextBased()) { - return { - content: [{ type: "text", text: `Cannot find text channel ID: ${channelId}` }], - isError: true - }; - } + case "discord_create_forum_post": + toolResponse = await createForumPostHandler(args, toolContext); + return toolResponse; - // Ensure channel is text-based and can send messages - if ('send' in channel) { - await channel.send(message); - return { - content: [{ type: "text", text: `Message successfully sent to channel ID: ${channelId}` }] - }; - } else { - return { - content: [{ type: "text", text: `This channel type does not support sending messages` }], - isError: true - }; - } - } catch (error) { - return { - content: [{ type: "text", text: `Send message failed: ${error}` }], - isError: true - }; - } - } + case "discord_get_forum_post": + toolResponse = await getForumPostHandler(args, toolContext); + return toolResponse; - case "discord_get_forum_channels": { - const { guildId } = GetForumChannelsSchema.parse(args); - try { - if (!client.isReady()) { - return { - content: [{ type: "text", text: "Discord client not logged in. Please use discord_login tool first." }], - isError: true - }; - } + case "discord_reply_to_forum": + toolResponse = await replyToForumHandler(args, toolContext); + return toolResponse; - const guild = await client.guilds.fetch(guildId); - if (!guild) { - return { - content: [{ type: "text", text: `Cannot find guild with ID: ${guildId}` }], - isError: true - }; - } + case "discord_delete_forum_post": + toolResponse = await deleteForumPostHandler(args, toolContext); + return toolResponse; - // Fetch all channels from the guild - const channels = await guild.channels.fetch(); - - // Filter to get only forum channels - const forumChannels = channels.filter(channel => channel?.type === ChannelType.GuildForum); - - if (forumChannels.size === 0) { - return { - content: [{ type: "text", text: `No forum channels found in guild: ${guild.name}` }] - }; - } + case "discord_create_text_channel": + toolResponse = await createTextChannelHandler(args, toolContext); + return toolResponse; - // Format forum channels information - const forumInfo = forumChannels.map(channel => ({ - id: channel.id, - name: channel.name, - topic: channel.topic || "No topic set" - })); + case "discord_delete_channel": + toolResponse = await deleteChannelHandler(args, toolContext); + return toolResponse; - return { - content: [{ type: "text", text: JSON.stringify(forumInfo, null, 2) }] - }; - } catch (error) { - return { - content: [{ type: "text", text: `Failed to fetch forum channels: ${error}` }], - isError: true - }; - } - } + case "discord_read_messages": + toolResponse = await readMessagesHandler(args, toolContext); + return toolResponse; - case "discord_create_forum_post": { - const { forumChannelId, title, content, tags } = CreateForumPostSchema.parse(args); - try { - if (!client.isReady()) { - return { - content: [{ type: "text", text: "Discord client not logged in. Please use discord_login tool first." }], - isError: true - }; - } + case "discord_get_server_info": + toolResponse = await getServerInfoHandler(args, toolContext); + return toolResponse; - const channel = await client.channels.fetch(forumChannelId); - if (!channel || channel.type !== ChannelType.GuildForum) { - return { - content: [{ type: "text", text: `Channel ID ${forumChannelId} is not a forum channel.` }], - isError: true - }; - } + case "discord_add_reaction": + toolResponse = await addReactionHandler(args, toolContext); + return toolResponse; - const forumChannel = channel as ForumChannel; - - // Get available tags in the forum - const availableTags = forumChannel.availableTags; - let selectedTagIds: string[] = []; - - // If tags are provided, find their IDs - if (tags && tags.length > 0) { - selectedTagIds = availableTags - .filter(tag => tags.includes(tag.name)) - .map(tag => tag.id); - } + case "discord_add_multiple_reactions": + toolResponse = await addMultipleReactionsHandler(args, toolContext); + return toolResponse; - // Create the forum post - const thread = await forumChannel.threads.create({ - name: title, - message: { - content: content - }, - appliedTags: selectedTagIds.length > 0 ? selectedTagIds : undefined - }); + case "discord_remove_reaction": + toolResponse = await removeReactionHandler(args, toolContext); + return toolResponse; - return { - content: [{ - type: "text", - text: `Successfully created forum post "${title}" with ID: ${thread.id}` - }] - }; - } catch (error) { - return { - content: [{ type: "text", text: `Failed to create forum post: ${error}` }], - isError: true - }; - } - } + case "discord_delete_message": + toolResponse = await deleteMessageHandler(args, toolContext); + return toolResponse; - case "discord_get_forum_post": { - const { threadId } = GetForumPostSchema.parse(args); - try { - if (!client.isReady()) { - return { - content: [{ type: "text", text: "Discord client not logged in. Please use discord_login tool first." }], - isError: true - }; - } + case "discord_create_webhook": + toolResponse = await createWebhookHandler(args, toolContext); + return toolResponse; - const thread = await client.channels.fetch(threadId); - if (!thread || !(thread.isThread())) { - return { - content: [{ type: "text", text: `Cannot find thread with ID: ${threadId}` }], - isError: true - }; - } + case "discord_send_webhook_message": + toolResponse = await sendWebhookMessageHandler(args, toolContext); + return toolResponse; - // Get messages from the thread - const messages = await thread.messages.fetch({ limit: 10 }); - - const threadDetails = { - id: thread.id, - name: thread.name, - parentId: thread.parentId, - messageCount: messages.size, - createdAt: thread.createdAt, - messages: messages.map(msg => ({ - id: msg.id, - content: msg.content, - author: msg.author.tag, - createdAt: msg.createdAt - })) - }; + case "discord_edit_webhook": + toolResponse = await editWebhookHandler(args, toolContext); + return toolResponse; - return { - content: [{ type: "text", text: JSON.stringify(threadDetails, null, 2) }] - }; - } catch (error) { - return { - content: [{ type: "text", text: `Failed to fetch forum post: ${error}` }], - isError: true - }; - } - } - - case "discord_reply_to_forum": { - const { threadId, message } = ReplyToForumSchema.parse(args); - try { - if (!client.isReady()) { - return { - content: [{ type: "text", text: "Discord client not logged in. Please use discord_login tool first." }], - isError: true - }; - } - - const thread = await client.channels.fetch(threadId); - if (!thread || !(thread.isThread())) { - return { - content: [{ type: "text", text: `Cannot find thread with ID: ${threadId}` }], - isError: true - }; - } - - if (!('send' in thread)) { - return { - content: [{ type: "text", text: `This thread does not support sending messages` }], - isError: true - }; - } - - // Send the reply - const sentMessage = await thread.send(message); - - return { - content: [{ - type: "text", - text: `Successfully replied to forum post. Message ID: ${sentMessage.id}` - }] - }; - } catch (error) { - return { - content: [{ type: "text", text: `Failed to reply to forum post: ${error}` }], - isError: true - }; - } - } - - case "discord_create_text_channel": { - const { guildId, channelName, topic } = CreateTextChannelSchema.parse(args); - try { - if (!client.isReady()) { - return { - content: [{ type: "text", text: "Discord client not logged in. Please use discord_login tool first." }], - isError: true - }; - } - - const guild = await client.guilds.fetch(guildId); - if (!guild) { - return { - content: [{ type: "text", text: `Cannot find guild with ID: ${guildId}` }], - isError: true - }; - } - - // Create the text channel - const channel = await guild.channels.create({ - name: channelName, - type: ChannelType.GuildText, - topic: topic - }); - - return { - content: [{ - type: "text", - text: `Successfully created text channel "${channelName}" with ID: ${channel.id}` - }] - }; - } catch (error) { - return { - content: [{ type: "text", text: `Failed to create text channel: ${error}` }], - isError: true - }; - } - } - - case "discord_delete_channel": { - const { channelId, reason } = DeleteChannelSchema.parse(args); - try { - if (!client.isReady()) { - return { - content: [{ type: "text", text: "Discord client not logged in. Please use discord_login tool first." }], - isError: true - }; - } - - const channel = await client.channels.fetch(channelId); - if (!channel) { - return { - content: [{ type: "text", text: `Cannot find channel with ID: ${channelId}` }], - isError: true - }; - } - - // Check if channel can be deleted (has delete method) - if (!('delete' in channel)) { - return { - content: [{ type: "text", text: `This channel type does not support deletion or the bot lacks permissions` }], - isError: true - }; - } - - // Delete the channel - await channel.delete(reason || "Channel deleted via API"); - - return { - content: [{ - type: "text", - text: `Successfully deleted channel with ID: ${channelId}` - }] - }; - } catch (error) { - return { - content: [{ type: "text", text: `Failed to delete channel: ${error}` }], - isError: true - }; - } - } - - case "discord_read_messages": { - const { channelId, limit } = ReadMessagesSchema.parse(args); - try { - if (!client.isReady()) { - return { - content: [{ type: "text", text: "Discord client not logged in. Please use discord_login tool first." }], - isError: true - }; - } - - const channel = await client.channels.fetch(channelId); - if (!channel) { - return { - content: [{ type: "text", text: `Cannot find channel with ID: ${channelId}` }], - isError: true - }; - } - - // Check if channel has messages (text channel, thread, etc.) - if (!channel.isTextBased() || !('messages' in channel)) { - return { - content: [{ type: "text", text: `Channel type does not support reading messages` }], - isError: true - }; - } - - // Fetch messages - const messages = await channel.messages.fetch({ limit }); - - if (messages.size === 0) { - return { - content: [{ type: "text", text: `No messages found in channel` }] - }; - } - - // Format messages - const formattedMessages = messages.map(msg => ({ - id: msg.id, - content: msg.content, - author: { - id: msg.author.id, - username: msg.author.username, - bot: msg.author.bot - }, - timestamp: msg.createdAt, - attachments: msg.attachments.size, - embeds: msg.embeds.length, - replyTo: msg.reference ? msg.reference.messageId : null - })).sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime()); - - return { - content: [{ - type: "text", - text: JSON.stringify({ - channelId, - messageCount: formattedMessages.length, - messages: formattedMessages - }, null, 2) - }] - }; - } catch (error) { - return { - content: [{ type: "text", text: `Failed to read messages: ${error}` }], - isError: true - }; - } - } - - case "discord_get_server_info": { - const { guildId } = GetServerInfoSchema.parse(args); - try { - if (!client.isReady()) { - return { - content: [{ type: "text", text: "Discord client not logged in. Please use discord_login tool first." }], - isError: true - }; - } - - const guild = await client.guilds.fetch(guildId); - if (!guild) { - return { - content: [{ type: "text", text: `Cannot find guild with ID: ${guildId}` }], - isError: true - }; - } - - // Fetch additional guild data - await guild.fetch(); - - // Fetch channel information - const channels = await guild.channels.fetch(); - - // Categorize channels by type - const channelsByType = { - text: channels.filter(c => c?.type === ChannelType.GuildText).size, - voice: channels.filter(c => c?.type === ChannelType.GuildVoice).size, - category: channels.filter(c => c?.type === ChannelType.GuildCategory).size, - forum: channels.filter(c => c?.type === ChannelType.GuildForum).size, - announcement: channels.filter(c => c?.type === ChannelType.GuildAnnouncement).size, - stage: channels.filter(c => c?.type === ChannelType.GuildStageVoice).size, - total: channels.size - }; - - // Fetch member count - const approximateMemberCount = guild.approximateMemberCount || "unknown"; - - // Format guild information - const guildInfo = { - id: guild.id, - name: guild.name, - description: guild.description, - icon: guild.iconURL(), - owner: guild.ownerId, - createdAt: guild.createdAt, - memberCount: approximateMemberCount, - channels: channelsByType, - features: guild.features, - premium: { - tier: guild.premiumTier, - subscriptions: guild.premiumSubscriptionCount - } - }; - - return { - content: [{ type: "text", text: JSON.stringify(guildInfo, null, 2) }] - }; - } catch (error) { - return { - content: [{ type: "text", text: `Failed to fetch server info: ${error}` }], - isError: true - }; - } - } - - case "discord_add_reaction": { - const { channelId, messageId, emoji } = AddReactionSchema.parse(args); - try { - if (!client.isReady()) { - return { - content: [{ type: "text", text: "Discord client not logged in. Please use discord_login tool first." }], - isError: true - }; - } - - const channel = await client.channels.fetch(channelId); - if (!channel || !channel.isTextBased() || !('messages' in channel)) { - return { - content: [{ type: "text", text: `Cannot find text channel with ID: ${channelId}` }], - isError: true - }; - } - - const message = await channel.messages.fetch(messageId); - if (!message) { - return { - content: [{ type: "text", text: `Cannot find message with ID: ${messageId}` }], - isError: true - }; - } - - // Add the reaction - await message.react(emoji); - - return { - content: [{ - type: "text", - text: `Successfully added reaction ${emoji} to message ID: ${messageId}` - }] - }; - } catch (error) { - return { - content: [{ type: "text", text: `Failed to add reaction: ${error}` }], - isError: true - }; - } - } - - case "discord_add_multiple_reactions": { - const { channelId, messageId, emojis } = AddMultipleReactionsSchema.parse(args); - try { - if (!client.isReady()) { - return { - content: [{ type: "text", text: "Discord client not logged in. Please use discord_login tool first." }], - isError: true - }; - } - - const channel = await client.channels.fetch(channelId); - if (!channel || !channel.isTextBased() || !('messages' in channel)) { - return { - content: [{ type: "text", text: `Cannot find text channel with ID: ${channelId}` }], - isError: true - }; - } - - const message = await channel.messages.fetch(messageId); - if (!message) { - return { - content: [{ type: "text", text: `Cannot find message with ID: ${messageId}` }], - isError: true - }; - } - - // Add each reaction sequentially - for (const emoji of emojis) { - await message.react(emoji); - // Small delay to prevent rate limiting - await new Promise(resolve => setTimeout(resolve, 300)); - } - - return { - content: [{ - type: "text", - text: `Successfully added ${emojis.length} reactions to message ID: ${messageId}` - }] - }; - } catch (error) { - return { - content: [{ type: "text", text: `Failed to add reactions: ${error}` }], - isError: true - }; - } - } - - case "discord_remove_reaction": { - const { channelId, messageId, emoji, userId } = RemoveReactionSchema.parse(args); - try { - if (!client.isReady()) { - return { - content: [{ type: "text", text: "Discord client not logged in. Please use discord_login tool first." }], - isError: true - }; - } - - const channel = await client.channels.fetch(channelId); - if (!channel || !channel.isTextBased() || !('messages' in channel)) { - return { - content: [{ type: "text", text: `Cannot find text channel with ID: ${channelId}` }], - isError: true - }; - } - - const message = await channel.messages.fetch(messageId); - if (!message) { - return { - content: [{ type: "text", text: `Cannot find message with ID: ${messageId}` }], - isError: true - }; - } - - // Get the reactions - const reactions = message.reactions.cache; - - // Find the specific reaction - const reaction = reactions.find(r => r.emoji.toString() === emoji || r.emoji.name === emoji); - - if (!reaction) { - return { - content: [{ type: "text", text: `Reaction ${emoji} not found on message ID: ${messageId}` }], - isError: true - }; - } - - if (userId) { - // Remove a specific user's reaction - await reaction.users.remove(userId); - return { - content: [{ - type: "text", - text: `Successfully removed reaction ${emoji} from user ID: ${userId} on message ID: ${messageId}` - }] - }; - } else { - // Remove bot's reaction - await reaction.users.remove(client.user.id); - return { - content: [{ - type: "text", - text: `Successfully removed bot's reaction ${emoji} from message ID: ${messageId}` - }] - }; - } - } catch (error) { - return { - content: [{ type: "text", text: `Failed to remove reaction: ${error}` }], - isError: true - }; - } - } - - case "discord_delete_forum_post": { - const { threadId, reason } = DeleteForumPostSchema.parse(args); - try { - if (!client.isReady()) { - return { - content: [{ type: "text", text: "Discord client not logged in. Please use discord_login tool first." }], - isError: true - }; - } - - const thread = await client.channels.fetch(threadId); - if (!thread || !thread.isThread()) { - return { - content: [{ type: "text", text: `Cannot find forum post/thread with ID: ${threadId}` }], - isError: true - }; - } - - // Delete the forum post/thread - await thread.delete(reason || "Forum post deleted via API"); - - return { - content: [{ - type: "text", - text: `Successfully deleted forum post/thread with ID: ${threadId}` - }] - }; - } catch (error) { - return { - content: [{ type: "text", text: `Failed to delete forum post: ${error}` }], - isError: true - }; - } - } - - case "discord_delete_message": { - const { channelId, messageId, reason } = DeleteMessageSchema.parse(args); - try { - if (!client.isReady()) { - return { - content: [{ type: "text", text: "Discord client not logged in. Please use discord_login tool first." }], - isError: true - }; - } - - const channel = await client.channels.fetch(channelId); - if (!channel || !channel.isTextBased() || !('messages' in channel)) { - return { - content: [{ type: "text", text: `Cannot find text channel with ID: ${channelId}` }], - isError: true - }; - } - - // Fetch the message - const message = await channel.messages.fetch(messageId); - if (!message) { - return { - content: [{ type: "text", text: `Cannot find message with ID: ${messageId}` }], - isError: true - }; - } - - // Delete the message - await message.delete(); - - return { - content: [{ - type: "text", - text: `Successfully deleted message with ID: ${messageId} from channel: ${channelId}` - }] - }; - } catch (error) { - return { - content: [{ type: "text", text: `Failed to delete message: ${error}` }], - isError: true - }; - } - } - - case "discord_create_webhook": { - const { channelId, name, avatar, reason } = CreateWebhookSchema.parse(args); - try { - if (!client.isReady()) { - return { - content: [{ type: "text", text: "Discord client not logged in. Please use discord_login tool first." }], - isError: true - }; - } - - const channel = await client.channels.fetch(channelId); - if (!channel || !channel.isTextBased()) { - return { - content: [{ type: "text", text: `Cannot find text channel with ID: ${channelId}` }], - isError: true - }; - } - - // Check if the channel supports webhooks - if (!('createWebhook' in channel)) { - return { - content: [{ type: "text", text: `Channel type does not support webhooks: ${channelId}` }], - isError: true - }; - } - - // Create the webhook - const webhook = await channel.createWebhook({ - name: name, - avatar: avatar, - reason: reason - }); - - return { - content: [{ - type: "text", - text: `Successfully created webhook with ID: ${webhook.id} and token: ${webhook.token}` - }] - }; - } catch (error) { - return { - content: [{ type: "text", text: `Failed to create webhook: ${error}` }], - isError: true - }; - } - } - - case "discord_send_webhook_message": { - const { webhookId, webhookToken, content, username, avatarURL, threadId } = SendWebhookMessageSchema.parse(args); - try { - if (!client.isReady()) { - return { - content: [{ type: "text", text: "Discord client not logged in. Please use discord_login tool first." }], - isError: true - }; - } - - const webhook = await client.fetchWebhook(webhookId, webhookToken); - if (!webhook) { - return { - content: [{ type: "text", text: `Cannot find webhook with ID: ${webhookId}` }], - isError: true - }; - } - - // Send the message - await webhook.send({ - content: content, - username: username, - avatarURL: avatarURL, - threadId: threadId - }); - - return { - content: [{ - type: "text", - text: `Successfully sent webhook message to webhook ID: ${webhookId}` - }] - }; - } catch (error) { - return { - content: [{ type: "text", text: `Failed to send webhook message: ${error}` }], - isError: true - }; - } - } - - case "discord_edit_webhook": { - const { webhookId, webhookToken, name, avatar, channelId, reason } = EditWebhookSchema.parse(args); - try { - if (!client.isReady()) { - return { - content: [{ type: "text", text: "Discord client not logged in. Please use discord_login tool first." }], - isError: true - }; - } - - const webhook = await client.fetchWebhook(webhookId, webhookToken); - if (!webhook) { - return { - content: [{ type: "text", text: `Cannot find webhook with ID: ${webhookId}` }], - isError: true - }; - } - - // Edit the webhook - await webhook.edit({ - name: name, - avatar: avatar, - channel: channelId, - reason: reason - }); - - return { - content: [{ - type: "text", - text: `Successfully edited webhook with ID: ${webhook.id}` - }] - }; - } catch (error) { - return { - content: [{ type: "text", text: `Failed to edit webhook: ${error}` }], - isError: true - }; - } - } - - case "discord_delete_webhook": { - const { webhookId, webhookToken, reason } = DeleteWebhookSchema.parse(args); - try { - if (!client.isReady()) { - return { - content: [{ type: "text", text: "Discord client not logged in. Please use discord_login tool first." }], - isError: true - }; - } - - const webhook = await client.fetchWebhook(webhookId, webhookToken); - if (!webhook) { - return { - content: [{ type: "text", text: `Cannot find webhook with ID: ${webhookId}` }], - isError: true - }; - } - - // Delete the webhook - await webhook.delete(reason || "Webhook deleted via API"); - - return { - content: [{ - type: "text", - text: `Successfully deleted webhook with ID: ${webhook.id}` - }] - }; - } catch (error) { - return { - content: [{ type: "text", text: `Failed to delete webhook: ${error}` }], - isError: true - }; - } - } + case "discord_delete_webhook": + toolResponse = await deleteWebhookHandler(args, toolContext); + return toolResponse; default: throw new Error(`Unknown tool: ${name}`); diff --git a/src/schemas.ts b/src/schemas.ts new file mode 100644 index 0000000..2917524 --- /dev/null +++ b/src/schemas.ts @@ -0,0 +1,111 @@ +import { z } from "zod"; + +export const DiscordLoginSchema = z.object({ + random_string: z.string().optional() +}); + +export const SendMessageSchema = z.object({ + channelId: z.string(), + message: z.string() +}); + +export const GetForumChannelsSchema = z.object({ + guildId: z.string() +}); + +export const CreateForumPostSchema = z.object({ + forumChannelId: z.string(), + title: z.string(), + content: z.string(), + tags: z.array(z.string()).optional() +}); + +export const GetForumPostSchema = z.object({ + threadId: z.string() +}); + +export const ReplyToForumSchema = z.object({ + threadId: z.string(), + message: z.string() +}); + +export const CreateTextChannelSchema = z.object({ + guildId: z.string(), + channelName: z.string(), + topic: z.string().optional() +}); + +export const DeleteChannelSchema = z.object({ + channelId: z.string(), + reason: z.string().optional() +}); + +export const ReadMessagesSchema = z.object({ + channelId: z.string(), + limit: z.number().min(1).max(100).optional().default(50) +}); + +export const GetServerInfoSchema = z.object({ + guildId: z.string() +}); + +export const AddReactionSchema = z.object({ + channelId: z.string(), + messageId: z.string(), + emoji: z.string() +}); + +export const AddMultipleReactionsSchema = z.object({ + channelId: z.string(), + messageId: z.string(), + emojis: z.array(z.string()) +}); + +export const RemoveReactionSchema = z.object({ + channelId: z.string(), + messageId: z.string(), + emoji: z.string(), + userId: z.string().optional() +}); + +export const DeleteForumPostSchema = z.object({ + threadId: z.string(), + reason: z.string().optional() +}); + +export const DeleteMessageSchema = z.object({ + channelId: z.string(), + messageId: z.string(), + reason: z.string().optional() +}); + +export const CreateWebhookSchema = z.object({ + channelId: z.string(), + name: z.string(), + avatar: z.string().optional(), + reason: z.string().optional() +}); + +export const SendWebhookMessageSchema = z.object({ + webhookId: z.string(), + webhookToken: z.string(), + content: z.string(), + username: z.string().optional(), + avatarURL: z.string().optional(), + threadId: z.string().optional() +}); + +export const EditWebhookSchema = z.object({ + webhookId: z.string(), + webhookToken: z.string().optional(), + name: z.string().optional(), + avatar: z.string().optional(), + channelId: z.string().optional(), + reason: z.string().optional() +}); + +export const DeleteWebhookSchema = z.object({ + webhookId: z.string(), + webhookToken: z.string().optional(), + reason: z.string().optional() +}); \ No newline at end of file diff --git a/src/toolList.ts b/src/toolList.ts new file mode 100644 index 0000000..f197976 --- /dev/null +++ b/src/toolList.ts @@ -0,0 +1,256 @@ +export const toolList = [ + { + name: "discord_login", + description: "Logs in to Discord using the configured token", + inputSchema: { + type: "object", + properties: { + random_string: { type: "string" } + }, + required: [] + } + }, + { + name: "discord_send", + description: "Sends a message to a specified Discord text channel", + inputSchema: { + type: "object", + properties: { + channelId: { type: "string" }, + message: { type: "string" } + }, + required: ["channelId", "message"] + } + }, + { + name: "discord_get_forum_channels", + description: "Lists all forum channels in a specified Discord server (guild)", + inputSchema: { + type: "object", + properties: { + guildId: { type: "string" } + }, + required: ["guildId"] + } + }, + { + name: "discord_create_forum_post", + description: "Creates a new post in a Discord forum channel with optional tags", + inputSchema: { + type: "object", + properties: { + forumChannelId: { type: "string" }, + title: { type: "string" }, + content: { type: "string" }, + tags: { + type: "array", + items: { type: "string" } + } + }, + required: ["forumChannelId", "title", "content"] + } + }, + { + name: "discord_get_forum_post", + description: "Retrieves details about a forum post including its messages", + inputSchema: { + type: "object", + properties: { + threadId: { type: "string" } + }, + required: ["threadId"] + } + }, + { + name: "discord_reply_to_forum", + description: "Adds a reply to an existing forum post or thread", + inputSchema: { + type: "object", + properties: { + threadId: { type: "string" }, + message: { type: "string" } + }, + required: ["threadId", "message"] + } + }, + { + name: "discord_create_text_channel", + description: "Creates a new text channel in a Discord server with an optional topic", + inputSchema: { + type: "object", + properties: { + guildId: { type: "string" }, + channelName: { type: "string" }, + topic: { type: "string" } + }, + required: ["guildId", "channelName"] + } + }, + { + name: "discord_delete_channel", + description: "Deletes a Discord channel with an optional reason", + inputSchema: { + type: "object", + properties: { + channelId: { type: "string" }, + reason: { type: "string" } + }, + required: ["channelId"] + } + }, + { + name: "discord_read_messages", + description: "Retrieves messages from a Discord text channel with a configurable limit", + inputSchema: { + type: "object", + properties: { + channelId: { type: "string" }, + limit: { + type: "number", + minimum: 1, + maximum: 100, + default: 50 + } + }, + required: ["channelId"] + } + }, + { + name: "discord_get_server_info", + description: "Retrieves detailed information about a Discord server including channels and member count", + inputSchema: { + type: "object", + properties: { + guildId: { type: "string" } + }, + required: ["guildId"] + } + }, + { + name: "discord_add_reaction", + description: "Adds an emoji reaction to a specific Discord message", + inputSchema: { + type: "object", + properties: { + channelId: { type: "string" }, + messageId: { type: "string" }, + emoji: { type: "string" } + }, + required: ["channelId", "messageId", "emoji"] + } + }, + { + name: "discord_add_multiple_reactions", + description: "Adds multiple emoji reactions to a Discord message at once", + inputSchema: { + type: "object", + properties: { + channelId: { type: "string" }, + messageId: { type: "string" }, + emojis: { + type: "array", + items: { type: "string" } + } + }, + required: ["channelId", "messageId", "emojis"] + } + }, + { + name: "discord_remove_reaction", + description: "Removes a specific emoji reaction from a Discord message", + inputSchema: { + type: "object", + properties: { + channelId: { type: "string" }, + messageId: { type: "string" }, + emoji: { type: "string" }, + userId: { type: "string" } + }, + required: ["channelId", "messageId", "emoji"] + } + }, + { + name: "discord_delete_forum_post", + description: "Deletes a forum post or thread with an optional reason", + inputSchema: { + type: "object", + properties: { + threadId: { type: "string" }, + reason: { type: "string" } + }, + required: ["threadId"] + } + }, + { + name: "discord_delete_message", + description: "Deletes a specific message from a Discord text channel", + inputSchema: { + type: "object", + properties: { + channelId: { type: "string" }, + messageId: { type: "string" }, + reason: { type: "string" } + }, + required: ["channelId", "messageId"] + } + }, + { + name: "discord_create_webhook", + description: "Creates a new webhook for a Discord channel", + inputSchema: { + type: "object", + properties: { + channelId: { type: "string" }, + name: { type: "string" }, + avatar: { type: "string" }, + reason: { type: "string" } + }, + required: ["channelId", "name"] + } + }, + { + name: "discord_send_webhook_message", + description: "Sends a message to a Discord channel using a webhook", + inputSchema: { + type: "object", + properties: { + webhookId: { type: "string" }, + webhookToken: { type: "string" }, + content: { type: "string" }, + username: { type: "string" }, + avatarURL: { type: "string" }, + threadId: { type: "string" } + }, + required: ["webhookId", "webhookToken", "content"] + } + }, + { + name: "discord_edit_webhook", + description: "Edits an existing webhook for a Discord channel", + inputSchema: { + type: "object", + properties: { + webhookId: { type: "string" }, + webhookToken: { type: "string" }, + name: { type: "string" }, + avatar: { type: "string" }, + channelId: { type: "string" }, + reason: { type: "string" } + }, + required: ["webhookId"] + } + }, + { + name: "discord_delete_webhook", + description: "Deletes an existing webhook for a Discord channel", + inputSchema: { + type: "object", + properties: { + webhookId: { type: "string" }, + webhookToken: { type: "string" }, + reason: { type: "string" } + }, + required: ["webhookId"] + } + } +]; \ No newline at end of file diff --git a/src/tools/channel.ts b/src/tools/channel.ts new file mode 100644 index 0000000..71c7c29 --- /dev/null +++ b/src/tools/channel.ts @@ -0,0 +1,241 @@ +import { z } from "zod"; +import { ChannelType } from "discord.js"; +import { ToolContext, ToolResponse } from "./types.js"; +import { + CreateTextChannelSchema, + DeleteChannelSchema, + ReadMessagesSchema, + GetServerInfoSchema +} from "../schemas.js"; + + // Text channel creation handler +export async function createTextChannelHandler( + args: unknown, + context: ToolContext +): Promise { + const { guildId, channelName, topic } = CreateTextChannelSchema.parse(args); + try { + if (!context.client.isReady()) { + return { + content: [{ type: "text", text: "Discord client not logged in. Please use discord_login tool first." }], + isError: true + }; + } + + const guild = await context.client.guilds.fetch(guildId); + if (!guild) { + return { + content: [{ type: "text", text: `Cannot find guild with ID: ${guildId}` }], + isError: true + }; + } + + // Create the text channel + const channel = await guild.channels.create({ + name: channelName, + type: ChannelType.GuildText, + topic: topic + }); + + return { + content: [{ + type: "text", + text: `Successfully created text channel "${channelName}" with ID: ${channel.id}` + }] + }; + } catch (error) { + return { + content: [{ type: "text", text: `Failed to create text channel: ${error}` }], + isError: true + }; + } +} + +// Channel deletion handler +export async function deleteChannelHandler( + args: unknown, + context: ToolContext +): Promise { + const { channelId, reason } = DeleteChannelSchema.parse(args); + try { + if (!context.client.isReady()) { + return { + content: [{ type: "text", text: "Discord client not logged in. Please use discord_login tool first." }], + isError: true + }; + } + + const channel = await context.client.channels.fetch(channelId); + if (!channel) { + return { + content: [{ type: "text", text: `Cannot find channel with ID: ${channelId}` }], + isError: true + }; + } + + // Check if channel can be deleted (has delete method) + if (!('delete' in channel)) { + return { + content: [{ type: "text", text: `This channel type does not support deletion or the bot lacks permissions` }], + isError: true + }; + } + + // Delete the channel + await channel.delete(reason || "Channel deleted via API"); + + return { + content: [{ + type: "text", + text: `Successfully deleted channel with ID: ${channelId}` + }] + }; + } catch (error) { + return { + content: [{ type: "text", text: `Failed to delete channel: ${error}` }], + isError: true + }; + } +} + +// Message reading handler +export async function readMessagesHandler( + args: unknown, + context: ToolContext +): Promise { + const { channelId, limit } = ReadMessagesSchema.parse(args); + try { + if (!context.client.isReady()) { + return { + content: [{ type: "text", text: "Discord client not logged in. Please use discord_login tool first." }], + isError: true + }; + } + + const channel = await context.client.channels.fetch(channelId); + if (!channel) { + return { + content: [{ type: "text", text: `Cannot find channel with ID: ${channelId}` }], + isError: true + }; + } + + // Check if channel has messages (text channel, thread, etc.) + if (!channel.isTextBased() || !('messages' in channel)) { + return { + content: [{ type: "text", text: `Channel type does not support reading messages` }], + isError: true + }; + } + + // Fetch messages + const messages = await channel.messages.fetch({ limit }); + + if (messages.size === 0) { + return { + content: [{ type: "text", text: `No messages found in channel` }] + }; + } + + // Format messages + const formattedMessages = messages.map(msg => ({ + id: msg.id, + content: msg.content, + author: { + id: msg.author.id, + username: msg.author.username, + bot: msg.author.bot + }, + timestamp: msg.createdAt, + attachments: msg.attachments.size, + embeds: msg.embeds.length, + replyTo: msg.reference ? msg.reference.messageId : null + })).sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime()); + + return { + content: [{ + type: "text", + text: JSON.stringify({ + channelId, + messageCount: formattedMessages.length, + messages: formattedMessages + }, null, 2) + }] + }; + } catch (error) { + return { + content: [{ type: "text", text: `Failed to read messages: ${error}` }], + isError: true + }; + } +} + +// Server information handler +export async function getServerInfoHandler( + args: unknown, + context: ToolContext +): Promise { + const { guildId } = GetServerInfoSchema.parse(args); + try { + if (!context.client.isReady()) { + return { + content: [{ type: "text", text: "Discord client not logged in. Please use discord_login tool first." }], + isError: true + }; + } + + const guild = await context.client.guilds.fetch(guildId); + if (!guild) { + return { + content: [{ type: "text", text: `Cannot find guild with ID: ${guildId}` }], + isError: true + }; + } + + // Fetch additional guild data + await guild.fetch(); + + // Fetch channel information + const channels = await guild.channels.fetch(); + + // Categorize channels by type + const channelsByType = { + text: channels.filter(c => c?.type === ChannelType.GuildText).size, + voice: channels.filter(c => c?.type === ChannelType.GuildVoice).size, + category: channels.filter(c => c?.type === ChannelType.GuildCategory).size, + forum: channels.filter(c => c?.type === ChannelType.GuildForum).size, + announcement: channels.filter(c => c?.type === ChannelType.GuildAnnouncement).size, + stage: channels.filter(c => c?.type === ChannelType.GuildStageVoice).size, + total: channels.size + }; + + // Fetch member count + const approximateMemberCount = guild.approximateMemberCount || "unknown"; + + // Format guild information + const guildInfo = { + id: guild.id, + name: guild.name, + description: guild.description, + icon: guild.iconURL(), + owner: guild.ownerId, + createdAt: guild.createdAt, + memberCount: approximateMemberCount, + channels: channelsByType, + features: guild.features, + premium: { + tier: guild.premiumTier, + subscriptions: guild.premiumSubscriptionCount + } + }; + + return { + content: [{ type: "text", text: JSON.stringify(guildInfo, null, 2) }] + }; + } catch (error) { + return { + content: [{ type: "text", text: `Failed to fetch server info: ${error}` }], + isError: true + }; + } +} \ No newline at end of file diff --git a/src/tools/forum.ts b/src/tools/forum.ts new file mode 100644 index 0000000..cb633f6 --- /dev/null +++ b/src/tools/forum.ts @@ -0,0 +1,233 @@ +import { ChannelType, ForumChannel } from 'discord.js'; +import { GetForumChannelsSchema, CreateForumPostSchema, GetForumPostSchema, ReplyToForumSchema, DeleteForumPostSchema } from '../schemas.js'; +import { ToolHandler } from './types.js'; + +export const getForumChannelsHandler: ToolHandler = async (args, { client }) => { + const { guildId } = GetForumChannelsSchema.parse(args); + + try { + if (!client.isReady()) { + return { + content: [{ type: "text", text: "Discord client not logged in. Please use discord_login tool first." }], + isError: true + }; + } + + const guild = await client.guilds.fetch(guildId); + if (!guild) { + return { + content: [{ type: "text", text: `Cannot find guild with ID: ${guildId}` }], + isError: true + }; + } + + // Fetch all channels from the guild + const channels = await guild.channels.fetch(); + + // Filter to get only forum channels + const forumChannels = channels.filter(channel => channel?.type === ChannelType.GuildForum); + + if (forumChannels.size === 0) { + return { + content: [{ type: "text", text: `No forum channels found in guild: ${guild.name}` }] + }; + } + + // Format forum channels information + const forumInfo = forumChannels.map(channel => ({ + id: channel.id, + name: channel.name, + topic: channel.topic || "No topic set" + })); + + return { + content: [{ type: "text", text: JSON.stringify(forumInfo, null, 2) }] + }; + } catch (error) { + return { + content: [{ type: "text", text: `Failed to fetch forum channels: ${error}` }], + isError: true + }; + } +}; + +export const createForumPostHandler: ToolHandler = async (args, { client }) => { + const { forumChannelId, title, content, tags } = CreateForumPostSchema.parse(args); + + try { + if (!client.isReady()) { + return { + content: [{ type: "text", text: "Discord client not logged in. Please use discord_login tool first." }], + isError: true + }; + } + + const channel = await client.channels.fetch(forumChannelId); + if (!channel || channel.type !== ChannelType.GuildForum) { + return { + content: [{ type: "text", text: `Channel ID ${forumChannelId} is not a forum channel.` }], + isError: true + }; + } + + const forumChannel = channel as ForumChannel; + + // Get available tags in the forum + const availableTags = forumChannel.availableTags; + let selectedTagIds: string[] = []; + + // If tags are provided, find their IDs + if (tags && tags.length > 0) { + selectedTagIds = availableTags + .filter(tag => tags.includes(tag.name)) + .map(tag => tag.id); + } + + // Create the forum post + const thread = await forumChannel.threads.create({ + name: title, + message: { + content: content + }, + appliedTags: selectedTagIds.length > 0 ? selectedTagIds : undefined + }); + + return { + content: [{ + type: "text", + text: `Successfully created forum post "${title}" with ID: ${thread.id}` + }] + }; + } catch (error) { + return { + content: [{ type: "text", text: `Failed to create forum post: ${error}` }], + isError: true + }; + } +}; + +export const getForumPostHandler: ToolHandler = async (args, { client }) => { + const { threadId } = GetForumPostSchema.parse(args); + + try { + if (!client.isReady()) { + return { + content: [{ type: "text", text: "Discord client not logged in. Please use discord_login tool first." }], + isError: true + }; + } + + const thread = await client.channels.fetch(threadId); + if (!thread || !(thread.isThread())) { + return { + content: [{ type: "text", text: `Cannot find thread with ID: ${threadId}` }], + isError: true + }; + } + + // Get messages from the thread + const messages = await thread.messages.fetch({ limit: 10 }); + + const threadDetails = { + id: thread.id, + name: thread.name, + parentId: thread.parentId, + messageCount: messages.size, + createdAt: thread.createdAt, + messages: messages.map(msg => ({ + id: msg.id, + content: msg.content, + author: msg.author.tag, + createdAt: msg.createdAt + })) + }; + + return { + content: [{ type: "text", text: JSON.stringify(threadDetails, null, 2) }] + }; + } catch (error) { + return { + content: [{ type: "text", text: `Failed to fetch forum post: ${error}` }], + isError: true + }; + } +}; + +export const replyToForumHandler: ToolHandler = async (args, { client }) => { + const { threadId, message } = ReplyToForumSchema.parse(args); + + try { + if (!client.isReady()) { + return { + content: [{ type: "text", text: "Discord client not logged in. Please use discord_login tool first." }], + isError: true + }; + } + + const thread = await client.channels.fetch(threadId); + if (!thread || !(thread.isThread())) { + return { + content: [{ type: "text", text: `Cannot find thread with ID: ${threadId}` }], + isError: true + }; + } + + if (!('send' in thread)) { + return { + content: [{ type: "text", text: `This thread does not support sending messages` }], + isError: true + }; + } + + // Send the reply + const sentMessage = await thread.send(message); + + return { + content: [{ + type: "text", + text: `Successfully replied to forum post. Message ID: ${sentMessage.id}` + }] + }; + } catch (error) { + return { + content: [{ type: "text", text: `Failed to reply to forum post: ${error}` }], + isError: true + }; + } +}; + +export const deleteForumPostHandler: ToolHandler = async (args, { client }) => { + const { threadId, reason } = DeleteForumPostSchema.parse(args); + + try { + if (!client.isReady()) { + return { + content: [{ type: "text", text: "Discord client not logged in. Please use discord_login tool first." }], + isError: true + }; + } + + const thread = await client.channels.fetch(threadId); + if (!thread || !thread.isThread()) { + return { + content: [{ type: "text", text: `Cannot find forum post/thread with ID: ${threadId}` }], + isError: true + }; + } + + // Delete the forum post/thread + await thread.delete(reason || "Forum post deleted via API"); + + return { + content: [{ + type: "text", + text: `Successfully deleted forum post/thread with ID: ${threadId}` + }] + }; + } catch (error) { + return { + content: [{ type: "text", text: `Failed to delete forum post: ${error}` }], + isError: true + }; + } +}; \ No newline at end of file diff --git a/src/tools/login.ts b/src/tools/login.ts new file mode 100644 index 0000000..213f19b --- /dev/null +++ b/src/tools/login.ts @@ -0,0 +1,33 @@ +import { DiscordLoginSchema } from '../schemas.js'; +import { ToolHandler } from './types.js'; + +export const loginHandler: ToolHandler = async (args, { client }) => { + DiscordLoginSchema.parse(args); + + try { + // Check if client is already logged in + if (client.isReady()) { + return { + content: [{ type: "text", text: `Already logged in as: ${client.user?.tag}` }] + }; + } + + // loginHandler doesn't directly handle token, it needs to be set before invocation + if (!client.token) { + return { + content: [{ type: "text", text: "Discord token not configured. Cannot log in." }], + isError: true + }; + } + + await client.login(client.token); + return { + content: [{ type: "text", text: `Successfully logged in to Discord: ${client.user?.tag}` }] + }; + } catch (error) { + return { + content: [{ type: "text", text: `Login failed: ${error}` }], + isError: true + }; + } +}; \ No newline at end of file diff --git a/src/tools/reactions.ts b/src/tools/reactions.ts new file mode 100644 index 0000000..6e501ba --- /dev/null +++ b/src/tools/reactions.ts @@ -0,0 +1,224 @@ +import { z } from "zod"; +import { ToolContext, ToolResponse } from "./types.js"; +import { + AddReactionSchema, + AddMultipleReactionsSchema, + RemoveReactionSchema, + DeleteMessageSchema +} from "../schemas.js"; + +// Add reaction handler +export async function addReactionHandler( + args: unknown, + context: ToolContext +): Promise { + const { channelId, messageId, emoji } = AddReactionSchema.parse(args); + try { + if (!context.client.isReady()) { + return { + content: [{ type: "text", text: "Discord client not logged in. Please use discord_login tool first." }], + isError: true + }; + } + + const channel = await context.client.channels.fetch(channelId); + if (!channel || !channel.isTextBased() || !('messages' in channel)) { + return { + content: [{ type: "text", text: `Cannot find text channel with ID: ${channelId}` }], + isError: true + }; + } + + const message = await channel.messages.fetch(messageId); + if (!message) { + return { + content: [{ type: "text", text: `Cannot find message with ID: ${messageId}` }], + isError: true + }; + } + + // Add the reaction + await message.react(emoji); + + return { + content: [{ + type: "text", + text: `Successfully added reaction ${emoji} to message ID: ${messageId}` + }] + }; + } catch (error) { + return { + content: [{ type: "text", text: `Failed to add reaction: ${error}` }], + isError: true + }; + } +} + +// Add multiple reactions handler +export async function addMultipleReactionsHandler( + args: unknown, + context: ToolContext +): Promise { + const { channelId, messageId, emojis } = AddMultipleReactionsSchema.parse(args); + try { + if (!context.client.isReady()) { + return { + content: [{ type: "text", text: "Discord client not logged in. Please use discord_login tool first." }], + isError: true + }; + } + + const channel = await context.client.channels.fetch(channelId); + if (!channel || !channel.isTextBased() || !('messages' in channel)) { + return { + content: [{ type: "text", text: `Cannot find text channel with ID: ${channelId}` }], + isError: true + }; + } + + const message = await channel.messages.fetch(messageId); + if (!message) { + return { + content: [{ type: "text", text: `Cannot find message with ID: ${messageId}` }], + isError: true + }; + } + + // Add each reaction sequentially + for (const emoji of emojis) { + await message.react(emoji); + // Small delay to prevent rate limiting + await new Promise(resolve => setTimeout(resolve, 300)); + } + + return { + content: [{ + type: "text", + text: `Successfully added ${emojis.length} reactions to message ID: ${messageId}` + }] + }; + } catch (error) { + return { + content: [{ type: "text", text: `Failed to add reactions: ${error}` }], + isError: true + }; + } +} + +// Remove reaction handler +export async function removeReactionHandler( + args: unknown, + context: ToolContext +): Promise { + const { channelId, messageId, emoji, userId } = RemoveReactionSchema.parse(args); + try { + if (!context.client.isReady()) { + return { + content: [{ type: "text", text: "Discord client not logged in. Please use discord_login tool first." }], + isError: true + }; + } + + const channel = await context.client.channels.fetch(channelId); + if (!channel || !channel.isTextBased() || !('messages' in channel)) { + return { + content: [{ type: "text", text: `Cannot find text channel with ID: ${channelId}` }], + isError: true + }; + } + + const message = await channel.messages.fetch(messageId); + if (!message) { + return { + content: [{ type: "text", text: `Cannot find message with ID: ${messageId}` }], + isError: true + }; + } + + // Get the reactions + const reactions = message.reactions.cache; + + // Find the specific reaction + const reaction = reactions.find(r => r.emoji.toString() === emoji || r.emoji.name === emoji); + + if (!reaction) { + return { + content: [{ type: "text", text: `Reaction ${emoji} not found on message ID: ${messageId}` }], + isError: true + }; + } + + if (userId) { + // Remove a specific user's reaction + await reaction.users.remove(userId); + return { + content: [{ + type: "text", + text: `Successfully removed reaction ${emoji} from user ID: ${userId} on message ID: ${messageId}` + }] + }; + } else { + // Remove bot's reaction + await reaction.users.remove(context.client.user.id); + return { + content: [{ + type: "text", + text: `Successfully removed bot's reaction ${emoji} from message ID: ${messageId}` + }] + }; + } + } catch (error) { + return { + content: [{ type: "text", text: `Failed to remove reaction: ${error}` }], + isError: true + }; + } +} + +// Delete message handler +export async function deleteMessageHandler( + args: unknown, + context: ToolContext +): Promise { + const { channelId, messageId, reason } = DeleteMessageSchema.parse(args); + try { + if (!context.client.isReady()) { + return { + content: [{ type: "text", text: "Discord client not logged in. Please use discord_login tool first." }], + isError: true + }; + } + + const channel = await context.client.channels.fetch(channelId); + if (!channel || !channel.isTextBased() || !('messages' in channel)) { + return { + content: [{ type: "text", text: `Cannot find text channel with ID: ${channelId}` }], + isError: true + }; + } + + // Fetch the message + const message = await channel.messages.fetch(messageId); + if (!message) { + return { + content: [{ type: "text", text: `Cannot find message with ID: ${messageId}` }], + isError: true + }; + } + + // Delete the message + await message.delete(); + + return { + content: [{ + type: "text", + text: `Successfully deleted message with ID: ${messageId} from channel: ${channelId}` + }] + }; + } catch (error) { + return { + content: [{ type: "text", text: `Failed to delete message: ${error}` }], + isError: true + }; + } +} \ No newline at end of file diff --git a/src/tools/send-message.ts b/src/tools/send-message.ts new file mode 100644 index 0000000..f1dfca9 --- /dev/null +++ b/src/tools/send-message.ts @@ -0,0 +1,41 @@ +import { SendMessageSchema } from '../schemas.js'; +import { ToolHandler } from './types.js'; + +export const sendMessageHandler: ToolHandler = async (args, { client }) => { + const { channelId, message } = SendMessageSchema.parse(args); + + try { + if (!client.isReady()) { + return { + content: [{ type: "text", text: "Discord client not logged in. Please use discord_login tool first." }], + isError: true + }; + } + + const channel = await client.channels.fetch(channelId); + if (!channel || !channel.isTextBased()) { + return { + content: [{ type: "text", text: `Cannot find text channel ID: ${channelId}` }], + isError: true + }; + } + + // Ensure channel is text-based and can send messages + if ('send' in channel) { + await channel.send(message); + return { + content: [{ type: "text", text: `Message successfully sent to channel ID: ${channelId}` }] + }; + } else { + return { + content: [{ type: "text", text: `This channel type does not support sending messages` }], + isError: true + }; + } + } catch (error) { + return { + content: [{ type: "text", text: `Send message failed: ${error}` }], + isError: true + }; + } +}; \ No newline at end of file diff --git a/src/tools/tools.ts b/src/tools/tools.ts new file mode 100644 index 0000000..1ae1fe4 --- /dev/null +++ b/src/tools/tools.ts @@ -0,0 +1,61 @@ +import { Client } from "discord.js"; +import { z } from "zod"; +import { ToolResponse, ToolContext, ToolHandler } from "./types.js"; +import { loginHandler } from './login.js'; +import { sendMessageHandler } from './send-message.js'; +import { + getForumChannelsHandler, + createForumPostHandler, + getForumPostHandler, + replyToForumHandler, + deleteForumPostHandler +} from './forum.js'; +import { + createTextChannelHandler, + deleteChannelHandler, + readMessagesHandler, + getServerInfoHandler +} from './channel.js'; +import { + addReactionHandler, + addMultipleReactionsHandler, + removeReactionHandler, + deleteMessageHandler +} from './reactions.js'; +import { + createWebhookHandler, + sendWebhookMessageHandler, + editWebhookHandler, + deleteWebhookHandler +} from './webhooks.js'; + +// Export tool handlers +export { + loginHandler, + sendMessageHandler, + getForumChannelsHandler, + createForumPostHandler, + getForumPostHandler, + replyToForumHandler, + deleteForumPostHandler, + createTextChannelHandler, + deleteChannelHandler, + readMessagesHandler, + getServerInfoHandler, + addReactionHandler, + addMultipleReactionsHandler, + removeReactionHandler, + deleteMessageHandler, + createWebhookHandler, + sendWebhookMessageHandler, + editWebhookHandler, + deleteWebhookHandler +}; + +// Export common types +export { ToolResponse, ToolContext, ToolHandler }; + +// Create tool context +export function createToolContext(client: Client): ToolContext { + return { client }; +} \ No newline at end of file diff --git a/src/tools/types.ts b/src/tools/types.ts new file mode 100644 index 0000000..410dbcf --- /dev/null +++ b/src/tools/types.ts @@ -0,0 +1,13 @@ +import { Client } from "discord.js"; + +export interface ToolResponse { + content: { type: string; text: string }[]; + isError?: boolean; + [key: string]: unknown; +} + +export interface ToolContext { + client: Client; +} + +export type ToolHandler = (args: T, context: ToolContext) => Promise; \ No newline at end of file diff --git a/src/tools/webhooks.ts b/src/tools/webhooks.ts new file mode 100644 index 0000000..2d1ff60 --- /dev/null +++ b/src/tools/webhooks.ts @@ -0,0 +1,186 @@ +import { z } from "zod"; +import { ToolContext, ToolResponse } from "./types.js"; +import { + CreateWebhookSchema, + SendWebhookMessageSchema, + EditWebhookSchema, + DeleteWebhookSchema +} from "../schemas.js"; + +// Create webhook handler +export async function createWebhookHandler( + args: unknown, + context: ToolContext +): Promise { + const { channelId, name, avatar, reason } = CreateWebhookSchema.parse(args); + try { + if (!context.client.isReady()) { + return { + content: [{ type: "text", text: "Discord client not logged in. Please use discord_login tool first." }], + isError: true + }; + } + + const channel = await context.client.channels.fetch(channelId); + if (!channel || !channel.isTextBased()) { + return { + content: [{ type: "text", text: `Cannot find text channel with ID: ${channelId}` }], + isError: true + }; + } + + // Check if the channel supports webhooks + if (!('createWebhook' in channel)) { + return { + content: [{ type: "text", text: `Channel type does not support webhooks: ${channelId}` }], + isError: true + }; + } + + // Create the webhook + const webhook = await channel.createWebhook({ + name: name, + avatar: avatar, + reason: reason + }); + + return { + content: [{ + type: "text", + text: `Successfully created webhook with ID: ${webhook.id} and token: ${webhook.token}` + }] + }; + } catch (error) { + return { + content: [{ type: "text", text: `Failed to create webhook: ${error}` }], + isError: true + }; + } +} + +// Send webhook message handler +export async function sendWebhookMessageHandler( + args: unknown, + context: ToolContext +): Promise { + const { webhookId, webhookToken, content, username, avatarURL, threadId } = SendWebhookMessageSchema.parse(args); + try { + if (!context.client.isReady()) { + return { + content: [{ type: "text", text: "Discord client not logged in. Please use discord_login tool first." }], + isError: true + }; + } + + const webhook = await context.client.fetchWebhook(webhookId, webhookToken); + if (!webhook) { + return { + content: [{ type: "text", text: `Cannot find webhook with ID: ${webhookId}` }], + isError: true + }; + } + + // Send the message + await webhook.send({ + content: content, + username: username, + avatarURL: avatarURL, + threadId: threadId + }); + + return { + content: [{ + type: "text", + text: `Successfully sent webhook message to webhook ID: ${webhookId}` + }] + }; + } catch (error) { + return { + content: [{ type: "text", text: `Failed to send webhook message: ${error}` }], + isError: true + }; + } +} + +// Edit webhook handler +export async function editWebhookHandler( + args: unknown, + context: ToolContext +): Promise { + const { webhookId, webhookToken, name, avatar, channelId, reason } = EditWebhookSchema.parse(args); + try { + if (!context.client.isReady()) { + return { + content: [{ type: "text", text: "Discord client not logged in. Please use discord_login tool first." }], + isError: true + }; + } + + const webhook = await context.client.fetchWebhook(webhookId, webhookToken); + if (!webhook) { + return { + content: [{ type: "text", text: `Cannot find webhook with ID: ${webhookId}` }], + isError: true + }; + } + + // Edit the webhook + await webhook.edit({ + name: name, + avatar: avatar, + channel: channelId, + reason: reason + }); + + return { + content: [{ + type: "text", + text: `Successfully edited webhook with ID: ${webhook.id}` + }] + }; + } catch (error) { + return { + content: [{ type: "text", text: `Failed to edit webhook: ${error}` }], + isError: true + }; + } +} + +// Delete webhook handler +export async function deleteWebhookHandler( + args: unknown, + context: ToolContext +): Promise { + const { webhookId, webhookToken, reason } = DeleteWebhookSchema.parse(args); + try { + if (!context.client.isReady()) { + return { + content: [{ type: "text", text: "Discord client not logged in. Please use discord_login tool first." }], + isError: true + }; + } + + const webhook = await context.client.fetchWebhook(webhookId, webhookToken); + if (!webhook) { + return { + content: [{ type: "text", text: `Cannot find webhook with ID: ${webhookId}` }], + isError: true + }; + } + + // Delete the webhook + await webhook.delete(reason || "Webhook deleted via API"); + + return { + content: [{ + type: "text", + text: `Successfully deleted webhook with ID: ${webhook.id}` + }] + }; + } catch (error) { + return { + content: [{ type: "text", text: `Failed to delete webhook: ${error}` }], + isError: true + }; + } +} \ No newline at end of file