emojis.ts•11.4 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, InvalidInputError } from "../errors/discord.js";
import { validateGuildAccess } from "../utils/guild-validation.js";
import { PermissionFlagsBits } from "discord.js";
export function registerEmojiTools(
  server: McpServer,
  discordManager: DiscordClientManager,
  logger: Logger,
) {
  // List Guild Emojis Tool
  server.registerTool(
    "list_guild_emojis",
    {
      title: "List Guild Emojis",
      description: "Get all custom emojis for a guild",
      inputSchema: {
        guildId: z.string().describe("Guild ID"),
      },
      outputSchema: {
        success: z.boolean(),
        emojis: z
          .array(
            z.object({
              id: z.string(),
              name: z.string(),
              animated: z.boolean(),
              available: z.boolean(),
              managed: z.boolean(),
              requireColons: z.boolean(),
              roles: z.array(z.string()).optional(),
              creator: z
                .object({
                  id: z.string(),
                  username: z.string(),
                  discriminator: z.string(),
                })
                .optional(),
            }),
          )
          .optional(),
        count: z.number().optional(),
        error: z.string().optional(),
      },
    },
    async ({ guildId }) => {
      try {
        const client = discordManager.getClient();
        const guild = await validateGuildAccess(client, guildId);
        // Fetch all emojis
        const emojis = await guild.emojis.fetch();
        const emojiList = emojis.map((emoji) => ({
          id: emoji.id,
          name: emoji.name || "unknown",
          animated: emoji.animated || false,
          available: emoji.available !== false,
          managed: emoji.managed || false,
          requireColons: emoji.requiresColons || false,
          roles: emoji.roles?.cache.map((r) => r.id) || [],
          creator: emoji.author
            ? {
                id: emoji.author.id,
                username: emoji.author.username,
                discriminator: emoji.author.discriminator,
              }
            : undefined,
        }));
        const output = {
          success: true,
          emojis: emojiList,
          count: emojiList.length,
        };
        logger.info("Listed guild emojis", { guildId, count: emojiList.length });
        return {
          content: [
            {
              type: "text" as const,
              text: `Found ${emojiList.length} custom emojis in guild ${guild.name}`,
            },
          ],
          structuredContent: output,
        };
      } catch (error: any) {
        logger.error("Failed to list guild emojis", {
          error: error.message,
          guildId,
        });
        return {
          content: [
            {
              type: "text" as const,
              text: `Error: ${error.message}`,
            },
          ],
          structuredContent: {
            success: false,
            error: error.message,
          },
        };
      }
    },
  );
  // Create Emoji Tool
  server.registerTool(
    "create_emoji",
    {
      title: "Create Guild Emoji",
      description:
        "Upload a custom emoji to the guild from base64 image data or file path",
      inputSchema: {
        guildId: z.string().describe("Guild ID"),
        name: z
          .string()
          .min(2)
          .max(32)
          .regex(/^\w+$/)
          .describe("Emoji name (2-32 alphanumeric characters and underscores)"),
        image: z
          .string()
          .describe(
            "Base64 encoded image data (data:image/png;base64,...) or file path",
          ),
        roles: z
          .array(z.string())
          .optional()
          .describe("Array of role IDs that can use this emoji"),
        reason: z.string().optional().describe("Audit log reason"),
      },
      outputSchema: {
        success: z.boolean(),
        emoji: z
          .object({
            id: z.string(),
            name: z.string(),
            animated: z.boolean(),
            url: z.string(),
          })
          .optional(),
        error: z.string().optional(),
      },
    },
    async ({ guildId, name, image, roles, reason }) => {
      try {
        const client = discordManager.getClient();
        const guild = await validateGuildAccess(client, guildId);
        // Check permissions
        const botMember = await guild.members.fetch(client.user!.id);
        if (
          !botMember.permissions.has(PermissionFlagsBits.ManageGuildExpressions)
        ) {
          throw new PermissionDeniedError(
            "ManageGuildExpressions",
            guildId,
          );
        }
        // Validate image format
        if (!image.startsWith("data:image/") && !image.startsWith("/")) {
          throw new InvalidInputError(
            "image",
            "Must be base64 data URI or absolute file path",
          );
        }
        // Create emoji
        const emoji = await guild.emojis.create({
          attachment: image,
          name: name,
          roles: roles || [],
          reason: reason,
        });
        const output = {
          success: true,
          emoji: {
            id: emoji.id,
            name: emoji.name || name,
            animated: emoji.animated || false,
            url: emoji.imageURL() || "",
          },
        };
        logger.info("Created emoji", {
          guildId,
          emojiId: emoji.id,
          name,
        });
        return {
          content: [
            {
              type: "text" as const,
              text: `Created emoji :${emoji.name}: in ${guild.name}`,
            },
          ],
          structuredContent: output,
        };
      } catch (error: any) {
        logger.error("Failed to create emoji", {
          error: error.message,
          guildId,
          name,
        });
        return {
          content: [
            {
              type: "text" as const,
              text: `Error: ${error.message}`,
            },
          ],
          structuredContent: {
            success: false,
            error: error.message,
          },
        };
      }
    },
  );
  // Modify Emoji Tool
  server.registerTool(
    "modify_emoji",
    {
      title: "Modify Guild Emoji",
      description: "Update an existing custom emoji's name or role restrictions",
      inputSchema: {
        guildId: z.string().describe("Guild ID"),
        emojiId: z.string().describe("Emoji ID to modify"),
        name: z
          .string()
          .min(2)
          .max(32)
          .regex(/^\w+$/)
          .optional()
          .describe("New emoji name"),
        roles: z
          .array(z.string())
          .optional()
          .describe("New array of role IDs (replaces existing)"),
        reason: z.string().optional().describe("Audit log reason"),
      },
      outputSchema: {
        success: z.boolean(),
        emoji: z
          .object({
            id: z.string(),
            name: z.string(),
            roles: z.array(z.string()),
          })
          .optional(),
        error: z.string().optional(),
      },
    },
    async ({ guildId, emojiId, name, roles, reason }) => {
      try {
        const client = discordManager.getClient();
        const guild = await validateGuildAccess(client, guildId);
        // Check permissions
        const botMember = await guild.members.fetch(client.user!.id);
        if (
          !botMember.permissions.has(PermissionFlagsBits.ManageGuildExpressions)
        ) {
          throw new PermissionDeniedError(
            "ManageGuildExpressions",
            guildId,
          );
        }
        // Fetch emoji
        const emoji = await guild.emojis.fetch(emojiId);
        if (!emoji) {
          throw new InvalidInputError("emojiId", "Emoji not found");
        }
        // Update emoji
        const updated = await emoji.edit({
          name: name,
          roles: roles,
          reason: reason,
        });
        const output = {
          success: true,
          emoji: {
            id: updated.id,
            name: updated.name || "",
            roles: updated.roles?.cache.map((r) => r.id) || [],
          },
        };
        logger.info("Modified emoji", {
          guildId,
          emojiId,
        });
        return {
          content: [
            {
              type: "text" as const,
              text: `Updated emoji :${updated.name}: in ${guild.name}`,
            },
          ],
          structuredContent: output,
        };
      } catch (error: any) {
        logger.error("Failed to modify emoji", {
          error: error.message,
          guildId,
          emojiId,
        });
        return {
          content: [
            {
              type: "text" as const,
              text: `Error: ${error.message}`,
            },
          ],
          structuredContent: {
            success: false,
            error: error.message,
          },
        };
      }
    },
  );
  // Delete Emoji Tool
  server.registerTool(
    "delete_emoji",
    {
      title: "Delete Guild Emoji",
      description: "Delete a custom emoji from the guild",
      inputSchema: {
        guildId: z.string().describe("Guild ID"),
        emojiId: z.string().describe("Emoji ID to delete"),
        reason: z.string().optional().describe("Audit log reason"),
      },
      outputSchema: {
        success: z.boolean(),
        emojiId: z.string().optional(),
        error: z.string().optional(),
      },
    },
    async ({ guildId, emojiId, reason }) => {
      try {
        const client = discordManager.getClient();
        const guild = await validateGuildAccess(client, guildId);
        // Check permissions
        const botMember = await guild.members.fetch(client.user!.id);
        if (
          !botMember.permissions.has(PermissionFlagsBits.ManageGuildExpressions)
        ) {
          throw new PermissionDeniedError(
            "ManageGuildExpressions",
            guildId,
          );
        }
        // Fetch emoji
        const emoji = await guild.emojis.fetch(emojiId);
        if (!emoji) {
          throw new InvalidInputError("emojiId", "Emoji not found");
        }
        const emojiName = emoji.name;
        // Delete emoji
        await emoji.delete(reason);
        const output = {
          success: true,
          emojiId: emojiId,
        };
        logger.info("Deleted emoji", {
          guildId,
          emojiId,
        });
        return {
          content: [
            {
              type: "text" as const,
              text: `Deleted emoji :${emojiName}: from ${guild.name}`,
            },
          ],
          structuredContent: output,
        };
      } catch (error: any) {
        logger.error("Failed to delete emoji", {
          error: error.message,
          guildId,
          emojiId,
        });
        return {
          content: [
            {
              type: "text" as const,
              text: `Error: ${error.message}`,
            },
          ],
          structuredContent: {
            success: false,
            error: error.message,
          },
        };
      }
    },
  );
}