diff --git a/src/schemas.ts b/src/schemas.ts index e1b5212..bf94c61 100644 --- a/src/schemas.ts +++ b/src/schemas.ts @@ -56,6 +56,27 @@ export const CreateTextChannelSchema = z.object({ reason: z.string().optional() }); +export const CreateForumChannelSchema = z.object({ + guildId: z.string({ description: "The ID of the server (guild) to create the forum channel in." }), + name: z.string({ description: "The name of the forum channel to create." }), + topic: z.string({ description: "The forum channel guidelines/description." }).optional(), + categoryId: z.string({ description: "The ID of the parent category to create the channel under." }).optional(), + reason: z.string({ description: "Optional reason for audit logs." }).optional() +}, { + description: "Create a new forum channel in a specified server (guild), optionally under a category." +}); + +export const EditChannelSchema = z.object({ + channelId: z.string({ description: "The ID of the channel to edit." }), + name: z.string({ description: "New name for the channel." }).optional(), + topic: z.string({ description: "New topic for the channel." }).optional(), + parentId: z.string({ description: "The ID of a category to move the channel under." }).optional(), + position: z.number({ description: "New position of the channel in the list." }).optional(), + reason: z.string({ description: "Optional reason for audit logs." }).optional() +}, { + description: "Edit a Discord channel's name, topic, parent category, or position." +}); + // Category schemas export const CreateCategorySchema = z.object({ guildId: z.string({ description: "The ID of the server (guild) where the category will be created." }), @@ -146,6 +167,17 @@ export const UpdateForumPostSchema = z.object({ locked: z.boolean().optional() }); +export const SetForumTagsSchema = z.object({ + forumChannelId: z.string({ description: "The ID of the forum channel to set tags on." }), + tags: z.array(z.object({ + name: z.string({ description: "Tag name." }), + emoji: z.string({ description: "Unicode emoji for the tag (e.g. '🔬')." }).optional(), + moderated: z.boolean({ description: "Whether only moderators can apply this tag (default: false)." }).optional() + })) +}, { + description: "Sets the available tags for a Discord forum channel. Replaces all existing tags." +}); + export const EditMessageSchema = z.object({ channelId: z.string(), messageId: z.string(), diff --git a/src/server.ts b/src/server.ts index 51ec7d7..114a238 100644 --- a/src/server.ts +++ b/src/server.ts @@ -17,9 +17,12 @@ import { replyToForumHandler, deleteForumPostHandler, getForumTagsHandler, + setForumTagsHandler, updateForumPostHandler, editMessageHandler, createTextChannelHandler, + createForumChannelHandler, + editChannelHandler, deleteChannelHandler, readMessagesHandler, getServerInfoHandler, @@ -145,6 +148,11 @@ export class DiscordMCPServer { toolResponse = await getForumTagsHandler(args, this.toolContext); return toolResponse; + case "discord_set_forum_tags": + this.logClientState("before discord_set_forum_tags handler"); + toolResponse = await setForumTagsHandler(args, this.toolContext); + return toolResponse; + case "discord_update_forum_post": this.logClientState("before discord_update_forum_post handler"); toolResponse = await updateForumPostHandler(args, this.toolContext); @@ -160,6 +168,16 @@ export class DiscordMCPServer { toolResponse = await createTextChannelHandler(args, this.toolContext); return toolResponse; + case "discord_create_forum_channel": + this.logClientState("before discord_create_forum_channel handler"); + toolResponse = await createForumChannelHandler(args, this.toolContext); + return toolResponse; + + case "discord_edit_channel": + this.logClientState("before discord_edit_channel handler"); + toolResponse = await editChannelHandler(args, this.toolContext); + return toolResponse; + case "discord_delete_channel": this.logClientState("before discord_delete_channel handler"); toolResponse = await deleteChannelHandler(args, this.toolContext); diff --git a/src/toolList.ts b/src/toolList.ts index 2fe1ce4..68eaf2a 100644 --- a/src/toolList.ts +++ b/src/toolList.ts @@ -141,6 +141,36 @@ export const toolList = [ required: ["guildId", "channelName"] } }, + { + name: "discord_create_forum_channel", + description: "Creates a new forum channel in a Discord server, optionally under a category", + inputSchema: { + type: "object", + properties: { + guildId: { type: "string" }, + name: { type: "string" }, + topic: { type: "string", description: "The forum channel guidelines/description" }, + categoryId: { type: "string", description: "The ID of the parent category to create the channel under" } + }, + required: ["guildId", "name"] + } + }, + { + name: "discord_edit_channel", + description: "Edits a Discord channel's name, topic, parent category, or position", + inputSchema: { + type: "object", + properties: { + channelId: { type: "string", description: "The ID of the channel to edit" }, + name: { type: "string", description: "New name for the channel" }, + topic: { type: "string", description: "New topic for the channel" }, + parentId: { type: "string", description: "The ID of a category to move the channel under" }, + position: { type: "number", description: "New position of the channel in the list" }, + reason: { type: "string", description: "Reason for editing (shown in audit log)" } + }, + required: ["channelId"] + } + }, { name: "discord_delete_channel", description: "Deletes a Discord channel with an optional reason", @@ -247,6 +277,30 @@ export const toolList = [ required: ["forumChannelId"] } }, + { + name: "discord_set_forum_tags", + description: "Sets the available tags for a Discord forum channel. Replaces all existing tags.", + inputSchema: { + type: "object", + properties: { + forumChannelId: { type: "string", description: "The ID of the forum channel to set tags on" }, + tags: { + type: "array", + description: "Array of tag objects to set on the forum channel", + items: { + type: "object", + properties: { + name: { type: "string", description: "Tag name" }, + emoji: { type: "string", description: "Unicode emoji for the tag (e.g. '🔬')" }, + moderated: { type: "boolean", description: "Whether only moderators can apply this tag (default: false)" } + }, + required: ["name"] + } + } + }, + required: ["forumChannelId", "tags"] + } + }, { name: "discord_update_forum_post", description: "Updates a forum post's title, applied tags, archived status, or locked status. Tags can be specified by name or ID.", diff --git a/src/tools/channel.ts b/src/tools/channel.ts index 231cbe0..7e20437 100644 --- a/src/tools/channel.ts +++ b/src/tools/channel.ts @@ -3,6 +3,8 @@ import { ChannelType, PermissionsBitField } from "discord.js"; import { ToolContext, ToolResponse } from "./types.js"; import { CreateTextChannelSchema, + CreateForumChannelSchema, + EditChannelSchema, DeleteChannelSchema, ReadMessagesSchema, CreateCategorySchema, @@ -151,6 +153,96 @@ export async function createTextChannelHandler( } } +// Forum channel creation handler +export async function createForumChannelHandler( + args: unknown, + context: ToolContext +): Promise { + const { guildId, name, topic, categoryId, reason } = CreateForumChannelSchema.parse(args); + try { + if (!context.client.isReady()) { + return { + content: [{ type: "text", text: "Discord client not logged in." }], + 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 + }; + } + + const channelOptions: any = { + name, + type: ChannelType.GuildForum + }; + if (topic) channelOptions.topic = topic; + if (categoryId) channelOptions.parent = categoryId; + if (reason) channelOptions.reason = reason; + const channel = await guild.channels.create(channelOptions); + + return { + content: [{ + type: "text", + text: `Successfully created forum channel "${name}" with ID: ${channel.id}` + }] + }; + } catch (error) { + return handleDiscordError(error); + } +} + +// Edit channel handler +export async function editChannelHandler( + args: unknown, + context: ToolContext +): Promise { + const { channelId, name, topic, parentId, position, reason } = EditChannelSchema.parse(args); + try { + if (!context.client.isReady()) { + return { + content: [{ type: "text", text: "Discord client not logged in." }], + 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 + }; + } + + if (!('edit' in channel)) { + return { + content: [{ type: "text", text: `This channel type does not support editing` }], + isError: true + }; + } + + const update: any = {}; + if (name) update.name = name; + if (topic !== undefined) update.topic = topic; + if (parentId) update.parent = parentId; + if (typeof position === "number") update.position = position; + if (reason) update.reason = reason; + await channel.edit(update); + + return { + content: [{ + type: "text", + text: `Successfully edited channel with ID: ${channelId}` + }] + }; + } catch (error) { + return handleDiscordError(error); + } +} + // Channel deletion handler export async function deleteChannelHandler( args: unknown, diff --git a/src/tools/forum.ts b/src/tools/forum.ts index 2439605..e9f24f2 100644 --- a/src/tools/forum.ts +++ b/src/tools/forum.ts @@ -1,5 +1,5 @@ import { ChannelType, ForumChannel } from 'discord.js'; -import { GetForumChannelsSchema, CreateForumPostSchema, GetForumPostSchema, ListForumThreadsSchema, ReplyToForumSchema, DeleteForumPostSchema, GetForumTagsSchema, UpdateForumPostSchema } from '../schemas.js'; +import { GetForumChannelsSchema, CreateForumPostSchema, GetForumPostSchema, ListForumThreadsSchema, ReplyToForumSchema, DeleteForumPostSchema, GetForumTagsSchema, SetForumTagsSchema, UpdateForumPostSchema } from '../schemas.js'; import { ToolHandler } from './types.js'; import { handleDiscordError } from "../errorHandler.js"; @@ -346,6 +346,45 @@ export const getForumTagsHandler: ToolHandler = async (args, { client }) => { } }; +export const setForumTagsHandler: ToolHandler = async (args, { client }) => { + const { forumChannelId, tags } = SetForumTagsSchema.parse(args); + + try { + if (!client.isReady()) { + return { + content: [{ type: "text", text: "Discord client not logged in." }], + 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; + const newTags = tags.map(tag => ({ + name: tag.name, + moderated: tag.moderated ?? false, + emoji: tag.emoji ? { name: tag.emoji } : null + })); + + await forumChannel.setAvailableTags(newTags as any); + + return { + content: [{ + type: "text", + text: `Successfully set ${tags.length} tag(s) on forum channel ${forumChannelId}: ${tags.map(t => t.name).join(', ')}` + }] + }; + } catch (error) { + return handleDiscordError(error); + } +}; + export const updateForumPostHandler: ToolHandler = async (args, { client }) => { const { threadId, name, tags, archived, locked } = UpdateForumPostSchema.parse(args); diff --git a/src/tools/tools.ts b/src/tools/tools.ts index f683c6d..710a217 100644 --- a/src/tools/tools.ts +++ b/src/tools/tools.ts @@ -11,10 +11,13 @@ import { replyToForumHandler, deleteForumPostHandler, getForumTagsHandler, + setForumTagsHandler, updateForumPostHandler } from './forum.js'; import { createTextChannelHandler, + createForumChannelHandler, + editChannelHandler, deleteChannelHandler, readMessagesHandler, createCategoryHandler, @@ -63,9 +66,12 @@ export { replyToForumHandler, deleteForumPostHandler, getForumTagsHandler, + setForumTagsHandler, updateForumPostHandler, editMessageHandler, createTextChannelHandler, + createForumChannelHandler, + editChannelHandler, deleteChannelHandler, readMessagesHandler, getServerInfoHandler,