import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
import { TextChannel, NewsChannel, ThreadChannel, ChannelType } from 'discord.js';
import { getDiscordClient } from '../utils/discord-client.js';
import { withErrorHandling } from '../utils/error-handler.js';
type MessageableChannel = TextChannel | NewsChannel | ThreadChannel;
function isMessageableChannel(channel: unknown): channel is MessageableChannel {
if (!channel || typeof channel !== 'object') return false;
const ch = channel as { type?: number };
return ch.type === ChannelType.GuildText ||
ch.type === ChannelType.GuildAnnouncement ||
ch.type === ChannelType.PublicThread ||
ch.type === ChannelType.PrivateThread ||
ch.type === ChannelType.AnnouncementThread;
}
export function registerMessageTools(server: McpServer): void {
// Send a message
server.tool(
'send_message',
'Send a message to a channel',
{
guildId: z.string().describe('The ID of the server (guild)'),
channelId: z.string().describe('The ID of the channel'),
content: z.string().describe('The message content'),
replyToMessageId: z.string().optional().describe('ID of message to reply to'),
},
async ({ guildId, channelId, content, replyToMessageId }) => {
const result = await withErrorHandling(async () => {
const client = await getDiscordClient();
const guild = await client.guilds.fetch(guildId);
const channel = await guild.channels.fetch(channelId);
if (!isMessageableChannel(channel)) {
throw new Error('Channel does not support messages');
}
const messageOptions: { content: string; reply?: { messageReference: string } } = { content };
if (replyToMessageId) {
messageOptions.reply = { messageReference: replyToMessageId };
}
const message = await channel.send(messageOptions);
return {
messageId: message.id,
channelId: message.channelId,
content: message.content,
createdAt: message.createdAt.toISOString(),
};
});
if (!result.success) {
return { content: [{ type: 'text', text: result.error }], isError: true };
}
return { content: [{ type: 'text', text: JSON.stringify(result.data, null, 2) }] };
}
);
// Get messages from a channel
server.tool(
'get_messages',
'Get messages from a channel',
{
guildId: z.string().describe('The ID of the server (guild)'),
channelId: z.string().describe('The ID of the channel'),
limit: z.number().optional().describe('Number of messages to fetch (1-100, default 50)'),
before: z.string().optional().describe('Get messages before this message ID'),
after: z.string().optional().describe('Get messages after this message ID'),
},
async ({ guildId, channelId, limit = 50, before, after }) => {
const result = await withErrorHandling(async () => {
const client = await getDiscordClient();
const guild = await client.guilds.fetch(guildId);
const channel = await guild.channels.fetch(channelId);
if (!isMessageableChannel(channel)) {
throw new Error('Channel does not support messages');
}
const fetchOptions: { limit: number; before?: string; after?: string } = {
limit: Math.min(Math.max(1, limit), 100),
};
if (before) fetchOptions.before = before;
if (after) fetchOptions.after = after;
const messages = await channel.messages.fetch(fetchOptions);
return messages.map((msg) => ({
id: msg.id,
content: msg.content,
authorId: msg.author.id,
authorUsername: msg.author.username,
createdAt: msg.createdAt.toISOString(),
editedAt: msg.editedAt?.toISOString(),
pinned: msg.pinned,
attachments: msg.attachments.map((a) => ({ id: a.id, url: a.url, name: a.name })),
embeds: msg.embeds.length,
reactions: msg.reactions.cache.map((r) => ({
emoji: r.emoji.name,
count: r.count,
})),
}));
});
if (!result.success) {
return { content: [{ type: 'text', text: result.error }], isError: true };
}
return { content: [{ type: 'text', text: JSON.stringify(result.data, null, 2) }] };
}
);
// Edit a message
server.tool(
'edit_message',
'Edit a message sent by the bot',
{
guildId: z.string().describe('The ID of the server (guild)'),
channelId: z.string().describe('The ID of the channel'),
messageId: z.string().describe('The ID of the message to edit'),
content: z.string().describe('The new message content'),
},
async ({ guildId, channelId, messageId, content }) => {
const result = await withErrorHandling(async () => {
const client = await getDiscordClient();
const guild = await client.guilds.fetch(guildId);
const channel = await guild.channels.fetch(channelId);
if (!isMessageableChannel(channel)) {
throw new Error('Channel does not support messages');
}
const message = await channel.messages.fetch(messageId);
const edited = await message.edit(content);
return {
messageId: edited.id,
content: edited.content,
editedAt: edited.editedAt?.toISOString(),
};
});
if (!result.success) {
return { content: [{ type: 'text', text: result.error }], isError: true };
}
return { content: [{ type: 'text', text: JSON.stringify(result.data, null, 2) }] };
}
);
// Delete a message
server.tool(
'delete_message',
'Delete a message from a channel',
{
guildId: z.string().describe('The ID of the server (guild)'),
channelId: z.string().describe('The ID of the channel'),
messageId: z.string().describe('The ID of the message to delete'),
reason: z.string().optional().describe('Reason for deletion'),
},
async ({ guildId, channelId, messageId, 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 (!isMessageableChannel(channel)) {
throw new Error('Channel does not support messages');
}
const message = await channel.messages.fetch(messageId);
await message.delete();
return { messageId, message: 'Message 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) }] };
}
);
// Bulk delete messages
server.tool(
'bulk_delete_messages',
'Bulk delete messages from a channel (up to 100, messages must be < 14 days old)',
{
guildId: z.string().describe('The ID of the server (guild)'),
channelId: z.string().describe('The ID of the channel'),
messageIds: z.array(z.string()).describe('Array of message IDs to delete'),
reason: z.string().optional().describe('Reason for deletion'),
},
async ({ guildId, channelId, messageIds, 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 (!isMessageableChannel(channel)) {
throw new Error('Channel does not support messages');
}
const deleted = await channel.bulkDelete(messageIds.slice(0, 100));
return { deletedCount: deleted.size, message: 'Messages 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) }] };
}
);
// Pin a message
server.tool(
'pin_message',
'Pin a message in a channel',
{
guildId: z.string().describe('The ID of the server (guild)'),
channelId: z.string().describe('The ID of the channel'),
messageId: z.string().describe('The ID of the message to pin'),
reason: z.string().optional().describe('Reason for pinning'),
},
async ({ guildId, channelId, messageId, 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 (!isMessageableChannel(channel)) {
throw new Error('Channel does not support messages');
}
const message = await channel.messages.fetch(messageId);
await message.pin(reason);
return { messageId, message: 'Message pinned successfully' };
});
if (!result.success) {
return { content: [{ type: 'text', text: result.error }], isError: true };
}
return { content: [{ type: 'text', text: JSON.stringify(result.data, null, 2) }] };
}
);
// Unpin a message
server.tool(
'unpin_message',
'Unpin a message in a channel',
{
guildId: z.string().describe('The ID of the server (guild)'),
channelId: z.string().describe('The ID of the channel'),
messageId: z.string().describe('The ID of the message to unpin'),
reason: z.string().optional().describe('Reason for unpinning'),
},
async ({ guildId, channelId, messageId, 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 (!isMessageableChannel(channel)) {
throw new Error('Channel does not support messages');
}
const message = await channel.messages.fetch(messageId);
await message.unpin(reason);
return { messageId, message: 'Message unpinned successfully' };
});
if (!result.success) {
return { content: [{ type: 'text', text: result.error }], isError: true };
}
return { content: [{ type: 'text', text: JSON.stringify(result.data, null, 2) }] };
}
);
// Get pinned messages
server.tool(
'get_pinned_messages',
'Get all pinned messages in a channel',
{
guildId: z.string().describe('The ID of the server (guild)'),
channelId: z.string().describe('The ID of the channel'),
},
async ({ guildId, channelId }) => {
const result = await withErrorHandling(async () => {
const client = await getDiscordClient();
const guild = await client.guilds.fetch(guildId);
const channel = await guild.channels.fetch(channelId);
if (!isMessageableChannel(channel)) {
throw new Error('Channel does not support messages');
}
const pinned = await channel.messages.fetchPinned();
return pinned.map((msg) => ({
id: msg.id,
content: msg.content,
authorId: msg.author.id,
authorUsername: msg.author.username,
createdAt: msg.createdAt.toISOString(),
}));
});
if (!result.success) {
return { content: [{ type: 'text', text: result.error }], isError: true };
}
return { content: [{ type: 'text', text: JSON.stringify(result.data, null, 2) }] };
}
);
// Add reaction to message
server.tool(
'add_reaction',
'Add a reaction to a message',
{
guildId: z.string().describe('The ID of the server (guild)'),
channelId: z.string().describe('The ID of the channel'),
messageId: z.string().describe('The ID of the message'),
emoji: z.string().describe('The emoji to react with (unicode or custom emoji ID)'),
},
async ({ guildId, channelId, messageId, emoji }) => {
const result = await withErrorHandling(async () => {
const client = await getDiscordClient();
const guild = await client.guilds.fetch(guildId);
const channel = await guild.channels.fetch(channelId);
if (!isMessageableChannel(channel)) {
throw new Error('Channel does not support messages');
}
const message = await channel.messages.fetch(messageId);
await message.react(emoji);
return { messageId, emoji, message: 'Reaction added successfully' };
});
if (!result.success) {
return { content: [{ type: 'text', text: result.error }], isError: true };
}
return { content: [{ type: 'text', text: JSON.stringify(result.data, null, 2) }] };
}
);
// Remove reactions
server.tool(
'remove_reactions',
'Remove reactions from a message',
{
guildId: z.string().describe('The ID of the server (guild)'),
channelId: z.string().describe('The ID of the channel'),
messageId: z.string().describe('The ID of the message'),
emoji: z.string().optional().describe('Specific emoji to remove (removes all if not specified)'),
userId: z.string().optional().describe('Remove reaction from specific user'),
},
async ({ guildId, channelId, messageId, emoji, userId }) => {
const result = await withErrorHandling(async () => {
const client = await getDiscordClient();
const guild = await client.guilds.fetch(guildId);
const channel = await guild.channels.fetch(channelId);
if (!isMessageableChannel(channel)) {
throw new Error('Channel does not support messages');
}
const message = await channel.messages.fetch(messageId);
if (!emoji) {
await message.reactions.removeAll();
return { messageId, message: 'All reactions removed' };
} else if (userId) {
const reaction = message.reactions.cache.get(emoji);
if (reaction) await reaction.users.remove(userId);
return { messageId, emoji, userId, message: 'User reaction removed' };
} else {
const reaction = message.reactions.cache.get(emoji);
if (reaction) await reaction.remove();
return { messageId, emoji, message: 'Reaction removed' };
}
});
if (!result.success) {
return { content: [{ type: 'text', text: result.error }], isError: true };
}
return { content: [{ type: 'text', text: JSON.stringify(result.data, null, 2) }] };
}
);
}