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:
parent
646b55b234
commit
56faf1ad85
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "mcp-discord",
|
"name": "mcp-discord",
|
||||||
"version": "1.0.0",
|
"version": "1.1.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "mcp-discord",
|
"name": "mcp-discord",
|
||||||
"version": "1.0.0",
|
"version": "1.1.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"discord.js": "^14.18.0",
|
"discord.js": "^14.18.0",
|
||||||
|
|
1316
src/index.ts
1316
src/index.ts
File diff suppressed because it is too large
Load Diff
|
@ -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()
|
||||||
|
});
|
|
@ -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"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
|
@ -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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
|
@ -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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
|
@ -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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
|
@ -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 };
|
||||||
|
}
|
|
@ -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>;
|
|
@ -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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue