moderation.ts•9.51 kB
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { DiscordClientManager } from "../discord/client.js";
import { Logger } from "../utils/logger.js";
import { z } from "zod";
import {
  PermissionDeniedError,
  ChannelNotFoundError,
  GuildNotFoundError,
} from "../errors/discord.js";
import { PermissionFlagsBits } from "discord.js";
export function registerModerationTools(
  server: McpServer,
  discordManager: DiscordClientManager,
  logger: Logger,
) {
  // Bulk Delete Messages Tool
  server.registerTool(
    "bulk_delete_messages",
    {
      title: "Bulk Delete Messages",
      description:
        "Delete multiple messages at once (max 100, must be <14 days old)",
      inputSchema: {
        channelId: z.string().describe("Channel ID"),
        limit: z
          .number()
          .int()
          .min(2)
          .max(100)
          .describe("Number of messages to delete (2-100)"),
        filterUserId: z
          .string()
          .optional()
          .describe("Only delete messages from this user"),
        filterBots: z.boolean().optional().describe("Only delete bot messages"),
        reason: z
          .string()
          .optional()
          .describe("Reason for bulk delete (audit log)"),
      },
      outputSchema: {
        success: z.boolean(),
        deletedCount: z.number().optional(),
        error: z.string().optional(),
      },
    },
    async ({ channelId, limit, filterUserId, filterBots, reason }) => {
      try {
        const client = discordManager.getClient();
        const channel = await client.channels
          .fetch(channelId)
          .catch(() => null);
        if (!channel || !channel.isTextBased()) {
          throw new ChannelNotFoundError(channelId);
        }
        // Check permissions
        if ("guild" in channel && channel.guild) {
          const permissions = channel.permissionsFor(client.user!);
          if (!permissions?.has(PermissionFlagsBits.ManageMessages)) {
            throw new PermissionDeniedError("ManageMessages", channelId);
          }
        }
        // Fetch messages
        const messages = await channel.messages.fetch({ limit });
        // Apply filters
        let messagesToDelete = Array.from(messages.values());
        if (filterUserId) {
          messagesToDelete = messagesToDelete.filter(
            (msg) => msg.author.id === filterUserId,
          );
        }
        if (filterBots) {
          messagesToDelete = messagesToDelete.filter((msg) => msg.author.bot);
        }
        // Filter out messages older than 14 days (Discord limitation)
        const twoWeeksAgo = Date.now() - 14 * 24 * 60 * 60 * 1000;
        messagesToDelete = messagesToDelete.filter(
          (msg) => msg.createdTimestamp > twoWeeksAgo,
        );
        if (messagesToDelete.length === 0) {
          throw new Error(
            "No messages found matching criteria or all messages are too old (>14 days)",
          );
        }
        // Bulk delete
        if (!("bulkDelete" in channel)) {
          throw new ChannelNotFoundError(channelId);
        }
        const deleted = await (channel as any).bulkDelete(
          messagesToDelete,
          true,
        );
        const deletedCount = deleted.size;
        const output = {
          success: true,
          deletedCount,
        };
        logger.info("Bulk delete completed", {
          channelId,
          deletedCount,
          reason,
        });
        return {
          content: [
            {
              type: "text" as const,
              text: `Successfully deleted ${deletedCount} message(s)`,
            },
          ],
          structuredContent: output,
        };
      } catch (error: any) {
        logger.error("Failed to bulk delete messages", {
          error: error.message,
          channelId,
        });
        const output = {
          success: false,
          error: error.message,
        };
        return {
          content: [
            {
              type: "text" as const,
              text: `Failed to bulk delete messages: ${error.message}`,
            },
          ],
          structuredContent: output,
          isError: true,
        };
      }
    },
  );
  // Get Bans Tool
  server.registerTool(
    "get_bans",
    {
      title: "Get Banned Users",
      description: "List all banned users in the server",
      inputSchema: {
        guildId: z.string().describe("Guild ID"),
        limit: z
          .number()
          .int()
          .min(1)
          .max(1000)
          .optional()
          .default(100)
          .describe("Max bans to retrieve (max 1000)"),
      },
      outputSchema: {
        success: z.boolean(),
        bans: z
          .array(
            z.object({
              userId: z.string(),
              username: z.string(),
              reason: z.string().nullable(),
            }),
          )
          .optional(),
        totalCount: z.number().optional(),
        error: z.string().optional(),
      },
    },
    async ({ guildId, limit = 100 }) => {
      try {
        const client = discordManager.getClient();
        const guild = await client.guilds.fetch(guildId).catch(() => null);
        if (!guild) {
          throw new GuildNotFoundError(guildId);
        }
        const botMember = await guild.members.fetchMe();
        if (!botMember.permissions.has(PermissionFlagsBits.BanMembers)) {
          throw new PermissionDeniedError("BanMembers", guildId);
        }
        const bans = await guild.bans.fetch({ limit });
        const banList = bans.map((ban) => ({
          userId: ban.user.id,
          username: ban.user.username,
          reason: ban.reason,
        }));
        const output = {
          success: true,
          bans: banList,
          totalCount: banList.length,
        };
        logger.info("Bans retrieved", { guildId, count: banList.length });
        return {
          content: [
            {
              type: "text" as const,
              text: `Found ${banList.length} banned user(s)`,
            },
          ],
          structuredContent: output,
        };
      } catch (error: any) {
        logger.error("Failed to get bans", {
          error: error.message,
          guildId,
        });
        const output = {
          success: false,
          error: error.message,
        };
        return {
          content: [
            {
              type: "text" as const,
              text: `Failed to get bans: ${error.message}`,
            },
          ],
          structuredContent: output,
          isError: true,
        };
      }
    },
  );
  // Nickname Member Tool
  server.registerTool(
    "set_nickname",
    {
      title: "Set Member Nickname",
      description: "Change a member's server nickname",
      inputSchema: {
        guildId: z.string().describe("Guild ID"),
        userId: z.string().describe("User ID"),
        nickname: z
          .string()
          .max(32)
          .optional()
          .describe("New nickname (null/empty to remove)"),
        reason: z
          .string()
          .optional()
          .describe("Reason for nickname change (audit log)"),
      },
      outputSchema: {
        success: z.boolean(),
        userId: z.string().optional(),
        nickname: z.string().nullable().optional(),
        error: z.string().optional(),
      },
    },
    async ({ guildId, userId, nickname, reason }) => {
      try {
        const client = discordManager.getClient();
        const guild = await client.guilds.fetch(guildId).catch(() => null);
        if (!guild) {
          throw new GuildNotFoundError(guildId);
        }
        const botMember = await guild.members.fetchMe();
        if (!botMember.permissions.has(PermissionFlagsBits.ManageNicknames)) {
          throw new PermissionDeniedError("ManageNicknames", guildId);
        }
        const member = await guild.members.fetch(userId).catch(() => null);
        if (!member) {
          throw new Error(`Member ${userId} not found in server`);
        }
        // Check role hierarchy (can't change nickname of higher role)
        if (member.roles.highest.position >= botMember.roles.highest.position) {
          throw new Error(
            "Cannot change nickname - member's highest role is equal to or higher than bot's",
          );
        }
        await member.setNickname(nickname || null, reason);
        const output = {
          success: true,
          userId,
          nickname: nickname || null,
        };
        logger.info("Nickname set", { guildId, userId, nickname, reason });
        return {
          content: [
            {
              type: "text" as const,
              text: nickname
                ? `Nickname set to "${nickname}" for user ${userId}`
                : `Nickname removed for user ${userId}`,
            },
          ],
          structuredContent: output,
        };
      } catch (error: any) {
        logger.error("Failed to set nickname", {
          error: error.message,
          guildId,
          userId,
        });
        const output = {
          success: false,
          error: error.message,
        };
        return {
          content: [
            {
              type: "text" as const,
              text: `Failed to set nickname: ${error.message}`,
            },
          ],
          structuredContent: output,
          isError: true,
        };
      }
    },
  );
}