channels.tsā¢41.1 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 { ChannelType, PermissionFlagsBits } from "discord.js";
export function registerChannelTools(
  server: McpServer,
  discordManager: DiscordClientManager,
  logger: Logger,
) {
  // Create Text Channel Tool
  server.registerTool(
    "create_text_channel",
    {
      title: "Create Text Channel",
      description: "Create a new text channel in a Discord server",
      inputSchema: {
        guildId: z.string().describe("Guild ID"),
        name: z.string().min(1).max(100).describe("Channel name"),
        topic: z
          .string()
          .max(1024)
          .optional()
          .describe("Channel topic/description"),
        parent: z.string().optional().describe("Parent category ID"),
        nsfw: z.boolean().default(false).describe("Mark channel as NSFW"),
      },
      outputSchema: {
        success: z.boolean(),
        channel: z
          .object({
            id: z.string(),
            name: z.string(),
            type: z.number(),
            position: z.number(),
            parentId: z.string().nullable(),
          })
          .optional(),
        error: z.string().optional(),
      },
    },
    async ({ guildId, name, topic, parent, nsfw }) => {
      try {
        const client = discordManager.getClient();
        const guild = await client.guilds.fetch(guildId).catch(() => null);
        if (!guild) {
          throw new GuildNotFoundError(guildId);
        }
        // Check permissions
        const botMember = await guild.members.fetchMe();
        if (!botMember.permissions.has(PermissionFlagsBits.ManageChannels)) {
          throw new PermissionDeniedError("ManageChannels", guildId);
        }
        const channel = await guild.channels.create({
          name,
          type: ChannelType.GuildText,
          topic,
          parent: parent || undefined,
          nsfw,
        });
        const output = {
          success: true,
          channel: {
            id: channel.id,
            name: channel.name,
            type: channel.type,
            position: channel.position,
            parentId: channel.parentId,
          },
        };
        logger.info("Channel created", {
          guildId,
          channelId: channel.id,
          name,
        });
        return {
          content: [
            {
              type: "text" as const,
              text: `Channel <#${channel.id}> created successfully`,
            },
          ],
          structuredContent: output,
        };
      } catch (error: any) {
        logger.error("Failed to create channel", {
          error: error.message,
          guildId,
          name,
        });
        const output = {
          success: false,
          error: error.message,
        };
        return {
          content: [
            {
              type: "text" as const,
              text: `Failed to create channel: ${error.message}`,
            },
          ],
          structuredContent: output,
          isError: true,
        };
      }
    },
  );
  // Delete Channel Tool
  server.registerTool(
    "delete_channel",
    {
      title: "Delete Channel",
      description: "Permanently delete a Discord channel",
      inputSchema: {
        channelId: z.string().describe("Channel ID to delete"),
        reason: z
          .string()
          .optional()
          .describe("Reason for deletion (audit log)"),
      },
      outputSchema: {
        success: z.boolean(),
        deletedChannelId: z.string().optional(),
        error: z.string().optional(),
      },
    },
    async ({ channelId, reason }) => {
      try {
        const client = discordManager.getClient();
        const channel = await client.channels
          .fetch(channelId)
          .catch(() => null);
        if (!channel) {
          throw new ChannelNotFoundError(channelId);
        }
        // Check permissions
        if ("guild" in channel && channel.guild) {
          const botMember = await channel.guild.members.fetchMe();
          if (!botMember.permissions.has(PermissionFlagsBits.ManageChannels)) {
            throw new PermissionDeniedError("ManageChannels", channel.guild.id);
          }
        }
        await channel.delete(reason);
        const output = {
          success: true,
          deletedChannelId: channelId,
        };
        logger.info("Channel deleted", { channelId, reason });
        return {
          content: [
            {
              type: "text" as const,
              text: `Channel ${channelId} deleted successfully`,
            },
          ],
          structuredContent: output,
        };
      } catch (error: any) {
        logger.error("Failed to delete channel", {
          error: error.message,
          channelId,
        });
        const output = {
          success: false,
          error: error.message,
        };
        return {
          content: [
            {
              type: "text" as const,
              text: `Failed to delete channel: ${error.message}`,
            },
          ],
          structuredContent: output,
          isError: true,
        };
      }
    },
  );
  // Get Server Info Tool
  server.registerTool(
    "get_server_info",
    {
      title: "Get Server Information",
      description: "Retrieve detailed information about a Discord server",
      inputSchema: {
        guildId: z.string().describe("Guild ID"),
      },
      outputSchema: {
        success: z.boolean(),
        guild: z
          .object({
            id: z.string(),
            name: z.string(),
            ownerId: z.string(),
            memberCount: z.number(),
            channelCount: z.number(),
            roleCount: z.number(),
            description: z.string().nullable(),
            createdTimestamp: z.number(),
          })
          .optional(),
        error: z.string().optional(),
      },
    },
    async ({ guildId }) => {
      try {
        const client = discordManager.getClient();
        const guild = await client.guilds.fetch(guildId).catch(() => null);
        if (!guild) {
          throw new GuildNotFoundError(guildId);
        }
        const output = {
          success: true,
          guild: {
            id: guild.id,
            name: guild.name,
            ownerId: guild.ownerId,
            memberCount: guild.memberCount,
            channelCount: guild.channels.cache.size,
            roleCount: guild.roles.cache.size,
            description: guild.description,
            createdTimestamp: guild.createdTimestamp,
          },
        };
        logger.info("Server info retrieved", { guildId });
        return {
          content: [
            {
              type: "text" as const,
              text: `Server: ${guild.name}\nMembers: ${guild.memberCount}\nChannels: ${guild.channels.cache.size}`,
            },
          ],
          structuredContent: output,
        };
      } catch (error: any) {
        logger.error("Failed to get server info", {
          error: error.message,
          guildId,
        });
        const output = {
          success: false,
          error: error.message,
        };
        return {
          content: [
            {
              type: "text" as const,
              text: `Failed to get server info: ${error.message}`,
            },
          ],
          structuredContent: output,
          isError: true,
        };
      }
    },
  );
  // Create Voice Channel Tool
  server.registerTool(
    "create_voice_channel",
    {
      title: "Create Voice Channel",
      description: "Create a new voice channel in the server",
      inputSchema: {
        guildId: z.string().describe("Guild ID"),
        name: z.string().min(1).max(100).describe("Channel name"),
        parent: z.string().optional().describe("Parent category ID"),
        userLimit: z
          .number()
          .int()
          .min(0)
          .max(99)
          .optional()
          .describe("User limit (0 = unlimited)"),
        bitrate: z
          .number()
          .int()
          .min(8000)
          .max(384000)
          .optional()
          .describe("Audio bitrate in bps (8000-384000)"),
      },
      outputSchema: {
        success: z.boolean(),
        channel: z
          .object({
            id: z.string(),
            name: z.string(),
            type: z.number(),
            userLimit: z.number(),
            bitrate: z.number(),
          })
          .optional(),
        error: z.string().optional(),
      },
    },
    async ({ guildId, name, parent, userLimit, bitrate }) => {
      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.ManageChannels)) {
          throw new PermissionDeniedError("ManageChannels", guildId);
        }
        const channel = await guild.channels.create({
          name,
          type: ChannelType.GuildVoice,
          parent: parent || undefined,
          userLimit: userLimit,
          bitrate: bitrate,
        });
        const output = {
          success: true,
          channel: {
            id: channel.id,
            name: channel.name,
            type: channel.type,
            userLimit: (channel as any).userLimit || 0,
            bitrate: (channel as any).bitrate || 64000,
          },
        };
        logger.info("Voice channel created", {
          guildId,
          channelId: channel.id,
        });
        return {
          content: [
            {
              type: "text" as const,
              text: `Voice channel "${name}" created successfully (ID: ${channel.id})`,
            },
          ],
          structuredContent: output,
        };
      } catch (error: any) {
        logger.error("Failed to create voice channel", {
          error: error.message,
          guildId,
        });
        const output = {
          success: false,
          error: error.message,
        };
        return {
          content: [
            {
              type: "text" as const,
              text: `Failed to create voice channel: ${error.message}`,
            },
          ],
          structuredContent: output,
          isError: true,
        };
      }
    },
  );
  // Create Category Tool
  server.registerTool(
    "create_category",
    {
      title: "Create Channel Category",
      description: "Create a new category to organize channels",
      inputSchema: {
        guildId: z.string().describe("Guild ID"),
        name: z.string().min(1).max(100).describe("Category name"),
      },
      outputSchema: {
        success: z.boolean(),
        category: z
          .object({
            id: z.string(),
            name: z.string(),
            type: z.number(),
            position: z.number(),
          })
          .optional(),
        error: z.string().optional(),
      },
    },
    async ({ guildId, name }) => {
      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.ManageChannels)) {
          throw new PermissionDeniedError("ManageChannels", guildId);
        }
        const category = await guild.channels.create({
          name,
          type: ChannelType.GuildCategory,
        });
        const output = {
          success: true,
          category: {
            id: category.id,
            name: category.name,
            type: category.type,
            position: category.position,
          },
        };
        logger.info("Category created", { guildId, categoryId: category.id });
        return {
          content: [
            {
              type: "text" as const,
              text: `Category "${name}" created successfully (ID: ${category.id})`,
            },
          ],
          structuredContent: output,
        };
      } catch (error: any) {
        logger.error("Failed to create category", {
          error: error.message,
          guildId,
        });
        const output = {
          success: false,
          error: error.message,
        };
        return {
          content: [
            {
              type: "text" as const,
              text: `Failed to create category: ${error.message}`,
            },
          ],
          structuredContent: output,
          isError: true,
        };
      }
    },
  );
  // Create Forum Channel Tool
  server.registerTool(
    "create_forum_channel",
    {
      title: "Create Forum Channel",
      description: "Create a new forum channel for threaded discussions",
      inputSchema: {
        guildId: z.string().describe("Guild ID"),
        name: z.string().min(1).max(100).describe("Forum name"),
        topic: z.string().max(1024).optional().describe("Forum description"),
        parent: z.string().optional().describe("Parent category ID"),
        tags: z
          .array(z.string())
          .max(20)
          .optional()
          .describe("Forum tags (max 20)"),
      },
      outputSchema: {
        success: z.boolean(),
        forum: z
          .object({
            id: z.string(),
            name: z.string(),
            type: z.number(),
          })
          .optional(),
        error: z.string().optional(),
      },
    },
    async ({ guildId, name, topic, parent, tags }) => {
      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.ManageChannels)) {
          throw new PermissionDeniedError("ManageChannels", guildId);
        }
        const availableTags = tags
          ? tags.map((tag) => ({ name: tag }))
          : undefined;
        const forum = await guild.channels.create({
          name,
          type: ChannelType.GuildForum,
          topic: topic,
          parent: parent || undefined,
          availableTags,
        });
        const output = {
          success: true,
          forum: {
            id: forum.id,
            name: forum.name,
            type: forum.type,
          },
        };
        logger.info("Forum created", { guildId, forumId: forum.id });
        return {
          content: [
            {
              type: "text" as const,
              text: `Forum "${name}" created successfully (ID: ${forum.id})`,
            },
          ],
          structuredContent: output,
        };
      } catch (error: any) {
        logger.error("Failed to create forum", {
          error: error.message,
          guildId,
        });
        const output = {
          success: false,
          error: error.message,
        };
        return {
          content: [
            {
              type: "text" as const,
              text: `Failed to create forum: ${error.message}`,
            },
          ],
          structuredContent: output,
          isError: true,
        };
      }
    },
  );
  // Modify Channel Tool
  server.registerTool(
    "modify_channel",
    {
      title: "Modify Channel Settings",
      description: "Update channel name, topic, slowmode, or other settings",
      inputSchema: {
        channelId: z.string().describe("Channel ID to modify"),
        name: z
          .string()
          .min(1)
          .max(100)
          .optional()
          .describe("New channel name"),
        topic: z.string().max(1024).optional().describe("New channel topic"),
        nsfw: z.boolean().optional().describe("Mark channel as NSFW"),
        slowmode: z
          .number()
          .int()
          .min(0)
          .max(21600)
          .optional()
          .describe("Slowmode delay in seconds (0-21600)"),
        reason: z
          .string()
          .optional()
          .describe("Reason for modification (audit log)"),
      },
      outputSchema: {
        success: z.boolean(),
        channel: z
          .object({
            id: z.string(),
            name: z.string(),
          })
          .optional(),
        error: z.string().optional(),
      },
    },
    async ({ channelId, name, topic, nsfw, slowmode, reason }) => {
      try {
        const client = discordManager.getClient();
        const channel = await client.channels
          .fetch(channelId)
          .catch(() => null);
        if (!channel) {
          throw new ChannelNotFoundError(channelId);
        }
        if (!("guild" in channel) || !channel.guild) {
          throw new Error("This tool only works with server channels");
        }
        const botMember = await channel.guild.members.fetchMe();
        if (!botMember.permissions.has(PermissionFlagsBits.ManageChannels)) {
          throw new PermissionDeniedError("ManageChannels", channel.guild.id);
        }
        const updateOptions: any = {};
        if (name !== undefined) updateOptions.name = name;
        if (topic !== undefined) updateOptions.topic = topic;
        if (nsfw !== undefined) updateOptions.nsfw = nsfw;
        if (slowmode !== undefined) updateOptions.rateLimitPerUser = slowmode;
        if (reason !== undefined) updateOptions.reason = reason;
        const updatedChannel = await (channel as any).edit(updateOptions);
        const output = {
          success: true,
          channel: {
            id: updatedChannel.id,
            name: updatedChannel.name,
          },
        };
        logger.info("Channel modified", { channelId, reason });
        return {
          content: [
            {
              type: "text" as const,
              text: `Channel "${updatedChannel.name}" modified successfully`,
            },
          ],
          structuredContent: output,
        };
      } catch (error: any) {
        logger.error("Failed to modify channel", {
          error: error.message,
          channelId,
        });
        const output = {
          success: false,
          error: error.message,
        };
        return {
          content: [
            {
              type: "text" as const,
              text: `Failed to modify channel: ${error.message}`,
            },
          ],
          structuredContent: output,
          isError: true,
        };
      }
    },
  );
  // Create Thread Tool
  server.registerTool(
    "create_thread",
    {
      title: "Create Discussion Thread",
      description: "Create a new thread in a channel or from a message",
      inputSchema: {
        channelId: z.string().describe("Channel ID to create thread in"),
        name: z.string().min(1).max(100).describe("Thread name"),
        message: z
          .string()
          .optional()
          .describe("Initial message content for thread"),
        autoArchiveDuration: z
          .enum(["60", "1440", "4320", "10080"])
          .optional()
          .describe(
            "Auto-archive after inactivity: 60=1hr, 1440=24hr, 4320=3d, 10080=7d",
          ),
      },
      outputSchema: {
        success: z.boolean(),
        thread: z
          .object({
            id: z.string(),
            name: z.string(),
            parentId: z.string(),
          })
          .optional(),
        error: z.string().optional(),
      },
    },
    async ({ channelId, name, message, autoArchiveDuration }) => {
      try {
        const client = discordManager.getClient();
        const channel = await client.channels
          .fetch(channelId)
          .catch(() => null);
        if (!channel || !channel.isTextBased()) {
          throw new ChannelNotFoundError(channelId);
        }
        if ("guild" in channel && channel.guild) {
          const permissions = channel.permissionsFor(client.user!);
          if (!permissions?.has(PermissionFlagsBits.CreatePublicThreads)) {
            throw new PermissionDeniedError("CreatePublicThreads", channelId);
          }
        }
        // Create thread
        if (!("threads" in channel)) {
          throw new Error("This channel does not support threads");
        }
        const thread = await (channel as any).threads.create({
          name,
          autoArchiveDuration: autoArchiveDuration
            ? parseInt(autoArchiveDuration)
            : 60,
          reason: "Thread created via MCP",
        });
        // Send initial message if provided
        if (message) {
          await thread.send(message);
        }
        const output = {
          success: true,
          thread: {
            id: thread.id,
            name: thread.name,
            parentId: thread.parentId || channelId,
          },
        };
        logger.info("Thread created", { channelId, threadId: thread.id });
        return {
          content: [
            {
              type: "text" as const,
              text: `Thread "${name}" created successfully (ID: ${thread.id})`,
            },
          ],
          structuredContent: output,
        };
      } catch (error: any) {
        logger.error("Failed to create thread", {
          error: error.message,
          channelId,
        });
        const output = {
          success: false,
          error: error.message,
        };
        return {
          content: [
            {
              type: "text" as const,
              text: `Failed to create thread: ${error.message}`,
            },
          ],
          structuredContent: output,
          isError: true,
        };
      }
    },
  );
  // Archive Thread Tool
  server.registerTool(
    "archive_thread",
    {
      title: "Archive Thread",
      description: "Archive (lock) a thread",
      inputSchema: {
        threadId: z.string().describe("Thread ID to archive"),
        locked: z
          .boolean()
          .optional()
          .default(true)
          .describe("Lock thread (prevents new messages)"),
      },
      outputSchema: {
        success: z.boolean(),
        threadId: z.string().optional(),
        error: z.string().optional(),
      },
    },
    async ({ threadId, locked = true }) => {
      try {
        const client = discordManager.getClient();
        const thread = await client.channels.fetch(threadId).catch(() => null);
        if (!thread || !thread.isThread()) {
          throw new Error(`Thread ${threadId} not found or is not a thread`);
        }
        if (thread.guild) {
          const botMember = await thread.guild.members.fetchMe();
          if (!botMember.permissions.has(PermissionFlagsBits.ManageThreads)) {
            throw new PermissionDeniedError("ManageThreads", thread.guild.id);
          }
        }
        await thread.setArchived(true);
        if (locked) {
          await thread.setLocked(true);
        }
        const output = {
          success: true,
          threadId,
        };
        logger.info("Thread archived", { threadId, locked });
        return {
          content: [
            {
              type: "text" as const,
              text: `Thread archived successfully${locked ? " and locked" : ""}`,
            },
          ],
          structuredContent: output,
        };
      } catch (error: any) {
        logger.error("Failed to archive thread", {
          error: error.message,
          threadId,
        });
        const output = {
          success: false,
          error: error.message,
        };
        return {
          content: [
            {
              type: "text" as const,
              text: `Failed to archive thread: ${error.message}`,
            },
          ],
          structuredContent: output,
          isError: true,
        };
      }
    },
  );
  // List Channels Tool
  server.registerTool(
    "list_channels",
    {
      title: "List Server Channels",
      description: "Get all channels in the server organized by type",
      inputSchema: {
        guildId: z.string().describe("Guild ID"),
        type: z
          .enum(["text", "voice", "category", "forum", "all"])
          .optional()
          .default("all")
          .describe("Filter by channel type"),
      },
      outputSchema: {
        success: z.boolean(),
        channels: z
          .array(
            z.object({
              id: z.string(),
              name: z.string(),
              type: z.string(),
              position: z.number(),
              parentId: z.string().nullable(),
            }),
          )
          .optional(),
        totalCount: z.number().optional(),
        error: z.string().optional(),
      },
    },
    async ({ guildId, type = "all" }) => {
      try {
        const client = discordManager.getClient();
        const guild = await client.guilds.fetch(guildId).catch(() => null);
        if (!guild) {
          throw new GuildNotFoundError(guildId);
        }
        const channels = await guild.channels.fetch();
        let filteredChannels = Array.from(channels.values()).filter(
          (c): c is NonNullable<typeof c> => c !== null,
        );
        // Filter by type
        if (type !== "all") {
          const typeMap: Record<string, ChannelType> = {
            text: ChannelType.GuildText,
            voice: ChannelType.GuildVoice,
            category: ChannelType.GuildCategory,
            forum: ChannelType.GuildForum,
          };
          filteredChannels = filteredChannels.filter(
            (c) => c.type === typeMap[type],
          );
        }
        const channelList = filteredChannels.map((channel) => ({
          id: channel.id,
          name: channel.name,
          type: ChannelType[channel.type],
          position: channel.position,
          parentId: channel.parentId,
        }));
        // Sort by position
        channelList.sort((a, b) => a.position - b.position);
        const output = {
          success: true,
          channels: channelList,
          totalCount: channelList.length,
        };
        logger.info("Channels listed", {
          guildId,
          type,
          count: channelList.length,
        });
        return {
          content: [
            {
              type: "text" as const,
              text: `Found ${channelList.length} channel(s) in server`,
            },
          ],
          structuredContent: output,
        };
      } catch (error: any) {
        logger.error("Failed to list channels", {
          error: error.message,
          guildId,
        });
        const output = {
          success: false,
          error: error.message,
        };
        return {
          content: [
            {
              type: "text" as const,
              text: `Failed to list channels: ${error.message}`,
            },
          ],
          structuredContent: output,
          isError: true,
        };
      }
    },
  );
  // Create Stage Channel Tool
  server.registerTool(
    "create_stage_channel",
    {
      title: "Create Stage Channel",
      description: "Create a stage voice channel for presentations and events",
      inputSchema: {
        guildId: z.string().describe("Guild ID"),
        name: z.string().min(1).max(100).describe("Stage channel name"),
        topic: z.string().max(1024).optional().describe("Stage topic"),
        parent: z.string().optional().describe("Parent category ID"),
      },
      outputSchema: {
        success: z.boolean(),
        channel: z
          .object({
            id: z.string(),
            name: z.string(),
            type: z.number(),
          })
          .optional(),
        error: z.string().optional(),
      },
    },
    async ({ guildId, name, topic, parent }) => {
      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.ManageChannels)) {
          throw new PermissionDeniedError("ManageChannels", guildId);
        }
        const channel = await guild.channels.create({
          name,
          type: ChannelType.GuildStageVoice,
          topic: topic,
          parent: parent || undefined,
        });
        const output = {
          success: true,
          channel: {
            id: channel.id,
            name: channel.name,
            type: channel.type,
          },
        };
        logger.info("Stage channel created", {
          guildId,
          channelId: channel.id,
        });
        return {
          content: [
            {
              type: "text" as const,
              text: `Stage channel "${name}" created successfully (ID: ${channel.id})`,
            },
          ],
          structuredContent: output,
        };
      } catch (error: any) {
        logger.error("Failed to create stage channel", {
          error: error.message,
          guildId,
        });
        const output = {
          success: false,
          error: error.message,
        };
        return {
          content: [
            {
              type: "text" as const,
              text: `Failed to create stage channel: ${error.message}`,
            },
          ],
          structuredContent: output,
          isError: true,
        };
      }
    },
  );
  // Set Channel Permissions Tool
  server.registerTool(
    "set_channel_permissions",
    {
      title: "Set Channel Permissions",
      description:
        "Override permissions for a role or member on a specific channel",
      inputSchema: {
        channelId: z.string().describe("Channel ID"),
        targetId: z
          .string()
          .describe("Role ID or User ID to set permissions for"),
        targetType: z
          .enum(["role", "member"])
          .describe("Whether target is a role or member"),
        allow: z
          .array(z.string())
          .optional()
          .describe("Array of permission names to allow"),
        deny: z
          .array(z.string())
          .optional()
          .describe("Array of permission names to deny"),
      },
      outputSchema: {
        success: z.boolean(),
        channelId: z.string().optional(),
        targetId: z.string().optional(),
        error: z.string().optional(),
      },
    },
    async ({ channelId, targetId, targetType, allow, deny }) => {
      try {
        const client = discordManager.getClient();
        const channel = await client.channels
          .fetch(channelId)
          .catch(() => null);
        if (!channel) {
          throw new ChannelNotFoundError(channelId);
        }
        // Check if channel is in a guild
        if (!("guild" in channel) || !channel.guild) {
          throw new Error("Channel is not in a guild");
        }
        const botMember = await channel.guild.members.fetchMe();
        if (!botMember.permissions.has(PermissionFlagsBits.ManageChannels)) {
          throw new PermissionDeniedError("ManageChannels", channel.guild.id);
        }
        // Build permission overwrites
        const permissionOverwrites: any = {};
        if (allow && allow.length > 0) {
          permissionOverwrites.allow = allow;
        }
        if (deny && deny.length > 0) {
          permissionOverwrites.deny = deny;
        }
        // Apply permission overwrite
        await (channel as any).permissionOverwrites.create(
          targetId,
          permissionOverwrites,
        );
        const output = {
          success: true,
          channelId: channel.id,
          targetId,
        };
        logger.info("Channel permissions set", {
          channelId,
          targetId,
          targetType,
        });
        return {
          content: [
            {
              type: "text" as const,
              text: `Permissions set for ${targetType} ${targetId} on channel ${channelId}`,
            },
          ],
          structuredContent: output,
        };
      } catch (error: any) {
        logger.error("Failed to set channel permissions", {
          error: error.message,
          channelId,
          targetId,
        });
        const output = {
          success: false,
          error: error.message,
        };
        return {
          content: [
            {
              type: "text" as const,
              text: `Failed to set channel permissions: ${error.message}`,
            },
          ],
          structuredContent: output,
          isError: true,
        };
      }
    },
  );
  // Find Threads Tool
  server.registerTool(
    "find_threads",
    {
      title: "Find Threads in Forum",
      description: "Search for threads in a forum channel by name or list all threads",
      inputSchema: {
        forumId: z.string().describe("Forum channel ID"),
        name: z.string().optional().describe("Search for threads with this name (partial match)"),
        archived: z.boolean().optional().describe("Include archived threads"),
        limit: z.number().int().min(1).max(100).optional().default(50).describe("Max number of threads to return"),
      },
      outputSchema: {
        success: z.boolean(),
        threads: z.array(z.object({
          id: z.string(),
          name: z.string(),
          ownerId: z.string(),
          messageCount: z.number(),
          memberCount: z.number(),
          createdAt: z.string(),
          archived: z.boolean(),
          locked: z.boolean(),
          lastMessageAt: z.string().nullable(),
        })).optional(),
        totalCount: z.number().optional(),
        error: z.string().optional(),
      },
    },
    async ({ forumId, name, archived = false, limit = 50 }) => {
      try {
        const client = discordManager.getClient();
        const forum = await client.channels.fetch(forumId).catch(() => null);
        if (!forum || forum.type !== ChannelType.GuildForum) {
          throw new Error(`Channel ${forumId} is not a forum channel`);
        }
        if (forum.guild) {
          const botMember = await forum.guild.members.fetchMe();
          if (!botMember.permissions.has(PermissionFlagsBits.ViewChannel)) {
            throw new PermissionDeniedError("ViewChannel", forum.guild.id);
          }
        }
        // Fetch threads
        const threadsData = await forum.threads.fetchActive();
        let threads = Array.from(threadsData.threads.values());
        // Optionally fetch archived threads
        if (archived) {
          const archivedThreads = await forum.threads.fetchArchived();
          threads = threads.concat(Array.from(archivedThreads.threads.values()));
        }
        // Filter by name if provided
        if (name) {
          const searchLower = name.toLowerCase();
          threads = threads.filter(t => t.name.toLowerCase().includes(searchLower));
        }
        // Limit results
        threads = threads.slice(0, limit);
        const threadList = threads.map(thread => ({
          id: thread.id,
          name: thread.name,
          ownerId: thread.ownerId || "",
          messageCount: thread.messageCount || 0,
          memberCount: thread.memberCount || 0,
          createdAt: thread.createdAt?.toISOString() || new Date().toISOString(),
          archived: thread.archived || false,
          locked: thread.locked || false,
          lastMessageAt: thread.lastMessage?.createdAt?.toISOString() || null,
        }));
        const output = {
          success: true,
          threads: threadList,
          totalCount: threadList.length,
        };
        logger.info("Threads found", { forumId, count: threadList.length, name });
        return {
          content: [
            {
              type: "text" as const,
              text: `Found ${threadList.length} thread(s) in forum${name ? ` matching "${name}"` : ""}`,
            },
          ],
          structuredContent: output,
        };
      } catch (error: any) {
        logger.error("Failed to find threads", {
          error: error.message,
          forumId,
        });
        const output = {
          success: false,
          error: error.message,
        };
        return {
          content: [
            {
              type: "text" as const,
              text: `Failed to find threads: ${error.message}`,
            },
          ],
          structuredContent: output,
          isError: true,
        };
      }
    },
  );
  // Get Channel Details Tool
  server.registerTool(
    "get_channel_details",
    {
      title: "Get Channel Details",
      description: "Get detailed information about a channel including type, permissions, and capabilities",
      inputSchema: {
        channelId: z.string().describe("Channel ID"),
      },
      outputSchema: {
        success: z.boolean(),
        channel: z.object({
          id: z.string(),
          name: z.string(),
          type: z.number(),
          typeName: z.string(),
          topic: z.string().nullable(),
          nsfw: z.boolean().optional(),
          parentId: z.string().nullable(),
          parentName: z.string().nullable(),
          position: z.number(),
          rateLimitPerUser: z.number().optional(),
          supportsThreads: z.boolean(),
          supportsMessages: z.boolean(),
          isTextBased: z.boolean(),
          isForum: z.boolean(),
          isVoice: z.boolean(),
        }).optional(),
        error: z.string().optional(),
      },
    },
    async ({ channelId }) => {
      try {
        const client = discordManager.getClient();
        const channel = await client.channels.fetch(channelId).catch(() => null);
        if (!channel) {
          throw new ChannelNotFoundError(channelId);
        }
        // Check if this is a guild channel (not a DM)
        if (!("guild" in channel) || !channel.guild) {
          throw new Error("This tool only works with server channels, not DMs");
        }
        // Determine channel capabilities
        const supportsThreads = channel.type === ChannelType.GuildText ||
                               channel.type === ChannelType.GuildAnnouncement ||
                               channel.type === ChannelType.GuildForum;
        const supportsMessages = channel.isTextBased();
        const isForum = channel.type === ChannelType.GuildForum;
        const isVoice = channel.type === ChannelType.GuildVoice ||
                       channel.type === ChannelType.GuildStageVoice;
        // Get parent channel name if exists
        let parentName: string | null = null;
        if ("parentId" in channel && channel.parentId) {
          const parent = await channel.guild.channels.fetch(channel.parentId).catch(() => null);
          parentName = parent?.name || null;
        }
        // At this point we know it's a guild channel, so we can safely cast
        const guildChannel = channel as any;
        const channelDetails = {
          id: guildChannel.id,
          name: guildChannel.name || guildChannel.id,
          type: guildChannel.type,
          typeName: ChannelType[guildChannel.type],
          topic: guildChannel.topic || null,
          nsfw: guildChannel.nsfw ?? false,
          parentId: guildChannel.parentId || null,
          parentName,
          position: guildChannel.position ?? 0,
          rateLimitPerUser: guildChannel.rateLimitPerUser,
          supportsThreads,
          supportsMessages,
          isTextBased: guildChannel.isTextBased(),
          isForum,
          isVoice,
        };
        const output = {
          success: true,
          channel: channelDetails,
        };
        logger.info("Channel details retrieved", { channelId });
        const channelName = guildChannel.name || guildChannel.id;
        return {
          content: [
            {
              type: "text" as const,
              text: `Channel: ${channelName} (${ChannelType[channel.type]})`,
            },
          ],
          structuredContent: output,
        };
      } catch (error: any) {
        logger.error("Failed to get channel details", {
          error: error.message,
          channelId,
        });
        const output = {
          success: false,
          error: error.message,
        };
        return {
          content: [
            {
              type: "text" as const,
              text: `Failed to get channel details: ${error.message}`,
            },
          ],
          structuredContent: output,
          isError: true,
        };
      }
    },
  );
}