Skip to content

Commit 53ed667

Browse files
authored
Merge pull request #24 from WarrenParks/feat/forum-tags-edit-message
feat: add forum tags, update forum post, and edit message tools
2 parents ef0391f + b7e0098 commit 53ed667

File tree

6 files changed

+250
-10
lines changed

6 files changed

+250
-10
lines changed

src/schemas.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,24 @@ export const DeleteForumPostSchema = z.object({
135135
description: "Delete a forum post (thread) by its ID."
136136
});
137137

138+
export const GetForumTagsSchema = z.object({
139+
forumChannelId: z.string()
140+
});
141+
142+
export const UpdateForumPostSchema = z.object({
143+
threadId: z.string(),
144+
name: z.string().optional(),
145+
tags: z.array(z.string()).optional(),
146+
archived: z.boolean().optional(),
147+
locked: z.boolean().optional()
148+
});
149+
150+
export const EditMessageSchema = z.object({
151+
channelId: z.string(),
152+
messageId: z.string(),
153+
content: z.string()
154+
});
155+
138156
export const DeleteMessageSchema = z.object({
139157
channelId: z.string({ description: "The ID of the channel containing the message to delete." }),
140158
messageId: z.string({ description: "The ID of the message to delete." }),

src/server.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ import {
1616
listForumThreadsHandler,
1717
replyToForumHandler,
1818
deleteForumPostHandler,
19+
getForumTagsHandler,
20+
updateForumPostHandler,
21+
editMessageHandler,
1922
createTextChannelHandler,
2023
deleteChannelHandler,
2124
readMessagesHandler,
@@ -126,6 +129,21 @@ export class DiscordMCPServer {
126129
toolResponse = await deleteForumPostHandler(args, this.toolContext);
127130
return toolResponse;
128131

132+
case "discord_get_forum_tags":
133+
this.logClientState("before discord_get_forum_tags handler");
134+
toolResponse = await getForumTagsHandler(args, this.toolContext);
135+
return toolResponse;
136+
137+
case "discord_update_forum_post":
138+
this.logClientState("before discord_update_forum_post handler");
139+
toolResponse = await updateForumPostHandler(args, this.toolContext);
140+
return toolResponse;
141+
142+
case "discord_edit_message":
143+
this.logClientState("before discord_edit_message handler");
144+
toolResponse = await editMessageHandler(args, this.toolContext);
145+
return toolResponse;
146+
129147
case "discord_create_text_channel":
130148
this.logClientState("before discord_create_text_channel handler");
131149
toolResponse = await createTextChannelHandler(args, this.toolContext);

src/toolList.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,45 @@ export const toolList = [
235235
required: ["threadId"]
236236
}
237237
},
238+
{
239+
name: "discord_get_forum_tags",
240+
description: "Gets all available tags for a Discord forum channel, including tag IDs, names, and emoji",
241+
inputSchema: {
242+
type: "object",
243+
properties: {
244+
forumChannelId: { type: "string", description: "The ID of the forum channel to get tags from" }
245+
},
246+
required: ["forumChannelId"]
247+
}
248+
},
249+
{
250+
name: "discord_update_forum_post",
251+
description: "Updates a forum post's title, applied tags, archived status, or locked status. Tags can be specified by name or ID.",
252+
inputSchema: {
253+
type: "object",
254+
properties: {
255+
threadId: { type: "string", description: "The ID of the forum post/thread to update" },
256+
name: { type: "string", description: "New title for the forum post" },
257+
tags: { type: "array", items: { type: "string" }, description: "Tags to apply (by name or ID). Replaces all existing tags." },
258+
archived: { type: "boolean", description: "Whether to archive or unarchive the post" },
259+
locked: { type: "boolean", description: "Whether to lock or unlock the post" }
260+
},
261+
required: ["threadId"]
262+
}
263+
},
264+
{
265+
name: "discord_edit_message",
266+
description: "Edits a message previously sent by the bot. Only messages authored by the bot can be edited.",
267+
inputSchema: {
268+
type: "object",
269+
properties: {
270+
channelId: { type: "string", description: "The ID of the channel containing the message" },
271+
messageId: { type: "string", description: "The ID of the message to edit" },
272+
content: { type: "string", description: "The new content for the message" }
273+
},
274+
required: ["channelId", "messageId", "content"]
275+
}
276+
},
238277
{
239278
name: "discord_delete_message",
240279
description: "Deletes a specific message from a Discord text channel",

src/tools/forum.ts

Lines changed: 115 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { ChannelType, ForumChannel } from 'discord.js';
2-
import { GetForumChannelsSchema, CreateForumPostSchema, GetForumPostSchema, ListForumThreadsSchema, ReplyToForumSchema, DeleteForumPostSchema } from '../schemas.js';
2+
import { GetForumChannelsSchema, CreateForumPostSchema, GetForumPostSchema, ListForumThreadsSchema, ReplyToForumSchema, DeleteForumPostSchema, GetForumTagsSchema, UpdateForumPostSchema } from '../schemas.js';
33
import { ToolHandler } from './types.js';
44
import { handleDiscordError } from "../errorHandler.js";
55

@@ -280,7 +280,7 @@ export const replyToForumHandler: ToolHandler = async (args, { client }) => {
280280

281281
export const deleteForumPostHandler: ToolHandler = async (args, { client }) => {
282282
const { threadId, reason } = DeleteForumPostSchema.parse(args);
283-
283+
284284
try {
285285
if (!client.isReady()) {
286286
return {
@@ -301,12 +301,121 @@ export const deleteForumPostHandler: ToolHandler = async (args, { client }) => {
301301
await thread.delete(reason || "Forum post deleted via API");
302302

303303
return {
304-
content: [{
305-
type: "text",
306-
text: `Successfully deleted forum post/thread with ID: ${threadId}`
304+
content: [{
305+
type: "text",
306+
text: `Successfully deleted forum post/thread with ID: ${threadId}`
307+
}]
308+
};
309+
} catch (error) {
310+
return handleDiscordError(error);
311+
}
312+
};
313+
314+
export const getForumTagsHandler: ToolHandler = async (args, { client }) => {
315+
const { forumChannelId } = GetForumTagsSchema.parse(args);
316+
317+
try {
318+
if (!client.isReady()) {
319+
return {
320+
content: [{ type: "text", text: "Discord client not logged in." }],
321+
isError: true
322+
};
323+
}
324+
325+
const channel = await client.channels.fetch(forumChannelId);
326+
if (!channel || channel.type !== ChannelType.GuildForum) {
327+
return {
328+
content: [{ type: "text", text: `Channel ID ${forumChannelId} is not a forum channel.` }],
329+
isError: true
330+
};
331+
}
332+
333+
const forumChannel = channel as ForumChannel;
334+
const tags = forumChannel.availableTags.map(tag => ({
335+
id: tag.id,
336+
name: tag.name,
337+
moderated: tag.moderated,
338+
emoji: tag.emoji ? (tag.emoji.name || tag.emoji.id) : null
339+
}));
340+
341+
return {
342+
content: [{ type: "text", text: JSON.stringify(tags, null, 2) }]
343+
};
344+
} catch (error) {
345+
return handleDiscordError(error);
346+
}
347+
};
348+
349+
export const updateForumPostHandler: ToolHandler = async (args, { client }) => {
350+
const { threadId, name, tags, archived, locked } = UpdateForumPostSchema.parse(args);
351+
352+
try {
353+
if (!client.isReady()) {
354+
return {
355+
content: [{ type: "text", text: "Discord client not logged in." }],
356+
isError: true
357+
};
358+
}
359+
360+
const thread = await client.channels.fetch(threadId);
361+
if (!thread || !thread.isThread()) {
362+
return {
363+
content: [{ type: "text", text: `Cannot find thread with ID: ${threadId}` }],
364+
isError: true
365+
};
366+
}
367+
368+
const editOptions: any = {};
369+
if (name !== undefined) editOptions.name = name;
370+
if (archived !== undefined) editOptions.archived = archived;
371+
if (locked !== undefined) editOptions.locked = locked;
372+
373+
// Resolve tag names to IDs if tags are provided
374+
if (tags !== undefined) {
375+
const parent = thread.parent;
376+
if (parent && parent.type === ChannelType.GuildForum) {
377+
const forumChannel = parent as ForumChannel;
378+
const availableTags = forumChannel.availableTags;
379+
const resolved: string[] = [];
380+
const invalid: string[] = [];
381+
for (const tagInput of tags) {
382+
const byName = availableTags.find(t => t.name === tagInput);
383+
if (byName) { resolved.push(byName.id); continue; }
384+
const byId = availableTags.find(t => t.id === tagInput);
385+
if (byId) { resolved.push(byId.id); continue; }
386+
invalid.push(tagInput);
387+
}
388+
if (invalid.length > 0) {
389+
const validNames = availableTags.map(t => t.name).join(', ');
390+
return {
391+
content: [{ type: "text", text: `Unknown tag(s): ${invalid.join(', ')}. Available tags: ${validNames}` }],
392+
isError: true
393+
};
394+
}
395+
editOptions.appliedTags = resolved;
396+
} else {
397+
return {
398+
content: [{ type: "text", text: `Thread's parent channel is not a forum channel. Tags can only be applied to forum posts.` }],
399+
isError: true
400+
};
401+
}
402+
}
403+
404+
const updated = await thread.edit(editOptions);
405+
406+
const changes: string[] = [];
407+
if (name !== undefined) changes.push(`name → "${name}"`);
408+
if (tags !== undefined) changes.push(`tags → [${tags.join(', ')}]`);
409+
if (archived !== undefined) changes.push(`archived → ${archived}`);
410+
if (locked !== undefined) changes.push(`locked → ${locked}`);
411+
412+
return {
413+
content: [{
414+
type: "text",
415+
text: `Successfully updated forum post ${threadId}: ${changes.join(', ')}`
307416
}]
308417
};
309418
} catch (error) {
310419
return handleDiscordError(error);
311420
}
312-
};
421+
};

src/tools/send-message.ts

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { SendMessageSchema } from '../schemas.js';
1+
import { SendMessageSchema, EditMessageSchema } from '../schemas.js';
22
import { ToolHandler } from './types.js';
33
import { handleDiscordError } from "../errorHandler.js";
44

@@ -68,4 +68,55 @@ export const sendMessageHandler: ToolHandler = async (args, { client }) => {
6868
} catch (error) {
6969
return handleDiscordError(error);
7070
}
71-
};
71+
};
72+
73+
export const editMessageHandler: ToolHandler = async (args, { client }) => {
74+
const { channelId, messageId, content } = EditMessageSchema.parse(args);
75+
76+
try {
77+
if (!client.isReady()) {
78+
return {
79+
content: [{ type: "text", text: "Discord client not logged in." }],
80+
isError: true
81+
};
82+
}
83+
84+
const channel = await client.channels.fetch(channelId);
85+
if (!channel || !channel.isTextBased()) {
86+
return {
87+
content: [{ type: "text", text: `Cannot find text channel ID: ${channelId}` }],
88+
isError: true
89+
};
90+
}
91+
92+
if (!('messages' in channel)) {
93+
return {
94+
content: [{ type: "text", text: `This channel type does not support message editing` }],
95+
isError: true
96+
};
97+
}
98+
99+
const message = await channel.messages.fetch(messageId);
100+
if (!message) {
101+
return {
102+
content: [{ type: "text", text: `Cannot find message with ID: ${messageId}` }],
103+
isError: true
104+
};
105+
}
106+
107+
if (message.author.id !== client.user?.id) {
108+
return {
109+
content: [{ type: "text", text: `Cannot edit message: only messages sent by the bot can be edited` }],
110+
isError: true
111+
};
112+
}
113+
114+
await message.edit(content);
115+
116+
return {
117+
content: [{ type: "text", text: `Successfully edited message ${messageId} in channel ${channelId}` }]
118+
};
119+
} catch (error) {
120+
return handleDiscordError(error);
121+
}
122+
};

src/tools/tools.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,16 @@ import { Client } from "discord.js";
22
import { z } from "zod";
33
import { ToolResponse, ToolContext, ToolHandler } from "./types.js";
44
import { loginHandler } from './login.js';
5-
import { sendMessageHandler } from './send-message.js';
5+
import { sendMessageHandler, editMessageHandler } from './send-message.js';
66
import {
77
getForumChannelsHandler,
88
createForumPostHandler,
99
getForumPostHandler,
1010
listForumThreadsHandler,
1111
replyToForumHandler,
12-
deleteForumPostHandler
12+
deleteForumPostHandler,
13+
getForumTagsHandler,
14+
updateForumPostHandler
1315
} from './forum.js';
1416
import {
1517
createTextChannelHandler,
@@ -47,6 +49,9 @@ export {
4749
listForumThreadsHandler,
4850
replyToForumHandler,
4951
deleteForumPostHandler,
52+
getForumTagsHandler,
53+
updateForumPostHandler,
54+
editMessageHandler,
5055
createTextChannelHandler,
5156
deleteChannelHandler,
5257
readMessagesHandler,

0 commit comments

Comments
 (0)