feat: add forum channel creation, channel editing, and forum tag management

Add three new tools:
- discord_create_forum_channel: create forum channels with optional parent category
- discord_edit_channel: edit channel name, topic, parent category, and position
- discord_set_forum_tags: set available tags on a forum channel (replaces all)
This commit is contained in:
Warren 2026-03-17 17:15:42 -04:00
parent a2890ae1b8
commit ed42331176
6 changed files with 242 additions and 1 deletions

View File

@ -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(),

View File

@ -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);

View File

@ -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.",

View File

@ -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<ToolResponse> {
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<ToolResponse> {
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,

View File

@ -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);

View File

@ -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,