import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
import { TextChannel, NewsChannel, VoiceChannel, ForumChannel, ChannelType } from 'discord.js';
import { getDiscordClient } from '../utils/discord-client.js';
import { withErrorHandling } from '../utils/error-handler.js';
type WebhookableChannel = TextChannel | NewsChannel | VoiceChannel | ForumChannel;
function isWebhookableChannel(channel: unknown): channel is WebhookableChannel {
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.GuildVoice ||
ch.type === ChannelType.GuildForum;
}
export function registerWebhookTools(server: McpServer): void {
// List webhooks in a channel
server.tool(
'list_channel_webhooks',
'List all webhooks 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 (!isWebhookableChannel(channel)) {
throw new Error('Channel does not support webhooks');
}
const webhooks = await channel.fetchWebhooks();
return webhooks.map((wh) => ({
id: wh.id,
name: wh.name,
type: wh.type,
channelId: wh.channelId,
guildId: wh.guildId,
avatar: wh.avatar,
token: wh.token ? '[REDACTED]' : null,
owner: wh.owner ? { id: wh.owner.id, username: wh.owner.username } : null,
applicationId: wh.applicationId,
sourceGuild: wh.sourceGuild ? { id: wh.sourceGuild.id, name: wh.sourceGuild.name } : null,
sourceChannel: wh.sourceChannel ? { id: wh.sourceChannel.id, name: wh.sourceChannel.name } : null,
url: wh.url,
createdAt: wh.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) }] };
}
);
// List all webhooks in a guild
server.tool(
'list_guild_webhooks',
'List all webhooks in a server',
{
guildId: z.string().describe('The ID of the server (guild)'),
},
async ({ guildId }) => {
const result = await withErrorHandling(async () => {
const client = await getDiscordClient();
const guild = await client.guilds.fetch(guildId);
const webhooks = await guild.fetchWebhooks();
return webhooks.map((wh) => ({
id: wh.id,
name: wh.name,
type: wh.type,
channelId: wh.channelId,
avatar: wh.avatar,
owner: wh.owner ? { id: wh.owner.id, username: wh.owner.username } : null,
createdAt: wh.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) }] };
}
);
// Create webhook
server.tool(
'create_webhook',
'Create a webhook in a channel',
{
guildId: z.string().describe('The ID of the server (guild)'),
channelId: z.string().describe('The ID of the channel'),
name: z.string().describe('Name for the webhook'),
avatar: z.string().optional().describe('URL of the avatar image'),
reason: z.string().optional().describe('Reason for creating'),
},
async ({ guildId, channelId, name, avatar, 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 (!isWebhookableChannel(channel)) {
throw new Error('Channel does not support webhooks');
}
const webhook = await channel.createWebhook({ name, avatar, reason });
return {
id: webhook.id,
name: webhook.name,
channelId: webhook.channelId,
token: webhook.token,
url: webhook.url,
message: 'Webhook 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) }] };
}
);
// Delete webhook
server.tool(
'delete_webhook',
'Delete a webhook',
{
webhookId: z.string().describe('The ID of the webhook'),
reason: z.string().optional().describe('Reason for deleting'),
},
async ({ webhookId, reason }) => {
const result = await withErrorHandling(async () => {
const client = await getDiscordClient();
const webhook = await client.fetchWebhook(webhookId);
const webhookName = webhook.name;
await webhook.delete(reason);
return { webhookId, webhookName, message: 'Webhook 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) }] };
}
);
// Modify webhook
server.tool(
'modify_webhook',
'Modify a webhook',
{
webhookId: z.string().describe('The ID of the webhook'),
name: z.string().optional().describe('New name for the webhook'),
avatar: z.string().optional().describe('URL of the new avatar image'),
channelId: z.string().optional().describe('Move webhook to different channel'),
reason: z.string().optional().describe('Reason for modifying'),
},
async ({ webhookId, name, avatar, channelId, reason }) => {
const result = await withErrorHandling(async () => {
const client = await getDiscordClient();
const webhook = await client.fetchWebhook(webhookId);
const editData: { name?: string; avatar?: string; channel?: string; reason?: string } = {};
if (name) editData.name = name;
if (avatar) editData.avatar = avatar;
if (channelId) editData.channel = channelId;
if (reason) editData.reason = reason;
const updated = await webhook.edit(editData);
return {
id: updated.id,
name: updated.name,
channelId: updated.channelId,
message: 'Webhook 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) }] };
}
);
// Send message via webhook
server.tool(
'send_webhook_message',
'Send a message using a webhook',
{
webhookId: z.string().describe('The ID of the webhook'),
webhookToken: z.string().describe('The token of the webhook'),
content: z.string().optional().describe('The message content'),
username: z.string().optional().describe('Override the webhook username'),
avatarUrl: z.string().optional().describe('Override the webhook avatar'),
threadId: z.string().optional().describe('Send to a specific thread'),
},
async ({ webhookId, webhookToken, content, username, avatarUrl, threadId }) => {
const result = await withErrorHandling(async () => {
const client = await getDiscordClient();
const webhook = await client.fetchWebhook(webhookId, webhookToken);
const message = await webhook.send({
content,
username,
avatarURL: avatarUrl,
threadId,
});
return {
messageId: message.id,
channelId: message.channelId,
content: message.content,
createdAt: message.createdAt.toISOString(),
message: 'Webhook message sent successfully',
};
});
if (!result.success) {
return { content: [{ type: 'text', text: result.error }], isError: true };
}
return { content: [{ type: 'text', text: JSON.stringify(result.data, null, 2) }] };
}
);
}