Skip to main content
Glama

Discord Agent MCP

by aj-geddes
scheduled-events.ts22.8 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, GuildScheduledEventPrivacyLevel, GuildScheduledEventEntityType, GuildScheduledEventStatus } from "discord.js"; export function registerScheduledEventTools( server: McpServer, discordManager: DiscordClientManager, logger: Logger, ) { // List Scheduled Events Tool server.registerTool( "list_scheduled_events", { title: "List Scheduled Events", description: "Get all scheduled events for a guild", inputSchema: { guildId: z.string().describe("Guild ID"), withUserCount: z .boolean() .optional() .describe("Include interested user count (default: false)"), }, outputSchema: { success: z.boolean(), events: z .array( z.object({ id: z.string(), name: z.string(), description: z.string().nullable(), scheduledStartTime: z.string(), scheduledEndTime: z.string().nullable(), status: z.string(), entityType: z.string(), channelId: z.string().nullable(), creatorId: z.string().nullable(), userCount: z.number().optional(), image: z.string().nullable(), }), ) .optional(), count: z.number().optional(), error: z.string().optional(), }, }, async ({ guildId, withUserCount }) => { try { const client = discordManager.getClient(); const guild = await validateGuildAccess(client, guildId); // Fetch all scheduled events const events = await guild.scheduledEvents.fetch({ withUserCount }); const eventList = events.map((event) => ({ id: event.id, name: event.name, description: event.description || null, scheduledStartTime: event.scheduledStartAt?.toISOString() || "", scheduledEndTime: event.scheduledEndAt?.toISOString() || null, status: GuildScheduledEventStatus[event.status], entityType: GuildScheduledEventEntityType[event.entityType], channelId: event.channelId || null, creatorId: event.creatorId || null, userCount: event.userCount || undefined, image: event.coverImageURL() || null, })); const output = { success: true, events: eventList, count: eventList.length, }; logger.info("Listed scheduled events", { guildId, count: eventList.length, }); return { content: [ { type: "text" as const, text: `Found ${eventList.length} scheduled events in ${guild.name}`, }, ], structuredContent: output, }; } catch (error: any) { logger.error("Failed to list scheduled events", { error: error.message, guildId, }); return { content: [ { type: "text" as const, text: `Error: ${error.message}`, }, ], structuredContent: { success: false, error: error.message, }, }; } }, ); // Get Event Details Tool server.registerTool( "get_event_details", { title: "Get Event Details", description: "Get detailed information about a specific scheduled event", inputSchema: { guildId: z.string().describe("Guild ID"), eventId: z.string().describe("Event ID"), withUserCount: z .boolean() .optional() .describe("Include interested user count (default: true)"), }, outputSchema: { success: z.boolean(), event: z .object({ id: z.string(), name: z.string(), description: z.string().nullable(), scheduledStartTime: z.string(), scheduledEndTime: z.string().nullable(), privacyLevel: z.string(), status: z.string(), entityType: z.string(), channelId: z.string().nullable(), entityMetadata: z.object({ location: z.string().optional(), }).nullable(), creatorId: z.string().nullable(), userCount: z.number().optional(), image: z.string().nullable(), }) .optional(), error: z.string().optional(), }, }, async ({ guildId, eventId, withUserCount = true }) => { try { const client = discordManager.getClient(); const guild = await validateGuildAccess(client, guildId); // Fetch event const event = await guild.scheduledEvents.fetch({ guildScheduledEvent: eventId, withUserCount, }); if (!event) { throw new InvalidInputError("eventId", "Event not found"); } const output = { success: true, event: { id: event.id, name: event.name, description: event.description || null, scheduledStartTime: event.scheduledStartAt?.toISOString() || "", scheduledEndTime: event.scheduledEndAt?.toISOString() || null, privacyLevel: GuildScheduledEventPrivacyLevel[event.privacyLevel], status: GuildScheduledEventStatus[event.status], entityType: GuildScheduledEventEntityType[event.entityType], channelId: event.channelId || null, entityMetadata: event.entityMetadata ? { location: event.entityMetadata.location || undefined, } : null, creatorId: event.creatorId || null, userCount: event.userCount || undefined, image: event.coverImageURL() || null, }, }; logger.info("Got event details", { guildId, eventId, }); return { content: [ { type: "text" as const, text: `Event: ${event.name}\nStatus: ${GuildScheduledEventStatus[event.status]}\nStarts: ${event.scheduledStartAt?.toISOString()}`, }, ], structuredContent: output, }; } catch (error: any) { logger.error("Failed to get event details", { error: error.message, guildId, eventId, }); return { content: [ { type: "text" as const, text: `Error: ${error.message}`, }, ], structuredContent: { success: false, error: error.message, }, }; } }, ); // Create Scheduled Event Tool server.registerTool( "create_scheduled_event", { title: "Create Scheduled Event", description: "Create a new scheduled event (stage, voice, or external)", inputSchema: { guildId: z.string().describe("Guild ID"), name: z .string() .min(1) .max(100) .describe("Event name (1-100 characters)"), description: z .string() .min(1) .max(1000) .optional() .describe("Event description (1-1000 characters)"), scheduledStartTime: z .string() .describe("ISO 8601 timestamp for event start"), scheduledEndTime: z .string() .optional() .describe("ISO 8601 timestamp for event end (required for EXTERNAL)"), entityType: z .enum(["STAGE_INSTANCE", "VOICE", "EXTERNAL"]) .describe("Event type: STAGE_INSTANCE, VOICE, or EXTERNAL"), channelId: z .string() .optional() .describe("Voice or stage channel ID (required for STAGE/VOICE)"), location: z .string() .optional() .describe("External location URL or address (required for EXTERNAL)"), image: z .string() .optional() .describe("Base64 encoded cover image"), reason: z.string().optional().describe("Audit log reason"), }, outputSchema: { success: z.boolean(), event: z .object({ id: z.string(), name: z.string(), scheduledStartTime: z.string(), entityType: z.string(), url: z.string(), }) .optional(), error: z.string().optional(), }, }, async ({ guildId, name, description, scheduledStartTime, scheduledEndTime, entityType, channelId, location, image, 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.ManageEvents)) { throw new PermissionDeniedError("ManageEvents", guildId); } // Map entity type string to enum const entityTypeMap: Record<string, GuildScheduledEventEntityType> = { STAGE_INSTANCE: GuildScheduledEventEntityType.StageInstance, VOICE: GuildScheduledEventEntityType.Voice, EXTERNAL: GuildScheduledEventEntityType.External, }; const mappedEntityType = entityTypeMap[entityType]; // Validate required fields based on entity type if ( (entityType === "STAGE_INSTANCE" || entityType === "VOICE") && !channelId ) { throw new InvalidInputError( "channelId", "Required for STAGE_INSTANCE and VOICE events", ); } if (entityType === "EXTERNAL" && !location) { throw new InvalidInputError( "location", "Required for EXTERNAL events", ); } if (entityType === "EXTERNAL" && !scheduledEndTime) { throw new InvalidInputError( "scheduledEndTime", "Required for EXTERNAL events", ); } // Create event const event = await guild.scheduledEvents.create({ name, description, scheduledStartTime: new Date(scheduledStartTime), scheduledEndTime: scheduledEndTime ? new Date(scheduledEndTime) : undefined, privacyLevel: GuildScheduledEventPrivacyLevel.GuildOnly, entityType: mappedEntityType, channel: channelId || undefined, entityMetadata: location ? { location } : undefined, image: image || undefined, reason, }); const output = { success: true, event: { id: event.id, name: event.name, scheduledStartTime: event.scheduledStartAt?.toISOString() || "", entityType: GuildScheduledEventEntityType[event.entityType], url: event.url, }, }; logger.info("Created scheduled event", { guildId, eventId: event.id, name, }); return { content: [ { type: "text" as const, text: `Created event "${event.name}" in ${guild.name}\nURL: ${event.url}`, }, ], structuredContent: output, }; } catch (error: any) { logger.error("Failed to create scheduled event", { error: error.message, guildId, name, }); return { content: [ { type: "text" as const, text: `Error: ${error.message}`, }, ], structuredContent: { success: false, error: error.message, }, }; } }, ); // Modify Scheduled Event Tool server.registerTool( "modify_scheduled_event", { title: "Modify Scheduled Event", description: "Update an existing scheduled event", inputSchema: { guildId: z.string().describe("Guild ID"), eventId: z.string().describe("Event ID"), name: z .string() .min(1) .max(100) .optional() .describe("New event name"), description: z .string() .min(1) .max(1000) .optional() .describe("New description"), scheduledStartTime: z .string() .optional() .describe("New start time (ISO 8601)"), scheduledEndTime: z .string() .optional() .describe("New end time (ISO 8601)"), entityType: z .enum(["STAGE_INSTANCE", "VOICE", "EXTERNAL"]) .optional() .describe("New entity type"), channelId: z.string().optional().describe("New channel ID"), location: z.string().optional().describe("New external location"), status: z .enum(["ACTIVE", "COMPLETED", "CANCELED"]) .optional() .describe("New event status"), image: z.string().optional().describe("New cover image (base64)"), reason: z.string().optional().describe("Audit log reason"), }, outputSchema: { success: z.boolean(), event: z .object({ id: z.string(), name: z.string(), status: z.string(), }) .optional(), error: z.string().optional(), }, }, async ({ guildId, eventId, name, description, scheduledStartTime, scheduledEndTime, entityType, channelId, location, status, image, 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.ManageEvents)) { throw new PermissionDeniedError("ManageEvents", guildId); } // Fetch event const event = await guild.scheduledEvents.fetch(eventId); if (!event) { throw new InvalidInputError("eventId", "Event not found"); } // Map entity type and status if provided const entityTypeMap: Record<string, GuildScheduledEventEntityType> = { STAGE_INSTANCE: GuildScheduledEventEntityType.StageInstance, VOICE: GuildScheduledEventEntityType.Voice, EXTERNAL: GuildScheduledEventEntityType.External, }; const statusMap: Record< string, | GuildScheduledEventStatus.Active | GuildScheduledEventStatus.Completed | GuildScheduledEventStatus.Canceled > = { ACTIVE: GuildScheduledEventStatus.Active, COMPLETED: GuildScheduledEventStatus.Completed, CANCELED: GuildScheduledEventStatus.Canceled, }; // Update event const updated = await event.edit({ name, description, scheduledStartTime: scheduledStartTime ? new Date(scheduledStartTime) : undefined, scheduledEndTime: scheduledEndTime ? new Date(scheduledEndTime) : undefined, entityType: entityType ? entityTypeMap[entityType] : undefined, channel: channelId || undefined, entityMetadata: location ? { location } : undefined, status: status ? statusMap[status] : undefined, image: image || undefined, reason, }); const output = { success: true, event: { id: updated.id, name: updated.name, status: GuildScheduledEventStatus[updated.status], }, }; logger.info("Modified scheduled event", { guildId, eventId, }); return { content: [ { type: "text" as const, text: `Updated event "${updated.name}" in ${guild.name}`, }, ], structuredContent: output, }; } catch (error: any) { logger.error("Failed to modify scheduled event", { error: error.message, guildId, eventId, }); return { content: [ { type: "text" as const, text: `Error: ${error.message}`, }, ], structuredContent: { success: false, error: error.message, }, }; } }, ); // Delete Scheduled Event Tool server.registerTool( "delete_scheduled_event", { title: "Delete Scheduled Event", description: "Delete (cancel) a scheduled event", inputSchema: { guildId: z.string().describe("Guild ID"), eventId: z.string().describe("Event ID to delete"), }, outputSchema: { success: z.boolean(), eventId: z.string().optional(), error: z.string().optional(), }, }, async ({ guildId, eventId }) => { 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.ManageEvents)) { throw new PermissionDeniedError("ManageEvents", guildId); } // Fetch and delete event const event = await guild.scheduledEvents.fetch(eventId); if (!event) { throw new InvalidInputError("eventId", "Event not found"); } const eventName = event.name; await event.delete(); const output = { success: true, eventId: eventId, }; logger.info("Deleted scheduled event", { guildId, eventId, }); return { content: [ { type: "text" as const, text: `Deleted event "${eventName}" from ${guild.name}`, }, ], structuredContent: output, }; } catch (error: any) { logger.error("Failed to delete scheduled event", { error: error.message, guildId, eventId, }); return { content: [ { type: "text" as const, text: `Error: ${error.message}`, }, ], structuredContent: { success: false, error: error.message, }, }; } }, ); // Get Event Users Tool server.registerTool( "get_event_users", { title: "Get Event Users", description: "Get users interested in a scheduled event", inputSchema: { guildId: z.string().describe("Guild ID"), eventId: z.string().describe("Event ID"), limit: z .number() .int() .min(1) .max(100) .optional() .describe("Max users to return (1-100, default: 100)"), withMember: z .boolean() .optional() .describe("Include guild member data (default: false)"), before: z .string() .optional() .describe("User ID to get users before"), after: z.string().optional().describe("User ID to get users after"), }, outputSchema: { success: z.boolean(), users: z .array( z.object({ userId: z.string(), username: z.string(), discriminator: z.string(), guildMember: z .object({ nickname: z.string().nullable(), roles: z.array(z.string()), joinedAt: z.string().nullable(), }) .optional(), }), ) .optional(), count: z.number().optional(), error: z.string().optional(), }, }, async ({ guildId, eventId, limit = 100, withMember, before, after }) => { try { const client = discordManager.getClient(); const guild = await validateGuildAccess(client, guildId); // Fetch event const event = await guild.scheduledEvents.fetch(eventId); if (!event) { throw new InvalidInputError("eventId", "Event not found"); } // Fetch interested users const users = await event.fetchSubscribers({ limit, withMember, before, after, }); const userList = users.map((subscriber) => { const member = subscriber.member; return { userId: subscriber.user.id, username: subscriber.user.username, discriminator: subscriber.user.discriminator, guildMember: member && withMember ? { nickname: (member as any).nickname || null, roles: (member as any).roles?.cache?.map((r: any) => r.id) || [], joinedAt: (member as any).joinedAt?.toISOString() || null, } : undefined, }; }); const output = { success: true, users: userList, count: userList.length, }; logger.info("Got event users", { guildId, eventId, count: userList.length, }); return { content: [ { type: "text" as const, text: `Found ${userList.length} users interested in event "${event.name}"`, }, ], structuredContent: output, }; } catch (error: any) { logger.error("Failed to get event users", { error: error.message, guildId, eventId, }); return { content: [ { type: "text" as const, text: `Error: ${error.message}`, }, ], structuredContent: { success: false, error: error.message, }, }; } }, ); }

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