feat: add forum tags, update forum post, and edit message tools

Add three new Discord tools:
- discord_get_forum_tags: retrieve available tags (id, name, emoji) for a forum channel
- discord_update_forum_post: update a forum post's title, tags, archived/locked status
- discord_edit_message: edit a bot-authored message with ownership validation

Tags can be specified by name or ID in update_forum_post for convenience.
This commit is contained in:
Claude 2026-02-26 15:17:54 -05:00 committed by Warren
parent 6558290613
commit 6b9dde03e9
6 changed files with 242 additions and 10 deletions

View File

@ -101,6 +101,24 @@ export const DeleteForumPostSchema = z.object({
reason: z.string().optional()
});
export const GetForumTagsSchema = z.object({
forumChannelId: z.string()
});
export const UpdateForumPostSchema = z.object({
threadId: z.string(),
name: z.string().optional(),
tags: z.array(z.string()).optional(),
archived: z.boolean().optional(),
locked: z.boolean().optional()
});
export const EditMessageSchema = z.object({
channelId: z.string(),
messageId: z.string(),
content: z.string()
});
export const DeleteMessageSchema = z.object({
channelId: z.string(),
messageId: z.string(),

View File

@ -16,6 +16,9 @@ import {
listForumThreadsHandler,
replyToForumHandler,
deleteForumPostHandler,
getForumTagsHandler,
updateForumPostHandler,
editMessageHandler,
createTextChannelHandler,
deleteChannelHandler,
readMessagesHandler,
@ -126,6 +129,21 @@ export class DiscordMCPServer {
toolResponse = await deleteForumPostHandler(args, this.toolContext);
return toolResponse;
case "discord_get_forum_tags":
this.logClientState("before discord_get_forum_tags handler");
toolResponse = await getForumTagsHandler(args, this.toolContext);
return toolResponse;
case "discord_update_forum_post":
this.logClientState("before discord_update_forum_post handler");
toolResponse = await updateForumPostHandler(args, this.toolContext);
return toolResponse;
case "discord_edit_message":
this.logClientState("before discord_edit_message handler");
toolResponse = await editMessageHandler(args, this.toolContext);
return toolResponse;
case "discord_create_text_channel":
this.logClientState("before discord_create_text_channel handler");
toolResponse = await createTextChannelHandler(args, this.toolContext);

View File

@ -235,6 +235,45 @@ export const toolList = [
required: ["threadId"]
}
},
{
name: "discord_get_forum_tags",
description: "Gets all available tags for a Discord forum channel, including tag IDs, names, and emoji",
inputSchema: {
type: "object",
properties: {
forumChannelId: { type: "string", description: "The ID of the forum channel to get tags from" }
},
required: ["forumChannelId"]
}
},
{
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.",
inputSchema: {
type: "object",
properties: {
threadId: { type: "string", description: "The ID of the forum post/thread to update" },
name: { type: "string", description: "New title for the forum post" },
tags: { type: "array", items: { type: "string" }, description: "Tags to apply (by name or ID). Replaces all existing tags." },
archived: { type: "boolean", description: "Whether to archive or unarchive the post" },
locked: { type: "boolean", description: "Whether to lock or unlock the post" }
},
required: ["threadId"]
}
},
{
name: "discord_edit_message",
description: "Edits a message previously sent by the bot. Only messages authored by the bot can be edited.",
inputSchema: {
type: "object",
properties: {
channelId: { type: "string", description: "The ID of the channel containing the message" },
messageId: { type: "string", description: "The ID of the message to edit" },
content: { type: "string", description: "The new content for the message" }
},
required: ["channelId", "messageId", "content"]
}
},
{
name: "discord_delete_message",
description: "Deletes a specific message from a Discord text channel",

View File

@ -1,5 +1,5 @@
import { ChannelType, ForumChannel } from 'discord.js';
import { GetForumChannelsSchema, CreateForumPostSchema, GetForumPostSchema, ListForumThreadsSchema, ReplyToForumSchema, DeleteForumPostSchema } from '../schemas.js';
import { GetForumChannelsSchema, CreateForumPostSchema, GetForumPostSchema, ListForumThreadsSchema, ReplyToForumSchema, DeleteForumPostSchema, GetForumTagsSchema, UpdateForumPostSchema } from '../schemas.js';
import { ToolHandler } from './types.js';
import { handleDiscordError } from "../errorHandler.js";
@ -280,7 +280,7 @@ export const replyToForumHandler: ToolHandler = async (args, { client }) => {
export const deleteForumPostHandler: ToolHandler = async (args, { client }) => {
const { threadId, reason } = DeleteForumPostSchema.parse(args);
try {
if (!client.isReady()) {
return {
@ -301,12 +301,113 @@ export const deleteForumPostHandler: ToolHandler = async (args, { client }) => {
await thread.delete(reason || "Forum post deleted via API");
return {
content: [{
type: "text",
text: `Successfully deleted forum post/thread with ID: ${threadId}`
content: [{
type: "text",
text: `Successfully deleted forum post/thread with ID: ${threadId}`
}]
};
} catch (error) {
return handleDiscordError(error);
}
};
};
export const getForumTagsHandler: ToolHandler = async (args, { client }) => {
const { forumChannelId } = GetForumTagsSchema.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 tags = forumChannel.availableTags.map(tag => ({
id: tag.id,
name: tag.name,
moderated: tag.moderated,
emoji: tag.emoji ? (tag.emoji.name || tag.emoji.id) : null
}));
return {
content: [{ type: "text", text: JSON.stringify(tags, null, 2) }]
};
} catch (error) {
return handleDiscordError(error);
}
};
export const updateForumPostHandler: ToolHandler = async (args, { client }) => {
const { threadId, name, tags, archived, locked } = UpdateForumPostSchema.parse(args);
try {
if (!client.isReady()) {
return {
content: [{ type: "text", text: "Discord client not logged in." }],
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
};
}
const editOptions: any = {};
if (name !== undefined) editOptions.name = name;
if (archived !== undefined) editOptions.archived = archived;
if (locked !== undefined) editOptions.locked = locked;
// Resolve tag names to IDs if tags are provided
if (tags !== undefined) {
const parent = thread.parent;
if (parent && parent.type === ChannelType.GuildForum) {
const forumChannel = parent as ForumChannel;
const availableTags = forumChannel.availableTags;
const tagIds = tags.map(tagInput => {
// Accept either tag name or tag ID
const byName = availableTags.find(t => t.name === tagInput);
if (byName) return byName.id;
const byId = availableTags.find(t => t.id === tagInput);
if (byId) return byId.id;
return null;
}).filter((id): id is string => id !== null);
editOptions.appliedTags = tagIds;
} else {
return {
content: [{ type: "text", text: `Thread's parent channel is not a forum channel. Tags can only be applied to forum posts.` }],
isError: true
};
}
}
const updated = await thread.edit(editOptions);
const changes: string[] = [];
if (name !== undefined) changes.push(`name → "${name}"`);
if (tags !== undefined) changes.push(`tags → [${tags.join(', ')}]`);
if (archived !== undefined) changes.push(`archived → ${archived}`);
if (locked !== undefined) changes.push(`locked → ${locked}`);
return {
content: [{
type: "text",
text: `Successfully updated forum post ${threadId}: ${changes.join(', ')}`
}]
};
} catch (error) {
return handleDiscordError(error);
}
};

View File

@ -1,4 +1,4 @@
import { SendMessageSchema } from '../schemas.js';
import { SendMessageSchema, EditMessageSchema } from '../schemas.js';
import { ToolHandler } from './types.js';
import { handleDiscordError } from "../errorHandler.js";
@ -68,4 +68,55 @@ export const sendMessageHandler: ToolHandler = async (args, { client }) => {
} catch (error) {
return handleDiscordError(error);
}
};
};
export const editMessageHandler: ToolHandler = async (args, { client }) => {
const { channelId, messageId, content } = EditMessageSchema.parse(args);
try {
if (!client.isReady()) {
return {
content: [{ type: "text", text: "Discord client not logged in." }],
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
};
}
if (!('messages' in channel)) {
return {
content: [{ type: "text", text: `This channel type does not support message editing` }],
isError: true
};
}
const message = await channel.messages.fetch(messageId);
if (!message) {
return {
content: [{ type: "text", text: `Cannot find message with ID: ${messageId}` }],
isError: true
};
}
if (message.author.id !== client.user?.id) {
return {
content: [{ type: "text", text: `Cannot edit message: only messages sent by the bot can be edited` }],
isError: true
};
}
await message.edit(content);
return {
content: [{ type: "text", text: `Successfully edited message ${messageId} in channel ${channelId}` }]
};
} catch (error) {
return handleDiscordError(error);
}
};

View File

@ -2,14 +2,16 @@ 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 { sendMessageHandler, editMessageHandler } from './send-message.js';
import {
getForumChannelsHandler,
createForumPostHandler,
getForumPostHandler,
listForumThreadsHandler,
replyToForumHandler,
deleteForumPostHandler
deleteForumPostHandler,
getForumTagsHandler,
updateForumPostHandler
} from './forum.js';
import {
createTextChannelHandler,
@ -47,6 +49,9 @@ export {
listForumThreadsHandler,
replyToForumHandler,
deleteForumPostHandler,
getForumTagsHandler,
updateForumPostHandler,
editMessageHandler,
createTextChannelHandler,
deleteChannelHandler,
readMessagesHandler,