Skip to main content
Glama
application-commands.ts22.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 { InvalidInputError } from "../errors/discord.js"; import { validateGuildAccess } from "../utils/guild-validation.js"; import { ApplicationCommandType, ApplicationCommandOptionType, } from "discord.js"; export function registerApplicationCommandTools( server: McpServer, discordManager: DiscordClientManager, logger: Logger, ) { // List Application Commands Tool server.registerTool( "list_application_commands", { title: "List Application Commands", description: "Get all application commands (slash commands) for a guild or globally", inputSchema: { guildId: z .string() .optional() .describe("Guild ID (omit for global commands)"), }, outputSchema: { success: z.boolean(), commands: z .array( z.object({ id: z.string(), name: z.string(), description: z.string(), type: z.string(), defaultMemberPermissions: z.string().nullable().optional(), }), ) .optional(), count: z.number().optional(), scope: z.string().optional(), error: z.string().optional(), }, }, async ({ guildId }) => { try { const client = discordManager.getClient(); let commands; let scope; if (guildId) { // Guild-specific commands const guild = await validateGuildAccess(client, guildId); commands = await guild.commands.fetch(); scope = `guild:${guild.name}`; } else { // Global commands commands = await client.application!.commands.fetch(); scope = "global"; } const commandList = commands.map((cmd) => ({ id: cmd.id, name: cmd.name, description: cmd.description, type: ApplicationCommandType[cmd.type], defaultMemberPermissions: cmd.defaultMemberPermissions?.toString() || null, })); const output = { success: true, commands: commandList, count: commandList.length, scope, }; logger.info("Listed application commands", { guildId: guildId || "global", count: commandList.length, }); return { content: [ { type: "text" as const, text: `Found ${commandList.length} application commands (${scope})`, }, ], structuredContent: output, }; } catch (error: any) { logger.error("Failed to list application commands", { error: error.message, guildId, }); return { content: [ { type: "text" as const, text: `Error: ${error.message}`, }, ], structuredContent: { success: false, error: error.message, }, }; } }, ); // Get Application Command Details Tool server.registerTool( "get_application_command", { title: "Get Application Command Details", description: "Get detailed information about a specific application command", inputSchema: { commandId: z.string().describe("Application command ID"), guildId: z .string() .optional() .describe("Guild ID (omit for global commands)"), }, outputSchema: { success: z.boolean(), command: z .object({ id: z.string(), name: z.string(), description: z.string(), type: z.string(), options: z.array(z.any()).optional(), defaultMemberPermissions: z.string().nullable().optional(), dmPermission: z.boolean().optional(), nsfw: z.boolean().optional(), }) .optional(), error: z.string().optional(), }, }, async ({ commandId, guildId }) => { try { const client = discordManager.getClient(); let command; if (guildId) { const guild = await validateGuildAccess(client, guildId); command = await guild.commands.fetch(commandId); } else { command = await client.application!.commands.fetch(commandId); } if (!command) { throw new InvalidInputError("commandId", "Command not found"); } const commandDetails = { id: command.id, name: command.name, description: command.description, type: ApplicationCommandType[command.type], options: command.options.map((opt) => ({ type: ApplicationCommandOptionType[opt.type], name: opt.name, description: opt.description, required: (opt as any).required || false, choices: (opt as any).choices || [], })), defaultMemberPermissions: command.defaultMemberPermissions?.toString() || null, dmPermission: command.dmPermission, nsfw: command.nsfw, }; const output = { success: true, command: commandDetails, }; logger.info("Fetched application command details", { commandId, guildId, }); return { content: [ { type: "text" as const, text: `Application command "${command.name}" details retrieved`, }, ], structuredContent: output, }; } catch (error: any) { logger.error("Failed to get application command", { error: error.message, commandId, guildId, }); return { content: [ { type: "text" as const, text: `Error: ${error.message}`, }, ], structuredContent: { success: false, error: error.message, }, }; } }, ); // Create Application Command Tool server.registerTool( "create_application_command", { title: "Create Application Command", description: "Create a new slash command, user context menu, or message context menu command", inputSchema: { name: z .string() .min(1) .max(32) .regex(/^[\w-]{1,32}$/) .describe("Command name (lowercase, alphanumeric, hyphens)"), description: z .string() .min(1) .max(100) .describe("Command description"), type: z .enum(["CHAT_INPUT", "USER", "MESSAGE"]) .optional() .describe("Command type (default: CHAT_INPUT)"), options: z .array( z.object({ type: z.enum([ "SUB_COMMAND", "SUB_COMMAND_GROUP", "STRING", "INTEGER", "BOOLEAN", "USER", "CHANNEL", "ROLE", "MENTIONABLE", "NUMBER", "ATTACHMENT", ]), name: z.string(), description: z.string(), required: z.boolean().optional(), choices: z .array( z.object({ name: z.string(), value: z.union([z.string(), z.number()]), }), ) .optional(), }), ) .optional() .describe("Command options (for CHAT_INPUT type)"), guildId: z .string() .optional() .describe("Guild ID (omit for global command)"), defaultMemberPermissions: z .string() .optional() .describe("Default required permissions (permission bit string)"), dmPermission: z .boolean() .optional() .describe("Enable in DMs (default: true)"), nsfw: z.boolean().optional().describe("Mark command as NSFW"), }, outputSchema: { success: z.boolean(), command: z .object({ id: z.string(), name: z.string(), type: z.string(), }) .optional(), error: z.string().optional(), }, }, async ({ name, description, type = "CHAT_INPUT", options, guildId, defaultMemberPermissions, dmPermission, nsfw, }) => { try { const client = discordManager.getClient(); const typeMap: Record<string, ApplicationCommandType> = { CHAT_INPUT: ApplicationCommandType.ChatInput, USER: ApplicationCommandType.User, MESSAGE: ApplicationCommandType.Message, }; const optionTypeMap: Record<string, ApplicationCommandOptionType> = { SUB_COMMAND: ApplicationCommandOptionType.Subcommand, SUB_COMMAND_GROUP: ApplicationCommandOptionType.SubcommandGroup, STRING: ApplicationCommandOptionType.String, INTEGER: ApplicationCommandOptionType.Integer, BOOLEAN: ApplicationCommandOptionType.Boolean, USER: ApplicationCommandOptionType.User, CHANNEL: ApplicationCommandOptionType.Channel, ROLE: ApplicationCommandOptionType.Role, MENTIONABLE: ApplicationCommandOptionType.Mentionable, NUMBER: ApplicationCommandOptionType.Number, ATTACHMENT: ApplicationCommandOptionType.Attachment, }; const commandData: any = { name, description: type === "CHAT_INPUT" ? description : "", type: typeMap[type], }; if (type === "CHAT_INPUT" && options) { commandData.options = options.map((opt) => ({ type: optionTypeMap[opt.type], name: opt.name, description: opt.description, required: opt.required, choices: opt.choices, })); } if (defaultMemberPermissions !== undefined) { commandData.defaultMemberPermissions = defaultMemberPermissions; } if (dmPermission !== undefined) { commandData.dmPermission = dmPermission; } if (nsfw !== undefined) { commandData.nsfw = nsfw; } let command; let scope; if (guildId) { const guild = await validateGuildAccess(client, guildId); command = await guild.commands.create(commandData); scope = `guild:${guild.name}`; } else { command = await client.application!.commands.create(commandData); scope = "global"; } const output = { success: true, command: { id: command.id, name: command.name, type: ApplicationCommandType[command.type], }, }; logger.info("Created application command", { commandId: command.id, name, guildId: guildId || "global", }); return { content: [ { type: "text" as const, text: `Created application command "/${name}" (${scope})`, }, ], structuredContent: output, }; } catch (error: any) { logger.error("Failed to create application command", { error: error.message, name, guildId, }); return { content: [ { type: "text" as const, text: `Error: ${error.message}`, }, ], structuredContent: { success: false, error: error.message, }, }; } }, ); // Modify Application Command Tool server.registerTool( "modify_application_command", { title: "Modify Application Command", description: "Update an existing application command's properties", inputSchema: { commandId: z.string().describe("Application command ID"), name: z .string() .min(1) .max(32) .regex(/^[\w-]{1,32}$/) .optional() .describe("New command name"), description: z .string() .min(1) .max(100) .optional() .describe("New description"), options: z .array( z.object({ type: z.enum([ "SUB_COMMAND", "SUB_COMMAND_GROUP", "STRING", "INTEGER", "BOOLEAN", "USER", "CHANNEL", "ROLE", "MENTIONABLE", "NUMBER", "ATTACHMENT", ]), name: z.string(), description: z.string(), required: z.boolean().optional(), choices: z .array( z.object({ name: z.string(), value: z.union([z.string(), z.number()]), }), ) .optional(), }), ) .optional() .describe("New options (replaces existing)"), guildId: z .string() .optional() .describe("Guild ID (omit for global commands)"), defaultMemberPermissions: z .string() .optional() .describe("New default permissions"), dmPermission: z.boolean().optional().describe("New DM permission"), nsfw: z.boolean().optional().describe("New NSFW flag"), }, outputSchema: { success: z.boolean(), command: z .object({ id: z.string(), name: z.string(), }) .optional(), error: z.string().optional(), }, }, async ({ commandId, name, description, options, guildId, defaultMemberPermissions, dmPermission, nsfw, }) => { try { const client = discordManager.getClient(); let command; if (guildId) { const guild = await validateGuildAccess(client, guildId); command = await guild.commands.fetch(commandId); } else { command = await client.application!.commands.fetch(commandId); } if (!command) { throw new InvalidInputError("commandId", "Command not found"); } const optionTypeMap: Record<string, ApplicationCommandOptionType> = { SUB_COMMAND: ApplicationCommandOptionType.Subcommand, SUB_COMMAND_GROUP: ApplicationCommandOptionType.SubcommandGroup, STRING: ApplicationCommandOptionType.String, INTEGER: ApplicationCommandOptionType.Integer, BOOLEAN: ApplicationCommandOptionType.Boolean, USER: ApplicationCommandOptionType.User, CHANNEL: ApplicationCommandOptionType.Channel, ROLE: ApplicationCommandOptionType.Role, MENTIONABLE: ApplicationCommandOptionType.Mentionable, NUMBER: ApplicationCommandOptionType.Number, ATTACHMENT: ApplicationCommandOptionType.Attachment, }; const updates: any = {}; if (name !== undefined) updates.name = name; if (description !== undefined) updates.description = description; if (options !== undefined) { updates.options = options.map((opt) => ({ type: optionTypeMap[opt.type], name: opt.name, description: opt.description, required: opt.required, choices: opt.choices, })); } if (defaultMemberPermissions !== undefined) { updates.defaultMemberPermissions = defaultMemberPermissions; } if (dmPermission !== undefined) updates.dmPermission = dmPermission; if (nsfw !== undefined) updates.nsfw = nsfw; const updated = await command.edit(updates); const output = { success: true, command: { id: updated.id, name: updated.name, }, }; logger.info("Modified application command", { commandId, guildId: guildId || "global", }); return { content: [ { type: "text" as const, text: `Updated application command "/${updated.name}"`, }, ], structuredContent: output, }; } catch (error: any) { logger.error("Failed to modify application command", { error: error.message, commandId, guildId, }); return { content: [ { type: "text" as const, text: `Error: ${error.message}`, }, ], structuredContent: { success: false, error: error.message, }, }; } }, ); // Delete Application Command Tool server.registerTool( "delete_application_command", { title: "Delete Application Command", description: "Delete an application command from the guild or globally", inputSchema: { commandId: z.string().describe("Application command ID to delete"), guildId: z .string() .optional() .describe("Guild ID (omit for global commands)"), }, outputSchema: { success: z.boolean(), commandId: z.string().optional(), error: z.string().optional(), }, }, async ({ commandId, guildId }) => { try { const client = discordManager.getClient(); let command; let scope; if (guildId) { const guild = await validateGuildAccess(client, guildId); command = await guild.commands.fetch(commandId); scope = `guild:${guild.name}`; } else { command = await client.application!.commands.fetch(commandId); scope = "global"; } if (!command) { throw new InvalidInputError("commandId", "Command not found"); } const commandName = command.name; await command.delete(); const output = { success: true, commandId: commandId, }; logger.info("Deleted application command", { commandId, guildId: guildId || "global", }); return { content: [ { type: "text" as const, text: `Deleted application command "/${commandName}" (${scope})`, }, ], structuredContent: output, }; } catch (error: any) { logger.error("Failed to delete application command", { error: error.message, commandId, guildId, }); return { content: [ { type: "text" as const, text: `Error: ${error.message}`, }, ], structuredContent: { success: false, error: error.message, }, }; } }, ); // Bulk Overwrite Application Commands Tool server.registerTool( "bulk_overwrite_commands", { title: "Bulk Overwrite Application Commands", description: "Replace all application commands with a new set (useful for syncing)", inputSchema: { commands: z .array( z.object({ name: z.string(), description: z.string(), type: z .enum(["CHAT_INPUT", "USER", "MESSAGE"]) .optional(), options: z.array(z.any()).optional(), }), ) .describe("Array of commands to set (replaces all existing)"), guildId: z .string() .optional() .describe("Guild ID (omit for global commands)"), }, outputSchema: { success: z.boolean(), count: z.number().optional(), scope: z.string().optional(), error: z.string().optional(), }, }, async ({ commands, guildId }) => { try { const client = discordManager.getClient(); const typeMap: Record<string, ApplicationCommandType> = { CHAT_INPUT: ApplicationCommandType.ChatInput, USER: ApplicationCommandType.User, MESSAGE: ApplicationCommandType.Message, }; const commandData = commands.map((cmd) => ({ name: cmd.name, description: cmd.description || "", type: cmd.type ? typeMap[cmd.type] : ApplicationCommandType.ChatInput, options: cmd.options || [], })); let result; let scope; if (guildId) { const guild = await validateGuildAccess(client, guildId); result = await guild.commands.set(commandData); scope = `guild:${guild.name}`; } else { result = await client.application!.commands.set(commandData); scope = "global"; } const output = { success: true, count: result.size, scope, }; logger.info("Bulk overwrote application commands", { guildId: guildId || "global", count: result.size, }); return { content: [ { type: "text" as const, text: `Synchronized ${result.size} application commands (${scope})`, }, ], structuredContent: output, }; } catch (error: any) { logger.error("Failed to bulk overwrite commands", { error: error.message, guildId, }); return { content: [ { type: "text" as const, text: `Error: ${error.message}`, }, ], structuredContent: { success: false, error: error.message, }, }; } }, ); }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/aj-geddes/discord-agent-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server