import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
import { TextChannel, NewsChannel, ForumChannel, ChannelType, ThreadAutoArchiveDuration } from 'discord.js';
import { getDiscordClient } from '../utils/discord-client.js';
import { withErrorHandling } from '../utils/error-handler.js';
export function registerThreadTools(server: McpServer): void {
// List threads in a channel
server.tool(
'list_threads',
'List all threads in a channel',
{
guildId: z.string().describe('The ID of the server (guild)'),
channelId: z.string().describe('The ID of the parent channel'),
archived: z.boolean().optional().describe('Include archived threads (default false)'),
},
async ({ guildId, channelId, archived = false }) => {
const result = await withErrorHandling(async () => {
const client = await getDiscordClient();
const guild = await client.guilds.fetch(guildId);
const channel = await guild.channels.fetch(channelId);
if (!channel || (channel.type !== ChannelType.GuildText &&
channel.type !== ChannelType.GuildAnnouncement &&
channel.type !== ChannelType.GuildForum)) {
throw new Error('Channel does not support threads');
}
const threadChannel = channel as TextChannel | NewsChannel | ForumChannel;
const threads = archived
? await threadChannel.threads.fetchArchived()
: await threadChannel.threads.fetchActive();
return threads.threads.map((thread) => ({
id: thread.id,
name: thread.name,
type: ChannelType[thread.type],
parentId: thread.parentId,
ownerId: thread.ownerId,
archived: thread.archived,
locked: thread.locked,
autoArchiveDuration: thread.autoArchiveDuration,
messageCount: thread.messageCount,
memberCount: thread.memberCount,
createdAt: thread.createdAt?.toISOString(),
archiveTimestamp: thread.archiveTimestamp ? new Date(thread.archiveTimestamp).toISOString() : null,
}));
});
if (!result.success) {
return { content: [{ type: 'text', text: result.error }], isError: true };
}
return { content: [{ type: 'text', text: JSON.stringify(result.data, null, 2) }] };
}
);
// Create thread
server.tool(
'create_thread',
'Create a new thread',
{
guildId: z.string().describe('The ID of the server (guild)'),
channelId: z.string().describe('The ID of the parent channel'),
name: z.string().describe('Name of the thread'),
messageId: z.string().optional().describe('Message ID to start thread from'),
autoArchiveDuration: z.enum(['60', '1440', '4320', '10080']).optional().describe('Auto archive after minutes'),
type: z.enum(['public', 'private']).optional().describe('Thread type (default public)'),
reason: z.string().optional().describe('Reason for creating'),
},
async ({ guildId, channelId, name, messageId, autoArchiveDuration, type = 'public', reason }) => {
const result = await withErrorHandling(async () => {
const client = await getDiscordClient();
const guild = await client.guilds.fetch(guildId);
const channel = await guild.channels.fetch(channelId);
if (!channel || channel.type !== ChannelType.GuildText) {
throw new Error('Channel does not support creating threads');
}
const textChannel = channel as TextChannel;
const archiveDurationMap: Record<string, ThreadAutoArchiveDuration> = {
'60': ThreadAutoArchiveDuration.OneHour,
'1440': ThreadAutoArchiveDuration.OneDay,
'4320': ThreadAutoArchiveDuration.ThreeDays,
'10080': ThreadAutoArchiveDuration.OneWeek,
};
let thread;
if (messageId) {
const message = await textChannel.messages.fetch(messageId);
thread = await message.startThread({
name,
autoArchiveDuration: autoArchiveDuration ? archiveDurationMap[autoArchiveDuration] : undefined,
reason,
});
} else {
thread = await textChannel.threads.create({
name,
autoArchiveDuration: autoArchiveDuration ? archiveDurationMap[autoArchiveDuration] : undefined,
type: type === 'private' ? ChannelType.PrivateThread : ChannelType.PublicThread,
reason,
});
}
return {
id: thread.id,
name: thread.name,
type: ChannelType[thread.type],
parentId: thread.parentId,
message: 'Thread created successfully',
};
});
if (!result.success) {
return { content: [{ type: 'text', text: result.error }], isError: true };
}
return { content: [{ type: 'text', text: JSON.stringify(result.data, null, 2) }] };
}
);
// Create forum post
server.tool(
'create_forum_post',
'Create a new post in a forum channel',
{
guildId: z.string().describe('The ID of the server (guild)'),
channelId: z.string().describe('The ID of the forum channel'),
name: z.string().describe('Title of the post'),
content: z.string().describe('Content of the initial message'),
appliedTags: z.array(z.string()).optional().describe('Tag IDs to apply'),
autoArchiveDuration: z.enum(['60', '1440', '4320', '10080']).optional().describe('Auto archive minutes'),
reason: z.string().optional().describe('Reason for creating'),
},
async ({ guildId, channelId, name, content, appliedTags, autoArchiveDuration, reason }) => {
const result = await withErrorHandling(async () => {
const client = await getDiscordClient();
const guild = await client.guilds.fetch(guildId);
const channel = await guild.channels.fetch(channelId);
if (!channel || channel.type !== ChannelType.GuildForum) {
throw new Error('Channel is not a forum channel');
}
const forumChannel = channel as ForumChannel;
const archiveDurationMap: Record<string, ThreadAutoArchiveDuration> = {
'60': ThreadAutoArchiveDuration.OneHour,
'1440': ThreadAutoArchiveDuration.OneDay,
'4320': ThreadAutoArchiveDuration.ThreeDays,
'10080': ThreadAutoArchiveDuration.OneWeek,
};
const thread = await forumChannel.threads.create({
name,
message: { content },
appliedTags,
autoArchiveDuration: autoArchiveDuration ? archiveDurationMap[autoArchiveDuration] : undefined,
reason,
});
return {
id: thread.id,
name: thread.name,
parentId: thread.parentId,
appliedTags: thread.appliedTags,
message: 'Forum post created successfully',
};
});
if (!result.success) {
return { content: [{ type: 'text', text: result.error }], isError: true };
}
return { content: [{ type: 'text', text: JSON.stringify(result.data, null, 2) }] };
}
);
// Modify thread
server.tool(
'modify_thread',
'Modify a thread',
{
guildId: z.string().describe('The ID of the server (guild)'),
threadId: z.string().describe('The ID of the thread'),
name: z.string().optional().describe('New name'),
archived: z.boolean().optional().describe('Archive/unarchive the thread'),
locked: z.boolean().optional().describe('Lock/unlock the thread'),
autoArchiveDuration: z.enum(['60', '1440', '4320', '10080']).optional().describe('Auto archive minutes'),
rateLimitPerUser: z.number().optional().describe('Slowmode in seconds (0-21600)'),
appliedTags: z.array(z.string()).optional().describe('Applied tags (forum posts only)'),
reason: z.string().optional().describe('Reason for modifying'),
},
async ({ guildId, threadId, name, archived, locked, autoArchiveDuration, rateLimitPerUser, appliedTags, reason }) => {
const result = await withErrorHandling(async () => {
const client = await getDiscordClient();
const guild = await client.guilds.fetch(guildId);
const thread = await guild.channels.fetch(threadId);
if (!thread || !thread.isThread()) {
throw new Error('Thread not found');
}
const archiveDurationMap: Record<string, ThreadAutoArchiveDuration> = {
'60': ThreadAutoArchiveDuration.OneHour,
'1440': ThreadAutoArchiveDuration.OneDay,
'4320': ThreadAutoArchiveDuration.ThreeDays,
'10080': ThreadAutoArchiveDuration.OneWeek,
};
const editData: Record<string, unknown> = {};
if (name !== undefined) editData.name = name;
if (archived !== undefined) editData.archived = archived;
if (locked !== undefined) editData.locked = locked;
if (autoArchiveDuration) editData.autoArchiveDuration = archiveDurationMap[autoArchiveDuration];
if (rateLimitPerUser !== undefined) editData.rateLimitPerUser = rateLimitPerUser;
if (appliedTags) editData.appliedTags = appliedTags;
if (reason) editData.reason = reason;
const updated = await thread.edit(editData);
return {
id: updated.id,
name: updated.name,
archived: updated.archived,
locked: updated.locked,
message: 'Thread updated successfully',
};
});
if (!result.success) {
return { content: [{ type: 'text', text: result.error }], isError: true };
}
return { content: [{ type: 'text', text: JSON.stringify(result.data, null, 2) }] };
}
);
// Delete thread
server.tool(
'delete_thread',
'Delete a thread',
{
guildId: z.string().describe('The ID of the server (guild)'),
threadId: z.string().describe('The ID of the thread to delete'),
reason: z.string().optional().describe('Reason for deleting'),
},
async ({ guildId, threadId, reason }) => {
const result = await withErrorHandling(async () => {
const client = await getDiscordClient();
const guild = await client.guilds.fetch(guildId);
const thread = await guild.channels.fetch(threadId);
if (!thread || !thread.isThread()) {
throw new Error('Thread not found');
}
const threadName = thread.name;
await thread.delete(reason);
return { threadId, threadName, message: 'Thread deleted successfully' };
});
if (!result.success) {
return { content: [{ type: 'text', text: result.error }], isError: true };
}
return { content: [{ type: 'text', text: JSON.stringify(result.data, null, 2) }] };
}
);
// Join thread
server.tool(
'join_thread',
'Make the bot join a thread',
{
guildId: z.string().describe('The ID of the server (guild)'),
threadId: z.string().describe('The ID of the thread'),
},
async ({ guildId, threadId }) => {
const result = await withErrorHandling(async () => {
const client = await getDiscordClient();
const guild = await client.guilds.fetch(guildId);
const thread = await guild.channels.fetch(threadId);
if (!thread || !thread.isThread()) {
throw new Error('Thread not found');
}
await thread.join();
return { threadId, threadName: thread.name, message: 'Joined thread successfully' };
});
if (!result.success) {
return { content: [{ type: 'text', text: result.error }], isError: true };
}
return { content: [{ type: 'text', text: JSON.stringify(result.data, null, 2) }] };
}
);
// Leave thread
server.tool(
'leave_thread',
'Make the bot leave a thread',
{
guildId: z.string().describe('The ID of the server (guild)'),
threadId: z.string().describe('The ID of the thread'),
},
async ({ guildId, threadId }) => {
const result = await withErrorHandling(async () => {
const client = await getDiscordClient();
const guild = await client.guilds.fetch(guildId);
const thread = await guild.channels.fetch(threadId);
if (!thread || !thread.isThread()) {
throw new Error('Thread not found');
}
await thread.leave();
return { threadId, threadName: thread.name, message: 'Left thread successfully' };
});
if (!result.success) {
return { content: [{ type: 'text', text: result.error }], isError: true };
}
return { content: [{ type: 'text', text: JSON.stringify(result.data, null, 2) }] };
}
);
// Add thread member
server.tool(
'add_thread_member',
'Add a member to a thread',
{
guildId: z.string().describe('The ID of the server (guild)'),
threadId: z.string().describe('The ID of the thread'),
userId: z.string().describe('The ID of the user to add'),
},
async ({ guildId, threadId, userId }) => {
const result = await withErrorHandling(async () => {
const client = await getDiscordClient();
const guild = await client.guilds.fetch(guildId);
const thread = await guild.channels.fetch(threadId);
if (!thread || !thread.isThread()) {
throw new Error('Thread not found');
}
await thread.members.add(userId);
return { threadId, userId, message: 'Member added to thread' };
});
if (!result.success) {
return { content: [{ type: 'text', text: result.error }], isError: true };
}
return { content: [{ type: 'text', text: JSON.stringify(result.data, null, 2) }] };
}
);
// Remove thread member
server.tool(
'remove_thread_member',
'Remove a member from a thread',
{
guildId: z.string().describe('The ID of the server (guild)'),
threadId: z.string().describe('The ID of the thread'),
userId: z.string().describe('The ID of the user to remove'),
},
async ({ guildId, threadId, userId }) => {
const result = await withErrorHandling(async () => {
const client = await getDiscordClient();
const guild = await client.guilds.fetch(guildId);
const thread = await guild.channels.fetch(threadId);
if (!thread || !thread.isThread()) {
throw new Error('Thread not found');
}
await thread.members.remove(userId);
return { threadId, userId, message: 'Member removed from thread' };
});
if (!result.success) {
return { content: [{ type: 'text', text: result.error }], isError: true };
}
return { content: [{ type: 'text', text: JSON.stringify(result.data, null, 2) }] };
}
);
}