Modularize tool list and handlers

Modularize tool list and handlers for better maintainability

- Move tool list to separate toolList.ts file
- Split handlers into functional modules (channel, reactions, webhooks)
- Optimize import structure and switch statement in index.ts
- Maintain same functionality with improved code organization
This commit is contained in:
Barry Yip 2025-05-02 16:23:44 +08:00
parent 646b55b234
commit 56faf1ad85
12 changed files with 1492 additions and 1227 deletions

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{
"name": "mcp-discord",
"version": "1.0.0",
"version": "1.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "mcp-discord",
"version": "1.0.0",
"version": "1.1.0",
"license": "MIT",
"dependencies": {
"discord.js": "^14.18.0",

File diff suppressed because it is too large Load Diff

111
src/schemas.ts Normal file
View File

@ -0,0 +1,111 @@
import { z } from "zod";
export const DiscordLoginSchema = z.object({
random_string: z.string().optional()
});
export const SendMessageSchema = z.object({
channelId: z.string(),
message: z.string()
});
export const GetForumChannelsSchema = z.object({
guildId: z.string()
});
export const CreateForumPostSchema = z.object({
forumChannelId: z.string(),
title: z.string(),
content: z.string(),
tags: z.array(z.string()).optional()
});
export const GetForumPostSchema = z.object({
threadId: z.string()
});
export const ReplyToForumSchema = z.object({
threadId: z.string(),
message: z.string()
});
export const CreateTextChannelSchema = z.object({
guildId: z.string(),
channelName: z.string(),
topic: z.string().optional()
});
export const DeleteChannelSchema = z.object({
channelId: z.string(),
reason: z.string().optional()
});
export const ReadMessagesSchema = z.object({
channelId: z.string(),
limit: z.number().min(1).max(100).optional().default(50)
});
export const GetServerInfoSchema = z.object({
guildId: z.string()
});
export const AddReactionSchema = z.object({
channelId: z.string(),
messageId: z.string(),
emoji: z.string()
});
export const AddMultipleReactionsSchema = z.object({
channelId: z.string(),
messageId: z.string(),
emojis: z.array(z.string())
});
export const RemoveReactionSchema = z.object({
channelId: z.string(),
messageId: z.string(),
emoji: z.string(),
userId: z.string().optional()
});
export const DeleteForumPostSchema = z.object({
threadId: z.string(),
reason: z.string().optional()
});
export const DeleteMessageSchema = z.object({
channelId: z.string(),
messageId: z.string(),
reason: z.string().optional()
});
export const CreateWebhookSchema = z.object({
channelId: z.string(),
name: z.string(),
avatar: z.string().optional(),
reason: z.string().optional()
});
export const SendWebhookMessageSchema = z.object({
webhookId: z.string(),
webhookToken: z.string(),
content: z.string(),
username: z.string().optional(),
avatarURL: z.string().optional(),
threadId: z.string().optional()
});
export const EditWebhookSchema = z.object({
webhookId: z.string(),
webhookToken: z.string().optional(),
name: z.string().optional(),
avatar: z.string().optional(),
channelId: z.string().optional(),
reason: z.string().optional()
});
export const DeleteWebhookSchema = z.object({
webhookId: z.string(),
webhookToken: z.string().optional(),
reason: z.string().optional()
});

256
src/toolList.ts Normal file
View File

@ -0,0 +1,256 @@
export const toolList = [
{
name: "discord_login",
description: "Logs in to Discord using the configured token",
inputSchema: {
type: "object",
properties: {
random_string: { type: "string" }
},
required: []
}
},
{
name: "discord_send",
description: "Sends a message to a specified Discord text channel",
inputSchema: {
type: "object",
properties: {
channelId: { type: "string" },
message: { type: "string" }
},
required: ["channelId", "message"]
}
},
{
name: "discord_get_forum_channels",
description: "Lists all forum channels in a specified Discord server (guild)",
inputSchema: {
type: "object",
properties: {
guildId: { type: "string" }
},
required: ["guildId"]
}
},
{
name: "discord_create_forum_post",
description: "Creates a new post in a Discord forum channel with optional tags",
inputSchema: {
type: "object",
properties: {
forumChannelId: { type: "string" },
title: { type: "string" },
content: { type: "string" },
tags: {
type: "array",
items: { type: "string" }
}
},
required: ["forumChannelId", "title", "content"]
}
},
{
name: "discord_get_forum_post",
description: "Retrieves details about a forum post including its messages",
inputSchema: {
type: "object",
properties: {
threadId: { type: "string" }
},
required: ["threadId"]
}
},
{
name: "discord_reply_to_forum",
description: "Adds a reply to an existing forum post or thread",
inputSchema: {
type: "object",
properties: {
threadId: { type: "string" },
message: { type: "string" }
},
required: ["threadId", "message"]
}
},
{
name: "discord_create_text_channel",
description: "Creates a new text channel in a Discord server with an optional topic",
inputSchema: {
type: "object",
properties: {
guildId: { type: "string" },
channelName: { type: "string" },
topic: { type: "string" }
},
required: ["guildId", "channelName"]
}
},
{
name: "discord_delete_channel",
description: "Deletes a Discord channel with an optional reason",
inputSchema: {
type: "object",
properties: {
channelId: { type: "string" },
reason: { type: "string" }
},
required: ["channelId"]
}
},
{
name: "discord_read_messages",
description: "Retrieves messages from a Discord text channel with a configurable limit",
inputSchema: {
type: "object",
properties: {
channelId: { type: "string" },
limit: {
type: "number",
minimum: 1,
maximum: 100,
default: 50
}
},
required: ["channelId"]
}
},
{
name: "discord_get_server_info",
description: "Retrieves detailed information about a Discord server including channels and member count",
inputSchema: {
type: "object",
properties: {
guildId: { type: "string" }
},
required: ["guildId"]
}
},
{
name: "discord_add_reaction",
description: "Adds an emoji reaction to a specific Discord message",
inputSchema: {
type: "object",
properties: {
channelId: { type: "string" },
messageId: { type: "string" },
emoji: { type: "string" }
},
required: ["channelId", "messageId", "emoji"]
}
},
{
name: "discord_add_multiple_reactions",
description: "Adds multiple emoji reactions to a Discord message at once",
inputSchema: {
type: "object",
properties: {
channelId: { type: "string" },
messageId: { type: "string" },
emojis: {
type: "array",
items: { type: "string" }
}
},
required: ["channelId", "messageId", "emojis"]
}
},
{
name: "discord_remove_reaction",
description: "Removes a specific emoji reaction from a Discord message",
inputSchema: {
type: "object",
properties: {
channelId: { type: "string" },
messageId: { type: "string" },
emoji: { type: "string" },
userId: { type: "string" }
},
required: ["channelId", "messageId", "emoji"]
}
},
{
name: "discord_delete_forum_post",
description: "Deletes a forum post or thread with an optional reason",
inputSchema: {
type: "object",
properties: {
threadId: { type: "string" },
reason: { type: "string" }
},
required: ["threadId"]
}
},
{
name: "discord_delete_message",
description: "Deletes a specific message from a Discord text channel",
inputSchema: {
type: "object",
properties: {
channelId: { type: "string" },
messageId: { type: "string" },
reason: { type: "string" }
},
required: ["channelId", "messageId"]
}
},
{
name: "discord_create_webhook",
description: "Creates a new webhook for a Discord channel",
inputSchema: {
type: "object",
properties: {
channelId: { type: "string" },
name: { type: "string" },
avatar: { type: "string" },
reason: { type: "string" }
},
required: ["channelId", "name"]
}
},
{
name: "discord_send_webhook_message",
description: "Sends a message to a Discord channel using a webhook",
inputSchema: {
type: "object",
properties: {
webhookId: { type: "string" },
webhookToken: { type: "string" },
content: { type: "string" },
username: { type: "string" },
avatarURL: { type: "string" },
threadId: { type: "string" }
},
required: ["webhookId", "webhookToken", "content"]
}
},
{
name: "discord_edit_webhook",
description: "Edits an existing webhook for a Discord channel",
inputSchema: {
type: "object",
properties: {
webhookId: { type: "string" },
webhookToken: { type: "string" },
name: { type: "string" },
avatar: { type: "string" },
channelId: { type: "string" },
reason: { type: "string" }
},
required: ["webhookId"]
}
},
{
name: "discord_delete_webhook",
description: "Deletes an existing webhook for a Discord channel",
inputSchema: {
type: "object",
properties: {
webhookId: { type: "string" },
webhookToken: { type: "string" },
reason: { type: "string" }
},
required: ["webhookId"]
}
}
];

241
src/tools/channel.ts Normal file
View File

@ -0,0 +1,241 @@
import { z } from "zod";
import { ChannelType } from "discord.js";
import { ToolContext, ToolResponse } from "./types.js";
import {
CreateTextChannelSchema,
DeleteChannelSchema,
ReadMessagesSchema,
GetServerInfoSchema
} from "../schemas.js";
// Text channel creation handler
export async function createTextChannelHandler(
args: unknown,
context: ToolContext
): Promise<ToolResponse> {
const { guildId, channelName, topic } = CreateTextChannelSchema.parse(args);
try {
if (!context.client.isReady()) {
return {
content: [{ type: "text", text: "Discord client not logged in. Please use discord_login tool first." }],
isError: true
};
}
const guild = await context.client.guilds.fetch(guildId);
if (!guild) {
return {
content: [{ type: "text", text: `Cannot find guild with ID: ${guildId}` }],
isError: true
};
}
// Create the text channel
const channel = await guild.channels.create({
name: channelName,
type: ChannelType.GuildText,
topic: topic
});
return {
content: [{
type: "text",
text: `Successfully created text channel "${channelName}" with ID: ${channel.id}`
}]
};
} catch (error) {
return {
content: [{ type: "text", text: `Failed to create text channel: ${error}` }],
isError: true
};
}
}
// Channel deletion handler
export async function deleteChannelHandler(
args: unknown,
context: ToolContext
): Promise<ToolResponse> {
const { channelId, reason } = DeleteChannelSchema.parse(args);
try {
if (!context.client.isReady()) {
return {
content: [{ type: "text", text: "Discord client not logged in. Please use discord_login tool first." }],
isError: true
};
}
const channel = await context.client.channels.fetch(channelId);
if (!channel) {
return {
content: [{ type: "text", text: `Cannot find channel with ID: ${channelId}` }],
isError: true
};
}
// Check if channel can be deleted (has delete method)
if (!('delete' in channel)) {
return {
content: [{ type: "text", text: `This channel type does not support deletion or the bot lacks permissions` }],
isError: true
};
}
// Delete the channel
await channel.delete(reason || "Channel deleted via API");
return {
content: [{
type: "text",
text: `Successfully deleted channel with ID: ${channelId}`
}]
};
} catch (error) {
return {
content: [{ type: "text", text: `Failed to delete channel: ${error}` }],
isError: true
};
}
}
// Message reading handler
export async function readMessagesHandler(
args: unknown,
context: ToolContext
): Promise<ToolResponse> {
const { channelId, limit } = ReadMessagesSchema.parse(args);
try {
if (!context.client.isReady()) {
return {
content: [{ type: "text", text: "Discord client not logged in. Please use discord_login tool first." }],
isError: true
};
}
const channel = await context.client.channels.fetch(channelId);
if (!channel) {
return {
content: [{ type: "text", text: `Cannot find channel with ID: ${channelId}` }],
isError: true
};
}
// Check if channel has messages (text channel, thread, etc.)
if (!channel.isTextBased() || !('messages' in channel)) {
return {
content: [{ type: "text", text: `Channel type does not support reading messages` }],
isError: true
};
}
// Fetch messages
const messages = await channel.messages.fetch({ limit });
if (messages.size === 0) {
return {
content: [{ type: "text", text: `No messages found in channel` }]
};
}
// Format messages
const formattedMessages = messages.map(msg => ({
id: msg.id,
content: msg.content,
author: {
id: msg.author.id,
username: msg.author.username,
bot: msg.author.bot
},
timestamp: msg.createdAt,
attachments: msg.attachments.size,
embeds: msg.embeds.length,
replyTo: msg.reference ? msg.reference.messageId : null
})).sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
return {
content: [{
type: "text",
text: JSON.stringify({
channelId,
messageCount: formattedMessages.length,
messages: formattedMessages
}, null, 2)
}]
};
} catch (error) {
return {
content: [{ type: "text", text: `Failed to read messages: ${error}` }],
isError: true
};
}
}
// Server information handler
export async function getServerInfoHandler(
args: unknown,
context: ToolContext
): Promise<ToolResponse> {
const { guildId } = GetServerInfoSchema.parse(args);
try {
if (!context.client.isReady()) {
return {
content: [{ type: "text", text: "Discord client not logged in. Please use discord_login tool first." }],
isError: true
};
}
const guild = await context.client.guilds.fetch(guildId);
if (!guild) {
return {
content: [{ type: "text", text: `Cannot find guild with ID: ${guildId}` }],
isError: true
};
}
// Fetch additional guild data
await guild.fetch();
// Fetch channel information
const channels = await guild.channels.fetch();
// Categorize channels by type
const channelsByType = {
text: channels.filter(c => c?.type === ChannelType.GuildText).size,
voice: channels.filter(c => c?.type === ChannelType.GuildVoice).size,
category: channels.filter(c => c?.type === ChannelType.GuildCategory).size,
forum: channels.filter(c => c?.type === ChannelType.GuildForum).size,
announcement: channels.filter(c => c?.type === ChannelType.GuildAnnouncement).size,
stage: channels.filter(c => c?.type === ChannelType.GuildStageVoice).size,
total: channels.size
};
// Fetch member count
const approximateMemberCount = guild.approximateMemberCount || "unknown";
// Format guild information
const guildInfo = {
id: guild.id,
name: guild.name,
description: guild.description,
icon: guild.iconURL(),
owner: guild.ownerId,
createdAt: guild.createdAt,
memberCount: approximateMemberCount,
channels: channelsByType,
features: guild.features,
premium: {
tier: guild.premiumTier,
subscriptions: guild.premiumSubscriptionCount
}
};
return {
content: [{ type: "text", text: JSON.stringify(guildInfo, null, 2) }]
};
} catch (error) {
return {
content: [{ type: "text", text: `Failed to fetch server info: ${error}` }],
isError: true
};
}
}

233
src/tools/forum.ts Normal file
View File

@ -0,0 +1,233 @@
import { ChannelType, ForumChannel } from 'discord.js';
import { GetForumChannelsSchema, CreateForumPostSchema, GetForumPostSchema, ReplyToForumSchema, DeleteForumPostSchema } from '../schemas.js';
import { ToolHandler } from './types.js';
export const getForumChannelsHandler: ToolHandler = async (args, { client }) => {
const { guildId } = GetForumChannelsSchema.parse(args);
try {
if (!client.isReady()) {
return {
content: [{ type: "text", text: "Discord client not logged in. Please use discord_login tool first." }],
isError: true
};
}
const guild = await client.guilds.fetch(guildId);
if (!guild) {
return {
content: [{ type: "text", text: `Cannot find guild with ID: ${guildId}` }],
isError: true
};
}
// Fetch all channels from the guild
const channels = await guild.channels.fetch();
// Filter to get only forum channels
const forumChannels = channels.filter(channel => channel?.type === ChannelType.GuildForum);
if (forumChannels.size === 0) {
return {
content: [{ type: "text", text: `No forum channels found in guild: ${guild.name}` }]
};
}
// Format forum channels information
const forumInfo = forumChannels.map(channel => ({
id: channel.id,
name: channel.name,
topic: channel.topic || "No topic set"
}));
return {
content: [{ type: "text", text: JSON.stringify(forumInfo, null, 2) }]
};
} catch (error) {
return {
content: [{ type: "text", text: `Failed to fetch forum channels: ${error}` }],
isError: true
};
}
};
export const createForumPostHandler: ToolHandler = async (args, { client }) => {
const { forumChannelId, title, content, tags } = CreateForumPostSchema.parse(args);
try {
if (!client.isReady()) {
return {
content: [{ type: "text", text: "Discord client not logged in. Please use discord_login tool first." }],
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;
// Get available tags in the forum
const availableTags = forumChannel.availableTags;
let selectedTagIds: string[] = [];
// If tags are provided, find their IDs
if (tags && tags.length > 0) {
selectedTagIds = availableTags
.filter(tag => tags.includes(tag.name))
.map(tag => tag.id);
}
// Create the forum post
const thread = await forumChannel.threads.create({
name: title,
message: {
content: content
},
appliedTags: selectedTagIds.length > 0 ? selectedTagIds : undefined
});
return {
content: [{
type: "text",
text: `Successfully created forum post "${title}" with ID: ${thread.id}`
}]
};
} catch (error) {
return {
content: [{ type: "text", text: `Failed to create forum post: ${error}` }],
isError: true
};
}
};
export const getForumPostHandler: ToolHandler = async (args, { client }) => {
const { threadId } = GetForumPostSchema.parse(args);
try {
if (!client.isReady()) {
return {
content: [{ type: "text", text: "Discord client not logged in. Please use discord_login tool first." }],
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
};
}
// Get messages from the thread
const messages = await thread.messages.fetch({ limit: 10 });
const threadDetails = {
id: thread.id,
name: thread.name,
parentId: thread.parentId,
messageCount: messages.size,
createdAt: thread.createdAt,
messages: messages.map(msg => ({
id: msg.id,
content: msg.content,
author: msg.author.tag,
createdAt: msg.createdAt
}))
};
return {
content: [{ type: "text", text: JSON.stringify(threadDetails, null, 2) }]
};
} catch (error) {
return {
content: [{ type: "text", text: `Failed to fetch forum post: ${error}` }],
isError: true
};
}
};
export const replyToForumHandler: ToolHandler = async (args, { client }) => {
const { threadId, message } = ReplyToForumSchema.parse(args);
try {
if (!client.isReady()) {
return {
content: [{ type: "text", text: "Discord client not logged in. Please use discord_login tool first." }],
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
};
}
if (!('send' in thread)) {
return {
content: [{ type: "text", text: `This thread does not support sending messages` }],
isError: true
};
}
// Send the reply
const sentMessage = await thread.send(message);
return {
content: [{
type: "text",
text: `Successfully replied to forum post. Message ID: ${sentMessage.id}`
}]
};
} catch (error) {
return {
content: [{ type: "text", text: `Failed to reply to forum post: ${error}` }],
isError: true
};
}
};
export const deleteForumPostHandler: ToolHandler = async (args, { client }) => {
const { threadId, reason } = DeleteForumPostSchema.parse(args);
try {
if (!client.isReady()) {
return {
content: [{ type: "text", text: "Discord client not logged in. Please use discord_login tool first." }],
isError: true
};
}
const thread = await client.channels.fetch(threadId);
if (!thread || !thread.isThread()) {
return {
content: [{ type: "text", text: `Cannot find forum post/thread with ID: ${threadId}` }],
isError: true
};
}
// Delete the forum post/thread
await thread.delete(reason || "Forum post deleted via API");
return {
content: [{
type: "text",
text: `Successfully deleted forum post/thread with ID: ${threadId}`
}]
};
} catch (error) {
return {
content: [{ type: "text", text: `Failed to delete forum post: ${error}` }],
isError: true
};
}
};

33
src/tools/login.ts Normal file
View File

@ -0,0 +1,33 @@
import { DiscordLoginSchema } from '../schemas.js';
import { ToolHandler } from './types.js';
export const loginHandler: ToolHandler = async (args, { client }) => {
DiscordLoginSchema.parse(args);
try {
// Check if client is already logged in
if (client.isReady()) {
return {
content: [{ type: "text", text: `Already logged in as: ${client.user?.tag}` }]
};
}
// loginHandler doesn't directly handle token, it needs to be set before invocation
if (!client.token) {
return {
content: [{ type: "text", text: "Discord token not configured. Cannot log in." }],
isError: true
};
}
await client.login(client.token);
return {
content: [{ type: "text", text: `Successfully logged in to Discord: ${client.user?.tag}` }]
};
} catch (error) {
return {
content: [{ type: "text", text: `Login failed: ${error}` }],
isError: true
};
}
};

224
src/tools/reactions.ts Normal file
View File

@ -0,0 +1,224 @@
import { z } from "zod";
import { ToolContext, ToolResponse } from "./types.js";
import {
AddReactionSchema,
AddMultipleReactionsSchema,
RemoveReactionSchema,
DeleteMessageSchema
} from "../schemas.js";
// Add reaction handler
export async function addReactionHandler(
args: unknown,
context: ToolContext
): Promise<ToolResponse> {
const { channelId, messageId, emoji } = AddReactionSchema.parse(args);
try {
if (!context.client.isReady()) {
return {
content: [{ type: "text", text: "Discord client not logged in. Please use discord_login tool first." }],
isError: true
};
}
const channel = await context.client.channels.fetch(channelId);
if (!channel || !channel.isTextBased() || !('messages' in channel)) {
return {
content: [{ type: "text", text: `Cannot find text channel with ID: ${channelId}` }],
isError: true
};
}
const message = await channel.messages.fetch(messageId);
if (!message) {
return {
content: [{ type: "text", text: `Cannot find message with ID: ${messageId}` }],
isError: true
};
}
// Add the reaction
await message.react(emoji);
return {
content: [{
type: "text",
text: `Successfully added reaction ${emoji} to message ID: ${messageId}`
}]
};
} catch (error) {
return {
content: [{ type: "text", text: `Failed to add reaction: ${error}` }],
isError: true
};
}
}
// Add multiple reactions handler
export async function addMultipleReactionsHandler(
args: unknown,
context: ToolContext
): Promise<ToolResponse> {
const { channelId, messageId, emojis } = AddMultipleReactionsSchema.parse(args);
try {
if (!context.client.isReady()) {
return {
content: [{ type: "text", text: "Discord client not logged in. Please use discord_login tool first." }],
isError: true
};
}
const channel = await context.client.channels.fetch(channelId);
if (!channel || !channel.isTextBased() || !('messages' in channel)) {
return {
content: [{ type: "text", text: `Cannot find text channel with ID: ${channelId}` }],
isError: true
};
}
const message = await channel.messages.fetch(messageId);
if (!message) {
return {
content: [{ type: "text", text: `Cannot find message with ID: ${messageId}` }],
isError: true
};
}
// Add each reaction sequentially
for (const emoji of emojis) {
await message.react(emoji);
// Small delay to prevent rate limiting
await new Promise(resolve => setTimeout(resolve, 300));
}
return {
content: [{
type: "text",
text: `Successfully added ${emojis.length} reactions to message ID: ${messageId}`
}]
};
} catch (error) {
return {
content: [{ type: "text", text: `Failed to add reactions: ${error}` }],
isError: true
};
}
}
// Remove reaction handler
export async function removeReactionHandler(
args: unknown,
context: ToolContext
): Promise<ToolResponse> {
const { channelId, messageId, emoji, userId } = RemoveReactionSchema.parse(args);
try {
if (!context.client.isReady()) {
return {
content: [{ type: "text", text: "Discord client not logged in. Please use discord_login tool first." }],
isError: true
};
}
const channel = await context.client.channels.fetch(channelId);
if (!channel || !channel.isTextBased() || !('messages' in channel)) {
return {
content: [{ type: "text", text: `Cannot find text channel with ID: ${channelId}` }],
isError: true
};
}
const message = await channel.messages.fetch(messageId);
if (!message) {
return {
content: [{ type: "text", text: `Cannot find message with ID: ${messageId}` }],
isError: true
};
}
// Get the reactions
const reactions = message.reactions.cache;
// Find the specific reaction
const reaction = reactions.find(r => r.emoji.toString() === emoji || r.emoji.name === emoji);
if (!reaction) {
return {
content: [{ type: "text", text: `Reaction ${emoji} not found on message ID: ${messageId}` }],
isError: true
};
}
if (userId) {
// Remove a specific user's reaction
await reaction.users.remove(userId);
return {
content: [{
type: "text",
text: `Successfully removed reaction ${emoji} from user ID: ${userId} on message ID: ${messageId}`
}]
};
} else {
// Remove bot's reaction
await reaction.users.remove(context.client.user.id);
return {
content: [{
type: "text",
text: `Successfully removed bot's reaction ${emoji} from message ID: ${messageId}`
}]
};
}
} catch (error) {
return {
content: [{ type: "text", text: `Failed to remove reaction: ${error}` }],
isError: true
};
}
}
// Delete message handler
export async function deleteMessageHandler(
args: unknown,
context: ToolContext
): Promise<ToolResponse> {
const { channelId, messageId, reason } = DeleteMessageSchema.parse(args);
try {
if (!context.client.isReady()) {
return {
content: [{ type: "text", text: "Discord client not logged in. Please use discord_login tool first." }],
isError: true
};
}
const channel = await context.client.channels.fetch(channelId);
if (!channel || !channel.isTextBased() || !('messages' in channel)) {
return {
content: [{ type: "text", text: `Cannot find text channel with ID: ${channelId}` }],
isError: true
};
}
// Fetch the message
const message = await channel.messages.fetch(messageId);
if (!message) {
return {
content: [{ type: "text", text: `Cannot find message with ID: ${messageId}` }],
isError: true
};
}
// Delete the message
await message.delete();
return {
content: [{
type: "text",
text: `Successfully deleted message with ID: ${messageId} from channel: ${channelId}`
}]
};
} catch (error) {
return {
content: [{ type: "text", text: `Failed to delete message: ${error}` }],
isError: true
};
}
}

41
src/tools/send-message.ts Normal file
View File

@ -0,0 +1,41 @@
import { SendMessageSchema } from '../schemas.js';
import { ToolHandler } from './types.js';
export const sendMessageHandler: ToolHandler = async (args, { client }) => {
const { channelId, message } = SendMessageSchema.parse(args);
try {
if (!client.isReady()) {
return {
content: [{ type: "text", text: "Discord client not logged in. Please use discord_login tool first." }],
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
};
}
// Ensure channel is text-based and can send messages
if ('send' in channel) {
await channel.send(message);
return {
content: [{ type: "text", text: `Message successfully sent to channel ID: ${channelId}` }]
};
} else {
return {
content: [{ type: "text", text: `This channel type does not support sending messages` }],
isError: true
};
}
} catch (error) {
return {
content: [{ type: "text", text: `Send message failed: ${error}` }],
isError: true
};
}
};

61
src/tools/tools.ts Normal file
View File

@ -0,0 +1,61 @@
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 {
getForumChannelsHandler,
createForumPostHandler,
getForumPostHandler,
replyToForumHandler,
deleteForumPostHandler
} from './forum.js';
import {
createTextChannelHandler,
deleteChannelHandler,
readMessagesHandler,
getServerInfoHandler
} from './channel.js';
import {
addReactionHandler,
addMultipleReactionsHandler,
removeReactionHandler,
deleteMessageHandler
} from './reactions.js';
import {
createWebhookHandler,
sendWebhookMessageHandler,
editWebhookHandler,
deleteWebhookHandler
} from './webhooks.js';
// Export tool handlers
export {
loginHandler,
sendMessageHandler,
getForumChannelsHandler,
createForumPostHandler,
getForumPostHandler,
replyToForumHandler,
deleteForumPostHandler,
createTextChannelHandler,
deleteChannelHandler,
readMessagesHandler,
getServerInfoHandler,
addReactionHandler,
addMultipleReactionsHandler,
removeReactionHandler,
deleteMessageHandler,
createWebhookHandler,
sendWebhookMessageHandler,
editWebhookHandler,
deleteWebhookHandler
};
// Export common types
export { ToolResponse, ToolContext, ToolHandler };
// Create tool context
export function createToolContext(client: Client): ToolContext {
return { client };
}

13
src/tools/types.ts Normal file
View File

@ -0,0 +1,13 @@
import { Client } from "discord.js";
export interface ToolResponse {
content: { type: string; text: string }[];
isError?: boolean;
[key: string]: unknown;
}
export interface ToolContext {
client: Client;
}
export type ToolHandler<T = any> = (args: T, context: ToolContext) => Promise<ToolResponse>;

186
src/tools/webhooks.ts Normal file
View File

@ -0,0 +1,186 @@
import { z } from "zod";
import { ToolContext, ToolResponse } from "./types.js";
import {
CreateWebhookSchema,
SendWebhookMessageSchema,
EditWebhookSchema,
DeleteWebhookSchema
} from "../schemas.js";
// Create webhook handler
export async function createWebhookHandler(
args: unknown,
context: ToolContext
): Promise<ToolResponse> {
const { channelId, name, avatar, reason } = CreateWebhookSchema.parse(args);
try {
if (!context.client.isReady()) {
return {
content: [{ type: "text", text: "Discord client not logged in. Please use discord_login tool first." }],
isError: true
};
}
const channel = await context.client.channels.fetch(channelId);
if (!channel || !channel.isTextBased()) {
return {
content: [{ type: "text", text: `Cannot find text channel with ID: ${channelId}` }],
isError: true
};
}
// Check if the channel supports webhooks
if (!('createWebhook' in channel)) {
return {
content: [{ type: "text", text: `Channel type does not support webhooks: ${channelId}` }],
isError: true
};
}
// Create the webhook
const webhook = await channel.createWebhook({
name: name,
avatar: avatar,
reason: reason
});
return {
content: [{
type: "text",
text: `Successfully created webhook with ID: ${webhook.id} and token: ${webhook.token}`
}]
};
} catch (error) {
return {
content: [{ type: "text", text: `Failed to create webhook: ${error}` }],
isError: true
};
}
}
// Send webhook message handler
export async function sendWebhookMessageHandler(
args: unknown,
context: ToolContext
): Promise<ToolResponse> {
const { webhookId, webhookToken, content, username, avatarURL, threadId } = SendWebhookMessageSchema.parse(args);
try {
if (!context.client.isReady()) {
return {
content: [{ type: "text", text: "Discord client not logged in. Please use discord_login tool first." }],
isError: true
};
}
const webhook = await context.client.fetchWebhook(webhookId, webhookToken);
if (!webhook) {
return {
content: [{ type: "text", text: `Cannot find webhook with ID: ${webhookId}` }],
isError: true
};
}
// Send the message
await webhook.send({
content: content,
username: username,
avatarURL: avatarURL,
threadId: threadId
});
return {
content: [{
type: "text",
text: `Successfully sent webhook message to webhook ID: ${webhookId}`
}]
};
} catch (error) {
return {
content: [{ type: "text", text: `Failed to send webhook message: ${error}` }],
isError: true
};
}
}
// Edit webhook handler
export async function editWebhookHandler(
args: unknown,
context: ToolContext
): Promise<ToolResponse> {
const { webhookId, webhookToken, name, avatar, channelId, reason } = EditWebhookSchema.parse(args);
try {
if (!context.client.isReady()) {
return {
content: [{ type: "text", text: "Discord client not logged in. Please use discord_login tool first." }],
isError: true
};
}
const webhook = await context.client.fetchWebhook(webhookId, webhookToken);
if (!webhook) {
return {
content: [{ type: "text", text: `Cannot find webhook with ID: ${webhookId}` }],
isError: true
};
}
// Edit the webhook
await webhook.edit({
name: name,
avatar: avatar,
channel: channelId,
reason: reason
});
return {
content: [{
type: "text",
text: `Successfully edited webhook with ID: ${webhook.id}`
}]
};
} catch (error) {
return {
content: [{ type: "text", text: `Failed to edit webhook: ${error}` }],
isError: true
};
}
}
// Delete webhook handler
export async function deleteWebhookHandler(
args: unknown,
context: ToolContext
): Promise<ToolResponse> {
const { webhookId, webhookToken, reason } = DeleteWebhookSchema.parse(args);
try {
if (!context.client.isReady()) {
return {
content: [{ type: "text", text: "Discord client not logged in. Please use discord_login tool first." }],
isError: true
};
}
const webhook = await context.client.fetchWebhook(webhookId, webhookToken);
if (!webhook) {
return {
content: [{ type: "text", text: `Cannot find webhook with ID: ${webhookId}` }],
isError: true
};
}
// Delete the webhook
await webhook.delete(reason || "Webhook deleted via API");
return {
content: [{
type: "text",
text: `Successfully deleted webhook with ID: ${webhook.id}`
}]
};
} catch (error) {
return {
content: [{ type: "text", text: `Failed to delete webhook: ${error}` }],
isError: true
};
}
}