@mahesvara/discord-mcpserver 1.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,2874 @@
1
+ #!/usr/bin/env node
2
+ import "dotenv/config";
3
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
6
+ import express from "express";
7
+ import { ChannelType, GuildChannel, PermissionsBitField, OverwriteType, GuildScheduledEventEntityType, GuildScheduledEventPrivacyLevel } from "discord.js";
8
+ import { getClient, formatGuild, formatChannel, formatMember, formatRole, formatMessage, formatResponse, getChannelType, guildToMarkdown, channelToMarkdown, memberToMarkdown, roleToMarkdown, messageToMarkdown, truncateIfNeeded, } from "./services/discord.js";
9
+ import { ListGuildsSchema, GetGuildSchema, LeaveGuildSchema, ListChannelsSchema, GetChannelSchema, SendMessageSchema, GetMessagesSchema, DeleteMessageSchema, EditMessageSchema, ListMembersSchema, GetMemberSchema, ListRolesSchema, AddRoleSchema, RemoveRoleSchema, CreateChannelSchema, DeleteChannelSchema, EditChannelSchema, SetChannelPermissionsSchema, RemoveChannelPermissionsSchema, GetChannelPermissionsSchema, SyncChannelPermissionsSchema, MoveMemberSchema, KickMemberSchema, BanMemberSchema, UnbanMemberSchema, CreateRoleSchema, DeleteRoleSchema, EditRoleSchema, SetRolePositionsSchema, SetNicknameSchema, AddReactionSchema, RemoveReactionSchema, PinMessageSchema, UnpinMessageSchema, GetPinnedMessagesSchema, EditGuildSchema, TimeoutMemberSchema, ListEmojisSchema, CreateEmojiSchema, DeleteEmojiSchema, ListInvitesSchema, CreateInviteSchema, DeleteInviteSchema, ListWebhooksSchema, CreateWebhookSchema, DeleteWebhookSchema, EditWebhookSchema, GetAuditLogSchema, PruneMembersSchema, ListStickersSchema, DeleteStickerSchema, ListEventsSchema, CreateEventSchema, DeleteEventSchema, ListBansSchema, } from "./schemas/index.js";
10
+ // Initialize MCP Server
11
+ const server = new McpServer({
12
+ name: "discord-mcp-server",
13
+ version: "1.0.0",
14
+ });
15
+ // ============================================================================
16
+ // GUILD TOOLS
17
+ // ============================================================================
18
+ server.registerTool("discord_list_guilds", {
19
+ title: "List Discord Servers",
20
+ description: `List all Discord servers (guilds) the bot has access to.
21
+
22
+ Returns server names, IDs, member counts, and owner information.
23
+
24
+ Args:
25
+ - response_format ('markdown' | 'json'): Output format (default: 'json')
26
+
27
+ Returns:
28
+ List of guilds with id, name, memberCount, icon, ownerId`,
29
+ inputSchema: ListGuildsSchema,
30
+ annotations: {
31
+ readOnlyHint: true,
32
+ destructiveHint: false,
33
+ idempotentHint: true,
34
+ openWorldHint: true,
35
+ },
36
+ }, async (params) => {
37
+ try {
38
+ const client = await getClient();
39
+ const guilds = client.guilds.cache.map(formatGuild);
40
+ const text = formatResponse(guilds, params.response_format, (items) => items.map(guildToMarkdown).join("\n\n"));
41
+ return {
42
+ content: [{ type: "text", text: truncateIfNeeded(text) }],
43
+ };
44
+ }
45
+ catch (error) {
46
+ return {
47
+ isError: true,
48
+ content: [{ type: "text", text: `Error listing guilds: ${error.message}` }],
49
+ };
50
+ }
51
+ });
52
+ server.registerTool("discord_get_guild", {
53
+ title: "Get Discord Server Info",
54
+ description: `Get detailed information about a specific Discord server.
55
+
56
+ Args:
57
+ - guild_id (string): Discord server/guild ID
58
+ - response_format ('markdown' | 'json'): Output format (default: 'json')
59
+
60
+ Returns:
61
+ Server details including name, member count, owner, and icon`,
62
+ inputSchema: GetGuildSchema,
63
+ annotations: {
64
+ readOnlyHint: true,
65
+ destructiveHint: false,
66
+ idempotentHint: true,
67
+ openWorldHint: true,
68
+ },
69
+ }, async (params) => {
70
+ try {
71
+ const client = await getClient();
72
+ const guild = client.guilds.cache.get(params.guild_id);
73
+ if (!guild) {
74
+ return {
75
+ isError: true,
76
+ content: [{ type: "text", text: `Guild not found: ${params.guild_id}. Use discord_list_guilds to see available servers.` }],
77
+ };
78
+ }
79
+ const formatted = formatGuild(guild);
80
+ const text = formatResponse(formatted, params.response_format, guildToMarkdown);
81
+ return {
82
+ content: [{ type: "text", text }],
83
+ };
84
+ }
85
+ catch (error) {
86
+ return {
87
+ isError: true,
88
+ content: [{ type: "text", text: `Error getting guild: ${error.message}` }],
89
+ };
90
+ }
91
+ });
92
+ server.registerTool("discord_leave_guild", {
93
+ title: "Leave Discord Server",
94
+ description: `Leave a Discord server (guild). The bot will no longer have access to this server.
95
+
96
+ WARNING: This is a destructive action. The bot will need to be re-invited to rejoin.
97
+
98
+ Args:
99
+ - guild_id (string): Discord server/guild ID to leave
100
+ - confirm (boolean): Must be set to true to confirm leaving
101
+
102
+ Returns:
103
+ Confirmation message with server details`,
104
+ inputSchema: LeaveGuildSchema,
105
+ annotations: {
106
+ readOnlyHint: false,
107
+ destructiveHint: true,
108
+ idempotentHint: false,
109
+ openWorldHint: true,
110
+ },
111
+ }, async (params) => {
112
+ try {
113
+ if (!params.confirm) {
114
+ return {
115
+ isError: true,
116
+ content: [{ type: "text", text: "You must set 'confirm: true' to leave a server. This action cannot be undone - the bot will need to be re-invited to rejoin." }],
117
+ };
118
+ }
119
+ const client = await getClient();
120
+ const guild = client.guilds.cache.get(params.guild_id);
121
+ if (!guild) {
122
+ return {
123
+ isError: true,
124
+ content: [{ type: "text", text: `Guild not found: ${params.guild_id}. Use discord_list_guilds to see available servers.` }],
125
+ };
126
+ }
127
+ const guildName = guild.name;
128
+ const memberCount = guild.memberCount;
129
+ const ownerId = guild.ownerId;
130
+ await guild.leave();
131
+ return {
132
+ content: [{ type: "text", text: `Successfully left server "${guildName}" (ID: ${params.guild_id}, Members: ${memberCount}, Owner: ${ownerId}). The bot will need to be re-invited to rejoin.` }],
133
+ };
134
+ }
135
+ catch (error) {
136
+ return {
137
+ isError: true,
138
+ content: [{ type: "text", text: `Error leaving guild: ${error.message}` }],
139
+ };
140
+ }
141
+ });
142
+ // ============================================================================
143
+ // CHANNEL TOOLS
144
+ // ============================================================================
145
+ server.registerTool("discord_list_channels", {
146
+ title: "List Discord Channels",
147
+ description: `List channels in a Discord server.
148
+
149
+ Args:
150
+ - guild_id (string): Discord server/guild ID
151
+ - type ('text' | 'voice' | 'category' | 'all'): Filter by channel type (default: 'all')
152
+ - response_format ('markdown' | 'json'): Output format (default: 'json')
153
+
154
+ Returns:
155
+ List of channels with id, name, type, topic, and category info`,
156
+ inputSchema: ListChannelsSchema,
157
+ annotations: {
158
+ readOnlyHint: true,
159
+ destructiveHint: false,
160
+ idempotentHint: true,
161
+ openWorldHint: true,
162
+ },
163
+ }, async (params) => {
164
+ try {
165
+ const client = await getClient();
166
+ const guild = client.guilds.cache.get(params.guild_id);
167
+ if (!guild) {
168
+ return {
169
+ isError: true,
170
+ content: [{ type: "text", text: `Guild not found: ${params.guild_id}` }],
171
+ };
172
+ }
173
+ // Get all channels as array to avoid type predicate issues
174
+ const allChannels = Array.from(guild.channels.cache.values());
175
+ // Filter out threads and apply type filter
176
+ let filteredChannels = allChannels.filter(c => !c.isThread());
177
+ if (params.type !== "all") {
178
+ const targetType = getChannelType(params.type);
179
+ if (targetType !== null) {
180
+ filteredChannels = filteredChannels.filter(c => c.type === targetType);
181
+ }
182
+ }
183
+ const formatted = filteredChannels.map(c => formatChannel(c));
184
+ const text = formatResponse(formatted, params.response_format, (items) => items.map(channelToMarkdown).join("\n\n"));
185
+ return {
186
+ content: [{ type: "text", text: truncateIfNeeded(text) }],
187
+ };
188
+ }
189
+ catch (error) {
190
+ return {
191
+ isError: true,
192
+ content: [{ type: "text", text: `Error listing channels: ${error.message}` }],
193
+ };
194
+ }
195
+ });
196
+ server.registerTool("discord_get_channel", {
197
+ title: "Get Discord Channel Info",
198
+ description: `Get detailed information about a specific channel.
199
+
200
+ Args:
201
+ - channel_id (string): Discord channel ID
202
+ - response_format ('markdown' | 'json'): Output format (default: 'json')
203
+
204
+ Returns:
205
+ Channel details including name, type, topic, and category`,
206
+ inputSchema: GetChannelSchema,
207
+ annotations: {
208
+ readOnlyHint: true,
209
+ destructiveHint: false,
210
+ idempotentHint: true,
211
+ openWorldHint: true,
212
+ },
213
+ }, async (params) => {
214
+ try {
215
+ const client = await getClient();
216
+ const channel = client.channels.cache.get(params.channel_id);
217
+ if (!channel || !(channel instanceof GuildChannel)) {
218
+ return {
219
+ isError: true,
220
+ content: [{ type: "text", text: `Channel not found: ${params.channel_id}` }],
221
+ };
222
+ }
223
+ const formatted = formatChannel(channel);
224
+ const text = formatResponse(formatted, params.response_format, channelToMarkdown);
225
+ return {
226
+ content: [{ type: "text", text }],
227
+ };
228
+ }
229
+ catch (error) {
230
+ return {
231
+ isError: true,
232
+ content: [{ type: "text", text: `Error getting channel: ${error.message}` }],
233
+ };
234
+ }
235
+ });
236
+ server.registerTool("discord_create_channel", {
237
+ title: "Create Discord Channel",
238
+ description: `Create a new channel or category in a Discord server.
239
+
240
+ Args:
241
+ - guild_id (string): Discord server/guild ID
242
+ - name (string): Channel name
243
+ - type ('text' | 'voice' | 'category'): Channel type (default: 'text')
244
+ - topic (string, optional): Channel topic (text channels only)
245
+ - parent_id (string, optional): Category ID to create channel under (not for categories)
246
+ - nsfw (boolean, optional): Whether the channel is NSFW (text channels only)
247
+ - bitrate (number, optional): Bitrate for voice channels (8000-384000)
248
+ - user_limit (number, optional): User limit for voice channels (0 = unlimited)
249
+
250
+ Returns:
251
+ The created channel's details`,
252
+ inputSchema: CreateChannelSchema,
253
+ annotations: {
254
+ readOnlyHint: false,
255
+ destructiveHint: false,
256
+ idempotentHint: false,
257
+ openWorldHint: true,
258
+ },
259
+ }, async (params) => {
260
+ try {
261
+ const client = await getClient();
262
+ const guild = client.guilds.cache.get(params.guild_id);
263
+ if (!guild) {
264
+ return {
265
+ isError: true,
266
+ content: [{ type: "text", text: `Guild not found: ${params.guild_id}` }],
267
+ };
268
+ }
269
+ let channelType;
270
+ switch (params.type) {
271
+ case "voice":
272
+ channelType = ChannelType.GuildVoice;
273
+ break;
274
+ case "category":
275
+ channelType = ChannelType.GuildCategory;
276
+ break;
277
+ default:
278
+ channelType = ChannelType.GuildText;
279
+ }
280
+ const channelOptions = {
281
+ name: params.name,
282
+ type: channelType,
283
+ };
284
+ // Only add these for non-category channels
285
+ if (params.type !== "category") {
286
+ if (params.parent_id)
287
+ channelOptions.parent = params.parent_id;
288
+ }
289
+ // Text channel specific options
290
+ if (params.type === "text" || !params.type) {
291
+ if (params.topic)
292
+ channelOptions.topic = params.topic;
293
+ if (params.nsfw !== undefined)
294
+ channelOptions.nsfw = params.nsfw;
295
+ }
296
+ // Voice channel specific options
297
+ if (params.type === "voice") {
298
+ if (params.bitrate)
299
+ channelOptions.bitrate = params.bitrate;
300
+ if (params.user_limit !== undefined)
301
+ channelOptions.userLimit = params.user_limit;
302
+ }
303
+ const channel = await guild.channels.create(channelOptions);
304
+ const typeLabel = params.type === "category" ? "category" : "channel";
305
+ const prefix = params.type === "category" ? "📁" : "#";
306
+ return {
307
+ content: [{ type: "text", text: `Created ${typeLabel} ${prefix}${channel.name} (ID: ${channel.id})` }],
308
+ };
309
+ }
310
+ catch (error) {
311
+ return {
312
+ isError: true,
313
+ content: [{ type: "text", text: `Error creating channel: ${error.message}` }],
314
+ };
315
+ }
316
+ });
317
+ server.registerTool("discord_delete_channel", {
318
+ title: "Delete Discord Channel",
319
+ description: `Delete a channel from a Discord server. This action is permanent!
320
+
321
+ Args:
322
+ - channel_id (string): Discord channel ID to delete
323
+
324
+ Returns:
325
+ Confirmation of deletion`,
326
+ inputSchema: DeleteChannelSchema,
327
+ annotations: {
328
+ readOnlyHint: false,
329
+ destructiveHint: true,
330
+ idempotentHint: false,
331
+ openWorldHint: true,
332
+ },
333
+ }, async (params) => {
334
+ try {
335
+ const client = await getClient();
336
+ const channel = client.channels.cache.get(params.channel_id);
337
+ if (!channel || !('delete' in channel)) {
338
+ return {
339
+ isError: true,
340
+ content: [{ type: "text", text: `Channel not found: ${params.channel_id}` }],
341
+ };
342
+ }
343
+ const name = 'name' in channel ? channel.name : params.channel_id;
344
+ await channel.delete();
345
+ return {
346
+ content: [{ type: "text", text: `Deleted channel: ${name}` }],
347
+ };
348
+ }
349
+ catch (error) {
350
+ return {
351
+ isError: true,
352
+ content: [{ type: "text", text: `Error deleting channel: ${error.message}` }],
353
+ };
354
+ }
355
+ });
356
+ server.registerTool("discord_edit_channel", {
357
+ title: "Edit Discord Channel",
358
+ description: `Edit a channel's properties including name, topic, category, position, and more.
359
+
360
+ Args:
361
+ - channel_id (string): Discord channel ID
362
+ - name (string, optional): New channel name
363
+ - topic (string, optional): New channel topic
364
+ - parent_id (string, optional): Move to a different category (or 'none' to remove from category)
365
+ - position (number, optional): New position of the channel
366
+ - nsfw (boolean, optional): Whether the channel is NSFW
367
+ - bitrate (number, optional): Bitrate for voice channels (8000-384000)
368
+ - user_limit (number, optional): User limit for voice channels
369
+
370
+ Returns:
371
+ Updated channel details`,
372
+ inputSchema: EditChannelSchema,
373
+ annotations: {
374
+ readOnlyHint: false,
375
+ destructiveHint: false,
376
+ idempotentHint: true,
377
+ openWorldHint: true,
378
+ },
379
+ }, async (params) => {
380
+ try {
381
+ const client = await getClient();
382
+ const channel = client.channels.cache.get(params.channel_id);
383
+ if (!channel || !('edit' in channel)) {
384
+ return {
385
+ isError: true,
386
+ content: [{ type: "text", text: `Channel not found: ${params.channel_id}` }],
387
+ };
388
+ }
389
+ const updates = {};
390
+ if (params.name)
391
+ updates.name = params.name;
392
+ if (params.topic !== undefined)
393
+ updates.topic = params.topic;
394
+ if (params.position !== undefined)
395
+ updates.position = params.position;
396
+ if (params.nsfw !== undefined)
397
+ updates.nsfw = params.nsfw;
398
+ if (params.bitrate !== undefined)
399
+ updates.bitrate = params.bitrate;
400
+ if (params.user_limit !== undefined)
401
+ updates.userLimit = params.user_limit;
402
+ if (params.parent_id !== undefined) {
403
+ updates.parent = params.parent_id === 'none' ? null : params.parent_id;
404
+ }
405
+ await channel.edit(updates);
406
+ return {
407
+ content: [{ type: "text", text: `Updated channel ${params.channel_id}` }],
408
+ };
409
+ }
410
+ catch (error) {
411
+ return {
412
+ isError: true,
413
+ content: [{ type: "text", text: `Error editing channel: ${error.message}` }],
414
+ };
415
+ }
416
+ });
417
+ // ============================================================================
418
+ // PERMISSION TOOLS
419
+ // ============================================================================
420
+ // Helper function to convert permission strings to bitfield
421
+ function parsePermissions(permissions) {
422
+ let bits = BigInt(0);
423
+ for (const perm of permissions) {
424
+ const flag = PermissionsBitField.Flags[perm];
425
+ if (flag) {
426
+ bits |= flag;
427
+ }
428
+ }
429
+ return bits;
430
+ }
431
+ // List of common permission names for reference
432
+ const PERMISSION_LIST = [
433
+ 'ViewChannel', 'ManageChannels', 'ManageRoles', 'CreateInstantInvite',
434
+ 'SendMessages', 'SendMessagesInThreads', 'CreatePublicThreads', 'CreatePrivateThreads',
435
+ 'EmbedLinks', 'AttachFiles', 'AddReactions', 'UseExternalEmojis', 'UseExternalStickers',
436
+ 'MentionEveryone', 'ManageMessages', 'ManageThreads', 'ReadMessageHistory',
437
+ 'SendTTSMessages', 'UseApplicationCommands',
438
+ 'Connect', 'Speak', 'Stream', 'UseEmbeddedActivities', 'UseSoundboard',
439
+ 'UseExternalSounds', 'UseVAD', 'PrioritySpeaker', 'MuteMembers', 'DeafenMembers', 'MoveMembers',
440
+ 'ManageEvents', 'ManageWebhooks'
441
+ ];
442
+ server.registerTool("discord_set_channel_permissions", {
443
+ title: "Set Channel Permissions",
444
+ description: `Set permission overrides for a role or member on a channel.
445
+
446
+ Common permission names:
447
+ - View: ViewChannel
448
+ - Messages: SendMessages, ReadMessageHistory, ManageMessages, EmbedLinks, AttachFiles, AddReactions
449
+ - Voice: Connect, Speak, Stream, MuteMembers, DeafenMembers, MoveMembers
450
+ - Threads: CreatePublicThreads, CreatePrivateThreads, SendMessagesInThreads
451
+ - Management: ManageChannels, ManageRoles, ManageWebhooks
452
+
453
+ Args:
454
+ - channel_id (string): Discord channel ID
455
+ - target_id (string): Role or User ID to set permissions for
456
+ - target_type ('role' | 'member'): Whether target is a role or member
457
+ - allow (string[], optional): Permissions to allow (e.g., ['ViewChannel', 'SendMessages'])
458
+ - deny (string[], optional): Permissions to deny (e.g., ['SendMessages'])
459
+
460
+ Returns:
461
+ Confirmation of permission changes`,
462
+ inputSchema: SetChannelPermissionsSchema,
463
+ annotations: {
464
+ readOnlyHint: false,
465
+ destructiveHint: false,
466
+ idempotentHint: true,
467
+ openWorldHint: true,
468
+ },
469
+ }, async (params) => {
470
+ try {
471
+ const client = await getClient();
472
+ const channel = client.channels.cache.get(params.channel_id);
473
+ if (!channel || !('permissionOverwrites' in channel)) {
474
+ return {
475
+ isError: true,
476
+ content: [{ type: "text", text: `Channel not found or doesn't support permissions: ${params.channel_id}` }],
477
+ };
478
+ }
479
+ const guildChannel = channel;
480
+ // Build permission overwrite object with individual permission keys
481
+ const permissionOverwrite = {};
482
+ // Set allowed permissions to true
483
+ if (params.allow) {
484
+ for (const perm of params.allow) {
485
+ permissionOverwrite[perm] = true;
486
+ }
487
+ }
488
+ // Set denied permissions to false
489
+ if (params.deny) {
490
+ for (const perm of params.deny) {
491
+ permissionOverwrite[perm] = false;
492
+ }
493
+ }
494
+ await guildChannel.permissionOverwrites.edit(params.target_id, permissionOverwrite, {
495
+ type: params.target_type === 'role' ? OverwriteType.Role : OverwriteType.Member,
496
+ });
497
+ const allowList = params.allow?.join(', ') || 'none';
498
+ const denyList = params.deny?.join(', ') || 'none';
499
+ return {
500
+ content: [{ type: "text", text: `Updated permissions for ${params.target_type} ${params.target_id} on channel ${params.channel_id}\nAllowed: ${allowList}\nDenied: ${denyList}` }],
501
+ };
502
+ }
503
+ catch (error) {
504
+ return {
505
+ isError: true,
506
+ content: [{ type: "text", text: `Error setting permissions: ${error.message}` }],
507
+ };
508
+ }
509
+ });
510
+ server.registerTool("discord_remove_channel_permissions", {
511
+ title: "Remove Channel Permission Overrides",
512
+ description: `Remove all permission overrides for a role or member on a channel.
513
+
514
+ Args:
515
+ - channel_id (string): Discord channel ID
516
+ - target_id (string): Role or User ID to remove permission overrides for
517
+
518
+ Returns:
519
+ Confirmation of removal`,
520
+ inputSchema: RemoveChannelPermissionsSchema,
521
+ annotations: {
522
+ readOnlyHint: false,
523
+ destructiveHint: true,
524
+ idempotentHint: true,
525
+ openWorldHint: true,
526
+ },
527
+ }, async (params) => {
528
+ try {
529
+ const client = await getClient();
530
+ const channel = client.channels.cache.get(params.channel_id);
531
+ if (!channel || !('permissionOverwrites' in channel)) {
532
+ return {
533
+ isError: true,
534
+ content: [{ type: "text", text: `Channel not found or doesn't support permissions: ${params.channel_id}` }],
535
+ };
536
+ }
537
+ const guildChannel = channel;
538
+ await guildChannel.permissionOverwrites.delete(params.target_id);
539
+ return {
540
+ content: [{ type: "text", text: `Removed permission overrides for ${params.target_id} on channel ${params.channel_id}` }],
541
+ };
542
+ }
543
+ catch (error) {
544
+ return {
545
+ isError: true,
546
+ content: [{ type: "text", text: `Error removing permissions: ${error.message}` }],
547
+ };
548
+ }
549
+ });
550
+ server.registerTool("discord_get_channel_permissions", {
551
+ title: "Get Channel Permissions",
552
+ description: `Get all permission overrides for a channel.
553
+
554
+ Args:
555
+ - channel_id (string): Discord channel ID
556
+ - response_format ('markdown' | 'json'): Output format (default: 'json')
557
+
558
+ Returns:
559
+ List of permission overrides with allow/deny for each role/member`,
560
+ inputSchema: GetChannelPermissionsSchema,
561
+ annotations: {
562
+ readOnlyHint: true,
563
+ destructiveHint: false,
564
+ idempotentHint: true,
565
+ openWorldHint: true,
566
+ },
567
+ }, async (params) => {
568
+ try {
569
+ const client = await getClient();
570
+ const channel = client.channels.cache.get(params.channel_id);
571
+ if (!channel || !('permissionOverwrites' in channel)) {
572
+ return {
573
+ isError: true,
574
+ content: [{ type: "text", text: `Channel not found or doesn't support permissions: ${params.channel_id}` }],
575
+ };
576
+ }
577
+ const guildChannel = channel;
578
+ const overwrites = guildChannel.permissionOverwrites.cache;
579
+ const formatted = overwrites.map(ow => {
580
+ const allowedPerms = new PermissionsBitField(ow.allow).toArray();
581
+ const deniedPerms = new PermissionsBitField(ow.deny).toArray();
582
+ return {
583
+ id: ow.id,
584
+ type: ow.type === OverwriteType.Role ? 'role' : 'member',
585
+ allow: allowedPerms,
586
+ deny: deniedPerms,
587
+ };
588
+ });
589
+ if (params.response_format === 'json') {
590
+ return {
591
+ content: [{ type: "text", text: JSON.stringify(Array.from(formatted.values()), null, 2) }],
592
+ };
593
+ }
594
+ // Markdown format
595
+ const lines = Array.from(formatted.values()).map(ow => {
596
+ const allowStr = ow.allow.length > 0 ? ow.allow.join(', ') : 'none';
597
+ const denyStr = ow.deny.length > 0 ? ow.deny.join(', ') : 'none';
598
+ return `### ${ow.type}: ${ow.id}\n- **Allow**: ${allowStr}\n- **Deny**: ${denyStr}`;
599
+ });
600
+ const text = lines.length > 0 ? lines.join('\n\n') : 'No permission overrides on this channel.';
601
+ return {
602
+ content: [{ type: "text", text }],
603
+ };
604
+ }
605
+ catch (error) {
606
+ return {
607
+ isError: true,
608
+ content: [{ type: "text", text: `Error getting permissions: ${error.message}` }],
609
+ };
610
+ }
611
+ });
612
+ server.registerTool("discord_sync_channel_permissions", {
613
+ title: "Sync Channel Permissions with Category",
614
+ description: `Sync a channel's permissions with its parent category.
615
+
616
+ Args:
617
+ - channel_id (string): Discord channel ID
618
+
619
+ Returns:
620
+ Confirmation of sync`,
621
+ inputSchema: SyncChannelPermissionsSchema,
622
+ annotations: {
623
+ readOnlyHint: false,
624
+ destructiveHint: false,
625
+ idempotentHint: true,
626
+ openWorldHint: true,
627
+ },
628
+ }, async (params) => {
629
+ try {
630
+ const client = await getClient();
631
+ const channel = client.channels.cache.get(params.channel_id);
632
+ if (!channel || !('lockPermissions' in channel)) {
633
+ return {
634
+ isError: true,
635
+ content: [{ type: "text", text: `Channel not found or doesn't support permission sync: ${params.channel_id}` }],
636
+ };
637
+ }
638
+ await channel.lockPermissions();
639
+ return {
640
+ content: [{ type: "text", text: `Synced permissions for channel ${params.channel_id} with its parent category` }],
641
+ };
642
+ }
643
+ catch (error) {
644
+ return {
645
+ isError: true,
646
+ content: [{ type: "text", text: `Error syncing permissions: ${error.message}` }],
647
+ };
648
+ }
649
+ });
650
+ server.registerTool("discord_send_message", {
651
+ title: "Send Discord Message",
652
+ description: `Send a message to a Discord channel.
653
+
654
+ Args:
655
+ - channel_id (string): Discord channel ID
656
+ - content (string): Message content (max 2000 characters)
657
+ - reply_to (string, optional): Message ID to reply to
658
+
659
+ Returns:
660
+ The sent message details including ID`,
661
+ inputSchema: SendMessageSchema,
662
+ annotations: {
663
+ readOnlyHint: false,
664
+ destructiveHint: false,
665
+ idempotentHint: false,
666
+ openWorldHint: true,
667
+ },
668
+ }, async (params) => {
669
+ try {
670
+ const client = await getClient();
671
+ const channel = client.channels.cache.get(params.channel_id);
672
+ if (!channel || !('send' in channel)) {
673
+ return {
674
+ isError: true,
675
+ content: [{ type: "text", text: `Text channel not found: ${params.channel_id}` }],
676
+ };
677
+ }
678
+ const options = { content: params.content };
679
+ if (params.reply_to) {
680
+ options.reply = { messageReference: params.reply_to };
681
+ }
682
+ const message = await channel.send(options);
683
+ return {
684
+ content: [{ type: "text", text: `Message sent (ID: ${message.id})` }],
685
+ };
686
+ }
687
+ catch (error) {
688
+ return {
689
+ isError: true,
690
+ content: [{ type: "text", text: `Error sending message: ${error.message}` }],
691
+ };
692
+ }
693
+ });
694
+ server.registerTool("discord_get_messages", {
695
+ title: "Get Discord Messages",
696
+ description: `Retrieve messages from a Discord channel.
697
+
698
+ Args:
699
+ - channel_id (string): Discord channel ID
700
+ - limit (number): Number of messages to retrieve (1-100, default: 20)
701
+ - before (string, optional): Get messages before this message ID
702
+ - after (string, optional): Get messages after this message ID
703
+ - response_format ('markdown' | 'json'): Output format (default: 'json')
704
+
705
+ Returns:
706
+ List of messages with content, author, timestamp, attachments`,
707
+ inputSchema: GetMessagesSchema,
708
+ annotations: {
709
+ readOnlyHint: true,
710
+ destructiveHint: false,
711
+ idempotentHint: true,
712
+ openWorldHint: true,
713
+ },
714
+ }, async (params) => {
715
+ try {
716
+ const client = await getClient();
717
+ const channel = client.channels.cache.get(params.channel_id);
718
+ if (!channel || !('messages' in channel)) {
719
+ return {
720
+ isError: true,
721
+ content: [{ type: "text", text: `Text channel not found: ${params.channel_id}` }],
722
+ };
723
+ }
724
+ const options = { limit: params.limit };
725
+ if (params.before)
726
+ options.before = params.before;
727
+ if (params.after)
728
+ options.after = params.after;
729
+ const messages = await channel.messages.fetch(options);
730
+ const formatted = messages.map((m) => formatMessage(m));
731
+ const text = formatResponse(Array.from(formatted.values()), params.response_format, (items) => items.map(messageToMarkdown).join("\n\n---\n\n"));
732
+ return {
733
+ content: [{ type: "text", text: truncateIfNeeded(text) }],
734
+ };
735
+ }
736
+ catch (error) {
737
+ return {
738
+ isError: true,
739
+ content: [{ type: "text", text: `Error getting messages: ${error.message}` }],
740
+ };
741
+ }
742
+ });
743
+ server.registerTool("discord_delete_message", {
744
+ title: "Delete Discord Message",
745
+ description: `Delete a message from a Discord channel.
746
+
747
+ Args:
748
+ - channel_id (string): Discord channel ID
749
+ - message_id (string): Message ID to delete
750
+
751
+ Returns:
752
+ Confirmation of deletion`,
753
+ inputSchema: DeleteMessageSchema,
754
+ annotations: {
755
+ readOnlyHint: false,
756
+ destructiveHint: true,
757
+ idempotentHint: false,
758
+ openWorldHint: true,
759
+ },
760
+ }, async (params) => {
761
+ try {
762
+ const client = await getClient();
763
+ const channel = client.channels.cache.get(params.channel_id);
764
+ if (!channel || !('messages' in channel)) {
765
+ return {
766
+ isError: true,
767
+ content: [{ type: "text", text: `Text channel not found: ${params.channel_id}` }],
768
+ };
769
+ }
770
+ const message = await channel.messages.fetch(params.message_id);
771
+ await message.delete();
772
+ return {
773
+ content: [{ type: "text", text: `Deleted message ${params.message_id}` }],
774
+ };
775
+ }
776
+ catch (error) {
777
+ return {
778
+ isError: true,
779
+ content: [{ type: "text", text: `Error deleting message: ${error.message}` }],
780
+ };
781
+ }
782
+ });
783
+ server.registerTool("discord_edit_message", {
784
+ title: "Edit Discord Message",
785
+ description: `Edit a message sent by the bot.
786
+
787
+ Args:
788
+ - channel_id (string): Discord channel ID
789
+ - message_id (string): Message ID to edit
790
+ - content (string): New message content (max 2000 characters)
791
+
792
+ Returns:
793
+ Confirmation of edit`,
794
+ inputSchema: EditMessageSchema,
795
+ annotations: {
796
+ readOnlyHint: false,
797
+ destructiveHint: false,
798
+ idempotentHint: true,
799
+ openWorldHint: true,
800
+ },
801
+ }, async (params) => {
802
+ try {
803
+ const client = await getClient();
804
+ const channel = client.channels.cache.get(params.channel_id);
805
+ if (!channel || !('messages' in channel)) {
806
+ return {
807
+ isError: true,
808
+ content: [{ type: "text", text: `Text channel not found: ${params.channel_id}` }],
809
+ };
810
+ }
811
+ const message = await channel.messages.fetch(params.message_id);
812
+ await message.edit(params.content);
813
+ return {
814
+ content: [{ type: "text", text: `Edited message ${params.message_id}` }],
815
+ };
816
+ }
817
+ catch (error) {
818
+ return {
819
+ isError: true,
820
+ content: [{ type: "text", text: `Error editing message: ${error.message}` }],
821
+ };
822
+ }
823
+ });
824
+ server.registerTool("discord_add_reaction", {
825
+ title: "Add Reaction to Message",
826
+ description: `Add an emoji reaction to a message.
827
+
828
+ Args:
829
+ - channel_id (string): Discord channel ID
830
+ - message_id (string): Message ID to react to
831
+ - emoji (string): Emoji to react with (e.g., '👍' or custom emoji format)
832
+
833
+ Returns:
834
+ Confirmation of reaction added`,
835
+ inputSchema: AddReactionSchema,
836
+ annotations: {
837
+ readOnlyHint: false,
838
+ destructiveHint: false,
839
+ idempotentHint: true,
840
+ openWorldHint: true,
841
+ },
842
+ }, async (params) => {
843
+ try {
844
+ const client = await getClient();
845
+ const channel = client.channels.cache.get(params.channel_id);
846
+ if (!channel || !('messages' in channel)) {
847
+ return {
848
+ isError: true,
849
+ content: [{ type: "text", text: `Text channel not found: ${params.channel_id}` }],
850
+ };
851
+ }
852
+ const message = await channel.messages.fetch(params.message_id);
853
+ await message.react(params.emoji);
854
+ return {
855
+ content: [{ type: "text", text: `Added reaction ${params.emoji} to message ${params.message_id}` }],
856
+ };
857
+ }
858
+ catch (error) {
859
+ return {
860
+ isError: true,
861
+ content: [{ type: "text", text: `Error adding reaction: ${error.message}` }],
862
+ };
863
+ }
864
+ });
865
+ server.registerTool("discord_remove_reaction", {
866
+ title: "Remove Reaction from Message",
867
+ description: `Remove the bot's emoji reaction from a message.
868
+
869
+ Args:
870
+ - channel_id (string): Discord channel ID
871
+ - message_id (string): Message ID
872
+ - emoji (string): Emoji to remove
873
+
874
+ Returns:
875
+ Confirmation of reaction removed`,
876
+ inputSchema: RemoveReactionSchema,
877
+ annotations: {
878
+ readOnlyHint: false,
879
+ destructiveHint: false,
880
+ idempotentHint: true,
881
+ openWorldHint: true,
882
+ },
883
+ }, async (params) => {
884
+ try {
885
+ const client = await getClient();
886
+ const channel = client.channels.cache.get(params.channel_id);
887
+ if (!channel || !('messages' in channel)) {
888
+ return {
889
+ isError: true,
890
+ content: [{ type: "text", text: `Text channel not found: ${params.channel_id}` }],
891
+ };
892
+ }
893
+ const message = await channel.messages.fetch(params.message_id);
894
+ const reaction = message.reactions.cache.get(params.emoji);
895
+ if (reaction) {
896
+ await reaction.users.remove(client.user.id);
897
+ }
898
+ return {
899
+ content: [{ type: "text", text: `Removed reaction ${params.emoji} from message ${params.message_id}` }],
900
+ };
901
+ }
902
+ catch (error) {
903
+ return {
904
+ isError: true,
905
+ content: [{ type: "text", text: `Error removing reaction: ${error.message}` }],
906
+ };
907
+ }
908
+ });
909
+ server.registerTool("discord_pin_message", {
910
+ title: "Pin Discord Message",
911
+ description: `Pin a message in a channel.
912
+
913
+ Args:
914
+ - channel_id (string): Discord channel ID
915
+ - message_id (string): Message ID to pin
916
+
917
+ Returns:
918
+ Confirmation of message pinned`,
919
+ inputSchema: PinMessageSchema,
920
+ annotations: {
921
+ readOnlyHint: false,
922
+ destructiveHint: false,
923
+ idempotentHint: true,
924
+ openWorldHint: true,
925
+ },
926
+ }, async (params) => {
927
+ try {
928
+ const client = await getClient();
929
+ const channel = client.channels.cache.get(params.channel_id);
930
+ if (!channel || !('messages' in channel)) {
931
+ return {
932
+ isError: true,
933
+ content: [{ type: "text", text: `Text channel not found: ${params.channel_id}` }],
934
+ };
935
+ }
936
+ const message = await channel.messages.fetch(params.message_id);
937
+ await message.pin();
938
+ return {
939
+ content: [{ type: "text", text: `Pinned message ${params.message_id}` }],
940
+ };
941
+ }
942
+ catch (error) {
943
+ return {
944
+ isError: true,
945
+ content: [{ type: "text", text: `Error pinning message: ${error.message}` }],
946
+ };
947
+ }
948
+ });
949
+ server.registerTool("discord_unpin_message", {
950
+ title: "Unpin Discord Message",
951
+ description: `Unpin a message in a channel.
952
+
953
+ Args:
954
+ - channel_id (string): Discord channel ID
955
+ - message_id (string): Message ID to unpin
956
+
957
+ Returns:
958
+ Confirmation of message unpinned`,
959
+ inputSchema: UnpinMessageSchema,
960
+ annotations: {
961
+ readOnlyHint: false,
962
+ destructiveHint: false,
963
+ idempotentHint: true,
964
+ openWorldHint: true,
965
+ },
966
+ }, async (params) => {
967
+ try {
968
+ const client = await getClient();
969
+ const channel = client.channels.cache.get(params.channel_id);
970
+ if (!channel || !('messages' in channel)) {
971
+ return {
972
+ isError: true,
973
+ content: [{ type: "text", text: `Text channel not found: ${params.channel_id}` }],
974
+ };
975
+ }
976
+ const message = await channel.messages.fetch(params.message_id);
977
+ await message.unpin();
978
+ return {
979
+ content: [{ type: "text", text: `Unpinned message ${params.message_id}` }],
980
+ };
981
+ }
982
+ catch (error) {
983
+ return {
984
+ isError: true,
985
+ content: [{ type: "text", text: `Error unpinning message: ${error.message}` }],
986
+ };
987
+ }
988
+ });
989
+ server.registerTool("discord_get_pinned_messages", {
990
+ title: "Get Pinned Messages",
991
+ description: `Get all pinned messages in a channel.
992
+
993
+ Args:
994
+ - channel_id (string): Discord channel ID
995
+ - response_format ('markdown' | 'json'): Output format (default: 'json')
996
+
997
+ Returns:
998
+ List of pinned messages`,
999
+ inputSchema: GetPinnedMessagesSchema,
1000
+ annotations: {
1001
+ readOnlyHint: true,
1002
+ destructiveHint: false,
1003
+ idempotentHint: true,
1004
+ openWorldHint: true,
1005
+ },
1006
+ }, async (params) => {
1007
+ try {
1008
+ const client = await getClient();
1009
+ const channel = client.channels.cache.get(params.channel_id);
1010
+ if (!channel || !('messages' in channel)) {
1011
+ return {
1012
+ isError: true,
1013
+ content: [{ type: "text", text: `Text channel not found: ${params.channel_id}` }],
1014
+ };
1015
+ }
1016
+ const messages = await channel.messages.fetchPinned();
1017
+ const formatted = messages.map((m) => formatMessage(m));
1018
+ const text = formatResponse(Array.from(formatted.values()), params.response_format, (items) => items.length > 0
1019
+ ? items.map(messageToMarkdown).join("\n\n---\n\n")
1020
+ : "No pinned messages in this channel.");
1021
+ return {
1022
+ content: [{ type: "text", text: truncateIfNeeded(text) }],
1023
+ };
1024
+ }
1025
+ catch (error) {
1026
+ return {
1027
+ isError: true,
1028
+ content: [{ type: "text", text: `Error getting pinned messages: ${error.message}` }],
1029
+ };
1030
+ }
1031
+ });
1032
+ // ============================================================================
1033
+ // MEMBER TOOLS
1034
+ // ============================================================================
1035
+ server.registerTool("discord_list_members", {
1036
+ title: "List Discord Members",
1037
+ description: `List members in a Discord server.
1038
+
1039
+ Args:
1040
+ - guild_id (string): Discord server/guild ID
1041
+ - limit (number): Maximum number of members (1-100, default: 20)
1042
+ - after (string, optional): Get members after this user ID (for pagination)
1043
+ - response_format ('markdown' | 'json'): Output format (default: 'json')
1044
+
1045
+ Returns:
1046
+ List of members with username, nickname, roles, join date`,
1047
+ inputSchema: ListMembersSchema,
1048
+ annotations: {
1049
+ readOnlyHint: true,
1050
+ destructiveHint: false,
1051
+ idempotentHint: true,
1052
+ openWorldHint: true,
1053
+ },
1054
+ }, async (params) => {
1055
+ try {
1056
+ const client = await getClient();
1057
+ const guild = client.guilds.cache.get(params.guild_id);
1058
+ if (!guild) {
1059
+ return {
1060
+ isError: true,
1061
+ content: [{ type: "text", text: `Guild not found: ${params.guild_id}` }],
1062
+ };
1063
+ }
1064
+ const options = { limit: params.limit };
1065
+ if (params.after)
1066
+ options.after = params.after;
1067
+ // The fetch API returns a Collection when given limit, use 'as unknown as' to handle the union type
1068
+ const membersResult = (await guild.members.fetch(options));
1069
+ // Convert Collection to array
1070
+ const membersArray = Array.from(membersResult.values());
1071
+ const formatted = membersArray.map((m) => formatMember(m));
1072
+ const text = formatResponse(formatted, params.response_format, (items) => items.map(memberToMarkdown).join("\n\n"));
1073
+ return {
1074
+ content: [{ type: "text", text: truncateIfNeeded(text) }],
1075
+ };
1076
+ }
1077
+ catch (error) {
1078
+ return {
1079
+ isError: true,
1080
+ content: [{ type: "text", text: `Error listing members: ${error.message}` }],
1081
+ };
1082
+ }
1083
+ });
1084
+ server.registerTool("discord_get_member", {
1085
+ title: "Get Discord Member",
1086
+ description: `Get detailed information about a specific member.
1087
+
1088
+ Args:
1089
+ - guild_id (string): Discord server/guild ID
1090
+ - user_id (string): Discord user ID
1091
+ - response_format ('markdown' | 'json'): Output format (default: 'json')
1092
+
1093
+ Returns:
1094
+ Member details including username, nickname, roles, join date`,
1095
+ inputSchema: GetMemberSchema,
1096
+ annotations: {
1097
+ readOnlyHint: true,
1098
+ destructiveHint: false,
1099
+ idempotentHint: true,
1100
+ openWorldHint: true,
1101
+ },
1102
+ }, async (params) => {
1103
+ try {
1104
+ const client = await getClient();
1105
+ const guild = client.guilds.cache.get(params.guild_id);
1106
+ if (!guild) {
1107
+ return {
1108
+ isError: true,
1109
+ content: [{ type: "text", text: `Guild not found: ${params.guild_id}` }],
1110
+ };
1111
+ }
1112
+ const member = await guild.members.fetch(params.user_id);
1113
+ const formatted = formatMember(member);
1114
+ const text = formatResponse(formatted, params.response_format, memberToMarkdown);
1115
+ return {
1116
+ content: [{ type: "text", text }],
1117
+ };
1118
+ }
1119
+ catch (error) {
1120
+ return {
1121
+ isError: true,
1122
+ content: [{ type: "text", text: `Error getting member: ${error.message}` }],
1123
+ };
1124
+ }
1125
+ });
1126
+ server.registerTool("discord_move_member", {
1127
+ title: "Move Member to Voice Channel",
1128
+ description: `Move a member to a different voice channel or disconnect them from voice.
1129
+
1130
+ The member must currently be connected to a voice channel to be moved.
1131
+
1132
+ Args:
1133
+ - guild_id (string): Discord server/guild ID
1134
+ - user_id (string): Discord user ID to move
1135
+ - channel_id (string | null): Target voice channel ID, or null to disconnect
1136
+
1137
+ Returns:
1138
+ Confirmation of move or disconnect`,
1139
+ inputSchema: MoveMemberSchema,
1140
+ annotations: {
1141
+ readOnlyHint: false,
1142
+ destructiveHint: false,
1143
+ idempotentHint: true,
1144
+ openWorldHint: true,
1145
+ },
1146
+ }, async (params) => {
1147
+ try {
1148
+ const client = await getClient();
1149
+ const guild = client.guilds.cache.get(params.guild_id);
1150
+ if (!guild) {
1151
+ return {
1152
+ isError: true,
1153
+ content: [{ type: "text", text: `Guild not found: ${params.guild_id}` }],
1154
+ };
1155
+ }
1156
+ const member = await guild.members.fetch(params.user_id);
1157
+ if (!member.voice.channel) {
1158
+ return {
1159
+ isError: true,
1160
+ content: [{ type: "text", text: `User ${params.user_id} is not connected to a voice channel` }],
1161
+ };
1162
+ }
1163
+ const previousChannel = member.voice.channel.name;
1164
+ if (params.channel_id === null) {
1165
+ await member.voice.disconnect();
1166
+ return {
1167
+ content: [{ type: "text", text: `Disconnected user ${member.user.tag} from voice channel "${previousChannel}"` }],
1168
+ };
1169
+ }
1170
+ const targetChannel = guild.channels.cache.get(params.channel_id);
1171
+ if (!targetChannel || !targetChannel.isVoiceBased()) {
1172
+ return {
1173
+ isError: true,
1174
+ content: [{ type: "text", text: `Voice channel not found: ${params.channel_id}` }],
1175
+ };
1176
+ }
1177
+ await member.voice.setChannel(targetChannel);
1178
+ return {
1179
+ content: [{ type: "text", text: `Moved user ${member.user.tag} from "${previousChannel}" to "${targetChannel.name}"` }],
1180
+ };
1181
+ }
1182
+ catch (error) {
1183
+ return {
1184
+ isError: true,
1185
+ content: [{ type: "text", text: `Error moving member: ${error.message}` }],
1186
+ };
1187
+ }
1188
+ });
1189
+ server.registerTool("discord_kick_member", {
1190
+ title: "Kick Discord Member",
1191
+ description: `Kick a member from a Discord server.
1192
+
1193
+ Args:
1194
+ - guild_id (string): Discord server/guild ID
1195
+ - user_id (string): Discord user ID to kick
1196
+ - reason (string, optional): Reason for kick (visible in audit log)
1197
+
1198
+ Returns:
1199
+ Confirmation of kick`,
1200
+ inputSchema: KickMemberSchema,
1201
+ annotations: {
1202
+ readOnlyHint: false,
1203
+ destructiveHint: true,
1204
+ idempotentHint: false,
1205
+ openWorldHint: true,
1206
+ },
1207
+ }, async (params) => {
1208
+ try {
1209
+ const client = await getClient();
1210
+ const guild = client.guilds.cache.get(params.guild_id);
1211
+ if (!guild) {
1212
+ return {
1213
+ isError: true,
1214
+ content: [{ type: "text", text: `Guild not found: ${params.guild_id}` }],
1215
+ };
1216
+ }
1217
+ const member = await guild.members.fetch(params.user_id);
1218
+ await member.kick(params.reason);
1219
+ return {
1220
+ content: [{ type: "text", text: `Kicked user ${params.user_id} from ${guild.name}` }],
1221
+ };
1222
+ }
1223
+ catch (error) {
1224
+ return {
1225
+ isError: true,
1226
+ content: [{ type: "text", text: `Error kicking member: ${error.message}` }],
1227
+ };
1228
+ }
1229
+ });
1230
+ server.registerTool("discord_ban_member", {
1231
+ title: "Ban Discord Member",
1232
+ description: `Ban a member from a Discord server.
1233
+
1234
+ Args:
1235
+ - guild_id (string): Discord server/guild ID
1236
+ - user_id (string): Discord user ID to ban
1237
+ - reason (string, optional): Reason for ban (visible in audit log)
1238
+ - delete_message_days (number): Days of messages to delete (0-7, default: 0)
1239
+
1240
+ Returns:
1241
+ Confirmation of ban`,
1242
+ inputSchema: BanMemberSchema,
1243
+ annotations: {
1244
+ readOnlyHint: false,
1245
+ destructiveHint: true,
1246
+ idempotentHint: false,
1247
+ openWorldHint: true,
1248
+ },
1249
+ }, async (params) => {
1250
+ try {
1251
+ const client = await getClient();
1252
+ const guild = client.guilds.cache.get(params.guild_id);
1253
+ if (!guild) {
1254
+ return {
1255
+ isError: true,
1256
+ content: [{ type: "text", text: `Guild not found: ${params.guild_id}` }],
1257
+ };
1258
+ }
1259
+ await guild.members.ban(params.user_id, {
1260
+ reason: params.reason,
1261
+ deleteMessageSeconds: params.delete_message_days * 86400,
1262
+ });
1263
+ return {
1264
+ content: [{ type: "text", text: `Banned user ${params.user_id} from ${guild.name}` }],
1265
+ };
1266
+ }
1267
+ catch (error) {
1268
+ return {
1269
+ isError: true,
1270
+ content: [{ type: "text", text: `Error banning member: ${error.message}` }],
1271
+ };
1272
+ }
1273
+ });
1274
+ server.registerTool("discord_unban_member", {
1275
+ title: "Unban Discord Member",
1276
+ description: `Remove a ban from a user.
1277
+
1278
+ Args:
1279
+ - guild_id (string): Discord server/guild ID
1280
+ - user_id (string): Discord user ID to unban
1281
+
1282
+ Returns:
1283
+ Confirmation of unban`,
1284
+ inputSchema: UnbanMemberSchema,
1285
+ annotations: {
1286
+ readOnlyHint: false,
1287
+ destructiveHint: false,
1288
+ idempotentHint: true,
1289
+ openWorldHint: true,
1290
+ },
1291
+ }, async (params) => {
1292
+ try {
1293
+ const client = await getClient();
1294
+ const guild = client.guilds.cache.get(params.guild_id);
1295
+ if (!guild) {
1296
+ return {
1297
+ isError: true,
1298
+ content: [{ type: "text", text: `Guild not found: ${params.guild_id}` }],
1299
+ };
1300
+ }
1301
+ await guild.members.unban(params.user_id);
1302
+ return {
1303
+ content: [{ type: "text", text: `Unbanned user ${params.user_id} from ${guild.name}` }],
1304
+ };
1305
+ }
1306
+ catch (error) {
1307
+ return {
1308
+ isError: true,
1309
+ content: [{ type: "text", text: `Error unbanning member: ${error.message}` }],
1310
+ };
1311
+ }
1312
+ });
1313
+ server.registerTool("discord_set_nickname", {
1314
+ title: "Set Member Nickname",
1315
+ description: `Set or clear a member's nickname.
1316
+
1317
+ Args:
1318
+ - guild_id (string): Discord server/guild ID
1319
+ - user_id (string): Discord user ID
1320
+ - nickname (string, optional): New nickname (empty/null to reset)
1321
+
1322
+ Returns:
1323
+ Confirmation of nickname change`,
1324
+ inputSchema: SetNicknameSchema,
1325
+ annotations: {
1326
+ readOnlyHint: false,
1327
+ destructiveHint: false,
1328
+ idempotentHint: true,
1329
+ openWorldHint: true,
1330
+ },
1331
+ }, async (params) => {
1332
+ try {
1333
+ const client = await getClient();
1334
+ const guild = client.guilds.cache.get(params.guild_id);
1335
+ if (!guild) {
1336
+ return {
1337
+ isError: true,
1338
+ content: [{ type: "text", text: `Guild not found: ${params.guild_id}` }],
1339
+ };
1340
+ }
1341
+ const member = await guild.members.fetch(params.user_id);
1342
+ await member.setNickname(params.nickname ?? null);
1343
+ const action = params.nickname ? `set to "${params.nickname}"` : "reset";
1344
+ return {
1345
+ content: [{ type: "text", text: `Nickname ${action} for user ${params.user_id}` }],
1346
+ };
1347
+ }
1348
+ catch (error) {
1349
+ return {
1350
+ isError: true,
1351
+ content: [{ type: "text", text: `Error setting nickname: ${error.message}` }],
1352
+ };
1353
+ }
1354
+ });
1355
+ // ============================================================================
1356
+ // ROLE TOOLS
1357
+ // ============================================================================
1358
+ server.registerTool("discord_list_roles", {
1359
+ title: "List Discord Roles",
1360
+ description: `List all roles in a Discord server.
1361
+
1362
+ Args:
1363
+ - guild_id (string): Discord server/guild ID
1364
+ - response_format ('markdown' | 'json'): Output format (default: 'json')
1365
+
1366
+ Returns:
1367
+ List of roles with name, color, position, permissions`,
1368
+ inputSchema: ListRolesSchema,
1369
+ annotations: {
1370
+ readOnlyHint: true,
1371
+ destructiveHint: false,
1372
+ idempotentHint: true,
1373
+ openWorldHint: true,
1374
+ },
1375
+ }, async (params) => {
1376
+ try {
1377
+ const client = await getClient();
1378
+ const guild = client.guilds.cache.get(params.guild_id);
1379
+ if (!guild) {
1380
+ return {
1381
+ isError: true,
1382
+ content: [{ type: "text", text: `Guild not found: ${params.guild_id}` }],
1383
+ };
1384
+ }
1385
+ const roles = guild.roles.cache
1386
+ .filter(r => r.name !== "@everyone")
1387
+ .sort((a, b) => b.position - a.position)
1388
+ .map(formatRole);
1389
+ const text = formatResponse(Array.from(roles.values()), params.response_format, (items) => items.map(roleToMarkdown).join("\n\n"));
1390
+ return {
1391
+ content: [{ type: "text", text: truncateIfNeeded(text) }],
1392
+ };
1393
+ }
1394
+ catch (error) {
1395
+ return {
1396
+ isError: true,
1397
+ content: [{ type: "text", text: `Error listing roles: ${error.message}` }],
1398
+ };
1399
+ }
1400
+ });
1401
+ server.registerTool("discord_add_role", {
1402
+ title: "Add Role to Member",
1403
+ description: `Add a role to a Discord member.
1404
+
1405
+ Args:
1406
+ - guild_id (string): Discord server/guild ID
1407
+ - user_id (string): Discord user ID
1408
+ - role_id (string): Discord role ID to add
1409
+
1410
+ Returns:
1411
+ Confirmation of role added`,
1412
+ inputSchema: AddRoleSchema,
1413
+ annotations: {
1414
+ readOnlyHint: false,
1415
+ destructiveHint: false,
1416
+ idempotentHint: true,
1417
+ openWorldHint: true,
1418
+ },
1419
+ }, async (params) => {
1420
+ try {
1421
+ const client = await getClient();
1422
+ const guild = client.guilds.cache.get(params.guild_id);
1423
+ if (!guild) {
1424
+ return {
1425
+ isError: true,
1426
+ content: [{ type: "text", text: `Guild not found: ${params.guild_id}` }],
1427
+ };
1428
+ }
1429
+ const member = await guild.members.fetch(params.user_id);
1430
+ const role = guild.roles.cache.get(params.role_id);
1431
+ if (!role) {
1432
+ return {
1433
+ isError: true,
1434
+ content: [{ type: "text", text: `Role not found: ${params.role_id}` }],
1435
+ };
1436
+ }
1437
+ await member.roles.add(role);
1438
+ return {
1439
+ content: [{ type: "text", text: `Added role "${role.name}" to user ${params.user_id}` }],
1440
+ };
1441
+ }
1442
+ catch (error) {
1443
+ return {
1444
+ isError: true,
1445
+ content: [{ type: "text", text: `Error adding role: ${error.message}` }],
1446
+ };
1447
+ }
1448
+ });
1449
+ server.registerTool("discord_remove_role", {
1450
+ title: "Remove Role from Member",
1451
+ description: `Remove a role from a Discord member.
1452
+
1453
+ Args:
1454
+ - guild_id (string): Discord server/guild ID
1455
+ - user_id (string): Discord user ID
1456
+ - role_id (string): Discord role ID to remove
1457
+
1458
+ Returns:
1459
+ Confirmation of role removed`,
1460
+ inputSchema: RemoveRoleSchema,
1461
+ annotations: {
1462
+ readOnlyHint: false,
1463
+ destructiveHint: false,
1464
+ idempotentHint: true,
1465
+ openWorldHint: true,
1466
+ },
1467
+ }, async (params) => {
1468
+ try {
1469
+ const client = await getClient();
1470
+ const guild = client.guilds.cache.get(params.guild_id);
1471
+ if (!guild) {
1472
+ return {
1473
+ isError: true,
1474
+ content: [{ type: "text", text: `Guild not found: ${params.guild_id}` }],
1475
+ };
1476
+ }
1477
+ const member = await guild.members.fetch(params.user_id);
1478
+ const role = guild.roles.cache.get(params.role_id);
1479
+ if (!role) {
1480
+ return {
1481
+ isError: true,
1482
+ content: [{ type: "text", text: `Role not found: ${params.role_id}` }],
1483
+ };
1484
+ }
1485
+ await member.roles.remove(role);
1486
+ return {
1487
+ content: [{ type: "text", text: `Removed role "${role.name}" from user ${params.user_id}` }],
1488
+ };
1489
+ }
1490
+ catch (error) {
1491
+ return {
1492
+ isError: true,
1493
+ content: [{ type: "text", text: `Error removing role: ${error.message}` }],
1494
+ };
1495
+ }
1496
+ });
1497
+ server.registerTool("discord_create_role", {
1498
+ title: "Create Discord Role",
1499
+ description: `Create a new role in a Discord server.
1500
+
1501
+ Args:
1502
+ - guild_id (string): Discord server/guild ID
1503
+ - name (string): Role name
1504
+ - color (number, optional): Role color as decimal integer (e.g., 16711680 for red)
1505
+ - mentionable (boolean): Whether the role can be mentioned (default: false)
1506
+ - hoist (boolean): Whether to display role members separately (default: false)
1507
+
1508
+ Returns:
1509
+ The created role's details`,
1510
+ inputSchema: CreateRoleSchema,
1511
+ annotations: {
1512
+ readOnlyHint: false,
1513
+ destructiveHint: false,
1514
+ idempotentHint: false,
1515
+ openWorldHint: true,
1516
+ },
1517
+ }, async (params) => {
1518
+ try {
1519
+ const client = await getClient();
1520
+ const guild = client.guilds.cache.get(params.guild_id);
1521
+ if (!guild) {
1522
+ return {
1523
+ isError: true,
1524
+ content: [{ type: "text", text: `Guild not found: ${params.guild_id}` }],
1525
+ };
1526
+ }
1527
+ const role = await guild.roles.create({
1528
+ name: params.name,
1529
+ color: params.color,
1530
+ mentionable: params.mentionable,
1531
+ hoist: params.hoist,
1532
+ });
1533
+ return {
1534
+ content: [{ type: "text", text: `Created role "${role.name}" (ID: ${role.id})` }],
1535
+ };
1536
+ }
1537
+ catch (error) {
1538
+ return {
1539
+ isError: true,
1540
+ content: [{ type: "text", text: `Error creating role: ${error.message}` }],
1541
+ };
1542
+ }
1543
+ });
1544
+ server.registerTool("discord_delete_role", {
1545
+ title: "Delete Discord Role",
1546
+ description: `Delete a role from a Discord server. This action is permanent!
1547
+
1548
+ Args:
1549
+ - guild_id (string): Discord server/guild ID
1550
+ - role_id (string): Discord role ID to delete
1551
+
1552
+ Returns:
1553
+ Confirmation of deletion`,
1554
+ inputSchema: DeleteRoleSchema,
1555
+ annotations: {
1556
+ readOnlyHint: false,
1557
+ destructiveHint: true,
1558
+ idempotentHint: false,
1559
+ openWorldHint: true,
1560
+ },
1561
+ }, async (params) => {
1562
+ try {
1563
+ const client = await getClient();
1564
+ const guild = client.guilds.cache.get(params.guild_id);
1565
+ if (!guild) {
1566
+ return {
1567
+ isError: true,
1568
+ content: [{ type: "text", text: `Guild not found: ${params.guild_id}` }],
1569
+ };
1570
+ }
1571
+ const role = guild.roles.cache.get(params.role_id);
1572
+ if (!role) {
1573
+ return {
1574
+ isError: true,
1575
+ content: [{ type: "text", text: `Role not found: ${params.role_id}` }],
1576
+ };
1577
+ }
1578
+ const name = role.name;
1579
+ await role.delete();
1580
+ return {
1581
+ content: [{ type: "text", text: `Deleted role: ${name}` }],
1582
+ };
1583
+ }
1584
+ catch (error) {
1585
+ return {
1586
+ isError: true,
1587
+ content: [{ type: "text", text: `Error deleting role: ${error.message}` }],
1588
+ };
1589
+ }
1590
+ });
1591
+ // Full list of role permissions for reference
1592
+ const ROLE_PERMISSION_LIST = [
1593
+ // General Server Permissions
1594
+ 'Administrator', 'ViewAuditLog', 'ViewGuildInsights', 'ManageGuild', 'ManageRoles',
1595
+ 'ManageChannels', 'KickMembers', 'BanMembers', 'CreateInstantInvite', 'ChangeNickname',
1596
+ 'ManageNicknames', 'ManageEmojisAndStickers', 'ManageWebhooks', 'ManageGuildExpressions',
1597
+ 'ViewCreatorMonetizationAnalytics', 'ModerateMembers',
1598
+ // Text Channel Permissions
1599
+ 'ViewChannel', 'SendMessages', 'SendTTSMessages', 'ManageMessages', 'EmbedLinks',
1600
+ 'AttachFiles', 'ReadMessageHistory', 'MentionEveryone', 'UseExternalEmojis',
1601
+ 'AddReactions', 'UseApplicationCommands', 'ManageThreads', 'CreatePublicThreads',
1602
+ 'CreatePrivateThreads', 'UseExternalStickers', 'SendMessagesInThreads', 'SendVoiceMessages',
1603
+ 'SendPolls', 'UseExternalApps',
1604
+ // Voice Channel Permissions
1605
+ 'Connect', 'Speak', 'MuteMembers', 'DeafenMembers', 'MoveMembers', 'UseVAD',
1606
+ 'PrioritySpeaker', 'Stream', 'UseSoundboard', 'UseExternalSounds', 'UseEmbeddedActivities',
1607
+ // Stage Channel Permissions
1608
+ 'RequestToSpeak',
1609
+ // Events Permissions
1610
+ 'ManageEvents', 'CreateEvents'
1611
+ ];
1612
+ server.registerTool("discord_edit_role", {
1613
+ title: "Edit Discord Role",
1614
+ description: `Edit a role's properties including name, color, and permissions.
1615
+
1616
+ Common permission names:
1617
+ - Admin: Administrator (grants all permissions)
1618
+ - General: ManageGuild, ManageRoles, ManageChannels, KickMembers, BanMembers, ModerateMembers
1619
+ - Messages: SendMessages, ManageMessages, EmbedLinks, AttachFiles, ReadMessageHistory, MentionEveryone
1620
+ - Voice: Connect, Speak, MuteMembers, DeafenMembers, MoveMembers, Stream
1621
+ - Other: ManageEmojisAndStickers, ManageWebhooks, ManageEvents, ViewAuditLog
1622
+
1623
+ Args:
1624
+ - guild_id (string): Discord server/guild ID
1625
+ - role_id (string): Discord role ID to edit
1626
+ - name (string, optional): New role name
1627
+ - color (number, optional): Role color as decimal integer (e.g., 16711680 for red)
1628
+ - mentionable (boolean, optional): Whether the role can be mentioned
1629
+ - hoist (boolean, optional): Whether to display role members separately
1630
+ - permissions (string[], optional): Permissions to grant (replaces existing). Examples: ['SendMessages', 'ViewChannel']
1631
+
1632
+ Returns:
1633
+ Updated role details`,
1634
+ inputSchema: EditRoleSchema,
1635
+ annotations: {
1636
+ readOnlyHint: false,
1637
+ destructiveHint: false,
1638
+ idempotentHint: true,
1639
+ openWorldHint: true,
1640
+ },
1641
+ }, async (params) => {
1642
+ try {
1643
+ const client = await getClient();
1644
+ const guild = client.guilds.cache.get(params.guild_id);
1645
+ if (!guild) {
1646
+ return {
1647
+ isError: true,
1648
+ content: [{ type: "text", text: `Guild not found: ${params.guild_id}` }],
1649
+ };
1650
+ }
1651
+ const role = guild.roles.cache.get(params.role_id);
1652
+ if (!role) {
1653
+ return {
1654
+ isError: true,
1655
+ content: [{ type: "text", text: `Role not found: ${params.role_id}` }],
1656
+ };
1657
+ }
1658
+ // Build the edit options
1659
+ const editOptions = {};
1660
+ if (params.name !== undefined)
1661
+ editOptions.name = params.name;
1662
+ if (params.color !== undefined)
1663
+ editOptions.color = params.color;
1664
+ if (params.mentionable !== undefined)
1665
+ editOptions.mentionable = params.mentionable;
1666
+ if (params.hoist !== undefined)
1667
+ editOptions.hoist = params.hoist;
1668
+ // Handle permissions if provided
1669
+ if (params.permissions !== undefined) {
1670
+ const permissionBits = new PermissionsBitField();
1671
+ for (const perm of params.permissions) {
1672
+ try {
1673
+ permissionBits.add(perm);
1674
+ }
1675
+ catch {
1676
+ return {
1677
+ isError: true,
1678
+ content: [{ type: "text", text: `Invalid permission: ${perm}. Valid permissions include: ${ROLE_PERMISSION_LIST.slice(0, 10).join(', ')}, ...` }],
1679
+ };
1680
+ }
1681
+ }
1682
+ editOptions.permissions = permissionBits.bitfield;
1683
+ }
1684
+ const updatedRole = await role.edit(editOptions);
1685
+ const changes = [];
1686
+ if (params.name !== undefined)
1687
+ changes.push(`name: "${updatedRole.name}"`);
1688
+ if (params.color !== undefined)
1689
+ changes.push(`color: ${updatedRole.hexColor}`);
1690
+ if (params.mentionable !== undefined)
1691
+ changes.push(`mentionable: ${updatedRole.mentionable}`);
1692
+ if (params.hoist !== undefined)
1693
+ changes.push(`hoist: ${updatedRole.hoist}`);
1694
+ if (params.permissions !== undefined)
1695
+ changes.push(`permissions updated`);
1696
+ return {
1697
+ content: [{ type: "text", text: `Updated role "${updatedRole.name}" (ID: ${updatedRole.id})\nChanges: ${changes.join(', ')}` }],
1698
+ };
1699
+ }
1700
+ catch (error) {
1701
+ return {
1702
+ isError: true,
1703
+ content: [{ type: "text", text: `Error editing role: ${error.message}` }],
1704
+ };
1705
+ }
1706
+ });
1707
+ server.registerTool("discord_set_role_positions", {
1708
+ title: "Set Role Positions",
1709
+ description: `Reorder role hierarchy by setting role positions.
1710
+
1711
+ Higher position = more authority in the hierarchy. The @everyone role is always at position 0.
1712
+ Roles can only manage other roles below them in the hierarchy.
1713
+
1714
+ IMPORTANT: The bot can only move roles that are below its own highest role in the hierarchy.
1715
+
1716
+ Args:
1717
+ - guild_id (string): Discord server/guild ID
1718
+ - positions (array): Array of objects with:
1719
+ - role_id (string): Discord role ID
1720
+ - position (number): New position for the role (higher = more authority)
1721
+
1722
+ Returns:
1723
+ Confirmation of position changes`,
1724
+ inputSchema: SetRolePositionsSchema,
1725
+ annotations: {
1726
+ readOnlyHint: false,
1727
+ destructiveHint: false,
1728
+ idempotentHint: true,
1729
+ openWorldHint: true,
1730
+ },
1731
+ }, async (params) => {
1732
+ try {
1733
+ const client = await getClient();
1734
+ const guild = client.guilds.cache.get(params.guild_id);
1735
+ if (!guild) {
1736
+ return {
1737
+ isError: true,
1738
+ content: [{ type: "text", text: `Guild not found: ${params.guild_id}` }],
1739
+ };
1740
+ }
1741
+ // Validate all roles exist before making changes
1742
+ for (const pos of params.positions) {
1743
+ const role = guild.roles.cache.get(pos.role_id);
1744
+ if (!role) {
1745
+ return {
1746
+ isError: true,
1747
+ content: [{ type: "text", text: `Role not found: ${pos.role_id}` }],
1748
+ };
1749
+ }
1750
+ }
1751
+ // Format positions for Discord API
1752
+ const positionUpdates = params.positions.map(pos => ({
1753
+ role: pos.role_id,
1754
+ position: pos.position,
1755
+ }));
1756
+ await guild.roles.setPositions(positionUpdates);
1757
+ // Build response with updated positions
1758
+ const updatedRoles = params.positions.map(pos => {
1759
+ const role = guild.roles.cache.get(pos.role_id);
1760
+ return `"${role?.name}" → position ${pos.position}`;
1761
+ });
1762
+ return {
1763
+ content: [{ type: "text", text: `Updated role positions:\n${updatedRoles.join('\n')}` }],
1764
+ };
1765
+ }
1766
+ catch (error) {
1767
+ return {
1768
+ isError: true,
1769
+ content: [{ type: "text", text: `Error setting role positions: ${error.message}` }],
1770
+ };
1771
+ }
1772
+ });
1773
+ // ============================================================================
1774
+ // GUILD MANAGEMENT TOOLS
1775
+ // ============================================================================
1776
+ server.registerTool("discord_edit_guild", {
1777
+ title: "Edit Guild Settings",
1778
+ description: `Edit server settings like name, verification level, and system channels.
1779
+
1780
+ Verification Levels: 0=None, 1=Low (verified email), 2=Medium (5 min member), 3=High (10 min member), 4=Very High (verified phone)
1781
+ Content Filter: 0=Disabled, 1=Members without roles, 2=All members
1782
+ Notifications: 0=All messages, 1=Only mentions
1783
+
1784
+ Args:
1785
+ - guild_id (string): Discord server/guild ID
1786
+ - name (string, optional): New server name
1787
+ - description (string, optional): Server description (Community servers only)
1788
+ - afk_channel_id (string, optional): AFK voice channel ID
1789
+ - afk_timeout (number, optional): AFK timeout in seconds (60, 300, 900, 1800, 3600)
1790
+ - system_channel_id (string, optional): System message channel ID
1791
+ - rules_channel_id (string, optional): Rules channel ID (Community only)
1792
+ - public_updates_channel_id (string, optional): Public updates channel ID (Community only)
1793
+ - verification_level (number, optional): 0-4
1794
+ - explicit_content_filter (number, optional): 0-2
1795
+ - default_message_notifications (number, optional): 0-1
1796
+
1797
+ Returns:
1798
+ Updated guild settings`,
1799
+ inputSchema: EditGuildSchema,
1800
+ annotations: {
1801
+ readOnlyHint: false,
1802
+ destructiveHint: false,
1803
+ idempotentHint: true,
1804
+ openWorldHint: true,
1805
+ },
1806
+ }, async (params) => {
1807
+ try {
1808
+ const client = await getClient();
1809
+ const guild = client.guilds.cache.get(params.guild_id);
1810
+ if (!guild) {
1811
+ return {
1812
+ isError: true,
1813
+ content: [{ type: "text", text: `Guild not found: ${params.guild_id}` }],
1814
+ };
1815
+ }
1816
+ const editOptions = {};
1817
+ if (params.name !== undefined)
1818
+ editOptions.name = params.name;
1819
+ if (params.description !== undefined)
1820
+ editOptions.description = params.description;
1821
+ if (params.afk_channel_id !== undefined)
1822
+ editOptions.afkChannel = params.afk_channel_id;
1823
+ if (params.afk_timeout !== undefined)
1824
+ editOptions.afkTimeout = params.afk_timeout;
1825
+ if (params.system_channel_id !== undefined)
1826
+ editOptions.systemChannel = params.system_channel_id;
1827
+ if (params.rules_channel_id !== undefined)
1828
+ editOptions.rulesChannel = params.rules_channel_id;
1829
+ if (params.public_updates_channel_id !== undefined)
1830
+ editOptions.publicUpdatesChannel = params.public_updates_channel_id;
1831
+ if (params.verification_level !== undefined)
1832
+ editOptions.verificationLevel = params.verification_level;
1833
+ if (params.explicit_content_filter !== undefined)
1834
+ editOptions.explicitContentFilter = params.explicit_content_filter;
1835
+ if (params.default_message_notifications !== undefined)
1836
+ editOptions.defaultMessageNotifications = params.default_message_notifications;
1837
+ await guild.edit(editOptions);
1838
+ return {
1839
+ content: [{ type: "text", text: `Updated guild settings for "${guild.name}"` }],
1840
+ };
1841
+ }
1842
+ catch (error) {
1843
+ return {
1844
+ isError: true,
1845
+ content: [{ type: "text", text: `Error editing guild: ${error.message}` }],
1846
+ };
1847
+ }
1848
+ });
1849
+ // ============================================================================
1850
+ // MEMBER MODERATION TOOLS
1851
+ // ============================================================================
1852
+ server.registerTool("discord_timeout_member", {
1853
+ title: "Timeout Member",
1854
+ description: `Timeout (mute) a member for a specified duration. They cannot send messages, react, or join voice.
1855
+
1856
+ Args:
1857
+ - guild_id (string): Discord server/guild ID
1858
+ - user_id (string): Discord user ID to timeout
1859
+ - duration_minutes (number): Timeout duration in minutes (0 to remove, max 40320 = 28 days)
1860
+ - reason (string, optional): Reason for timeout (visible in audit log)
1861
+
1862
+ Returns:
1863
+ Confirmation of timeout`,
1864
+ inputSchema: TimeoutMemberSchema,
1865
+ annotations: {
1866
+ readOnlyHint: false,
1867
+ destructiveHint: false,
1868
+ idempotentHint: true,
1869
+ openWorldHint: true,
1870
+ },
1871
+ }, async (params) => {
1872
+ try {
1873
+ const client = await getClient();
1874
+ const guild = client.guilds.cache.get(params.guild_id);
1875
+ if (!guild) {
1876
+ return {
1877
+ isError: true,
1878
+ content: [{ type: "text", text: `Guild not found: ${params.guild_id}` }],
1879
+ };
1880
+ }
1881
+ const member = await guild.members.fetch(params.user_id).catch(() => null);
1882
+ if (!member) {
1883
+ return {
1884
+ isError: true,
1885
+ content: [{ type: "text", text: `Member not found: ${params.user_id}` }],
1886
+ };
1887
+ }
1888
+ if (params.duration_minutes === 0) {
1889
+ await member.timeout(null, params.reason);
1890
+ return {
1891
+ content: [{ type: "text", text: `Removed timeout from ${member.user.username}` }],
1892
+ };
1893
+ }
1894
+ const timeoutMs = params.duration_minutes * 60 * 1000;
1895
+ await member.timeout(timeoutMs, params.reason);
1896
+ return {
1897
+ content: [{ type: "text", text: `Timed out ${member.user.username} for ${params.duration_minutes} minutes` }],
1898
+ };
1899
+ }
1900
+ catch (error) {
1901
+ return {
1902
+ isError: true,
1903
+ content: [{ type: "text", text: `Error timing out member: ${error.message}` }],
1904
+ };
1905
+ }
1906
+ });
1907
+ server.registerTool("discord_list_bans", {
1908
+ title: "List Bans",
1909
+ description: `List all banned users in a server.
1910
+
1911
+ Args:
1912
+ - guild_id (string): Discord server/guild ID
1913
+ - limit (number): Number of bans to return (1-1000, default 100)
1914
+ - response_format ('json' | 'markdown'): Output format
1915
+
1916
+ Returns:
1917
+ List of banned users with reasons`,
1918
+ inputSchema: ListBansSchema,
1919
+ annotations: {
1920
+ readOnlyHint: true,
1921
+ destructiveHint: false,
1922
+ idempotentHint: true,
1923
+ openWorldHint: true,
1924
+ },
1925
+ }, async (params) => {
1926
+ try {
1927
+ const client = await getClient();
1928
+ const guild = client.guilds.cache.get(params.guild_id);
1929
+ if (!guild) {
1930
+ return {
1931
+ isError: true,
1932
+ content: [{ type: "text", text: `Guild not found: ${params.guild_id}` }],
1933
+ };
1934
+ }
1935
+ const bans = await guild.bans.fetch({ limit: params.limit });
1936
+ const banList = bans.map(ban => ({
1937
+ userId: ban.user.id,
1938
+ username: ban.user.username,
1939
+ reason: ban.reason || 'No reason provided',
1940
+ }));
1941
+ const result = formatResponse(banList, params.response_format, (items) => items.map(b => `**${b.username}** (${b.userId})\nReason: ${b.reason}`).join('\n\n'));
1942
+ return {
1943
+ content: [{ type: "text", text: truncateIfNeeded(result) }],
1944
+ };
1945
+ }
1946
+ catch (error) {
1947
+ return {
1948
+ isError: true,
1949
+ content: [{ type: "text", text: `Error listing bans: ${error.message}` }],
1950
+ };
1951
+ }
1952
+ });
1953
+ // ============================================================================
1954
+ // EMOJI TOOLS
1955
+ // ============================================================================
1956
+ server.registerTool("discord_list_emojis", {
1957
+ title: "List Emojis",
1958
+ description: `List all custom emojis in a server.
1959
+
1960
+ Args:
1961
+ - guild_id (string): Discord server/guild ID
1962
+ - response_format ('json' | 'markdown'): Output format
1963
+
1964
+ Returns:
1965
+ List of emojis with name, ID, and whether animated`,
1966
+ inputSchema: ListEmojisSchema,
1967
+ annotations: {
1968
+ readOnlyHint: true,
1969
+ destructiveHint: false,
1970
+ idempotentHint: true,
1971
+ openWorldHint: true,
1972
+ },
1973
+ }, async (params) => {
1974
+ try {
1975
+ const client = await getClient();
1976
+ const guild = client.guilds.cache.get(params.guild_id);
1977
+ if (!guild) {
1978
+ return {
1979
+ isError: true,
1980
+ content: [{ type: "text", text: `Guild not found: ${params.guild_id}` }],
1981
+ };
1982
+ }
1983
+ const emojis = guild.emojis.cache.map(emoji => ({
1984
+ id: emoji.id,
1985
+ name: emoji.name,
1986
+ animated: emoji.animated,
1987
+ available: emoji.available,
1988
+ managed: emoji.managed,
1989
+ requireColons: emoji.requiresColons,
1990
+ roles: emoji.roles.cache.map(r => r.name),
1991
+ url: emoji.url,
1992
+ }));
1993
+ const result = formatResponse(emojis, params.response_format, (items) => items.map(e => `${e.animated ? '(animated) ' : ''}**${e.name}** - ID: ${e.id}`).join('\n'));
1994
+ return {
1995
+ content: [{ type: "text", text: truncateIfNeeded(result) }],
1996
+ };
1997
+ }
1998
+ catch (error) {
1999
+ return {
2000
+ isError: true,
2001
+ content: [{ type: "text", text: `Error listing emojis: ${error.message}` }],
2002
+ };
2003
+ }
2004
+ });
2005
+ server.registerTool("discord_create_emoji", {
2006
+ title: "Create Emoji",
2007
+ description: `Create a custom emoji in a server.
2008
+
2009
+ Args:
2010
+ - guild_id (string): Discord server/guild ID
2011
+ - name (string): Emoji name (2-32 chars, alphanumeric and underscores)
2012
+ - image_url (string): URL of image (PNG, JPG, GIF under 256KB)
2013
+ - roles (string[], optional): Role IDs that can use this emoji
2014
+
2015
+ Returns:
2016
+ Created emoji details`,
2017
+ inputSchema: CreateEmojiSchema,
2018
+ annotations: {
2019
+ readOnlyHint: false,
2020
+ destructiveHint: false,
2021
+ idempotentHint: false,
2022
+ openWorldHint: true,
2023
+ },
2024
+ }, async (params) => {
2025
+ try {
2026
+ const client = await getClient();
2027
+ const guild = client.guilds.cache.get(params.guild_id);
2028
+ if (!guild) {
2029
+ return {
2030
+ isError: true,
2031
+ content: [{ type: "text", text: `Guild not found: ${params.guild_id}` }],
2032
+ };
2033
+ }
2034
+ const emoji = await guild.emojis.create({
2035
+ attachment: params.image_url,
2036
+ name: params.name,
2037
+ roles: params.roles,
2038
+ });
2039
+ return {
2040
+ content: [{ type: "text", text: `Created emoji :${emoji.name}: (ID: ${emoji.id})` }],
2041
+ };
2042
+ }
2043
+ catch (error) {
2044
+ return {
2045
+ isError: true,
2046
+ content: [{ type: "text", text: `Error creating emoji: ${error.message}` }],
2047
+ };
2048
+ }
2049
+ });
2050
+ server.registerTool("discord_delete_emoji", {
2051
+ title: "Delete Emoji",
2052
+ description: `Delete a custom emoji from a server.
2053
+
2054
+ Args:
2055
+ - guild_id (string): Discord server/guild ID
2056
+ - emoji_id (string): Discord emoji ID
2057
+
2058
+ Returns:
2059
+ Confirmation of deletion`,
2060
+ inputSchema: DeleteEmojiSchema,
2061
+ annotations: {
2062
+ readOnlyHint: false,
2063
+ destructiveHint: true,
2064
+ idempotentHint: true,
2065
+ openWorldHint: true,
2066
+ },
2067
+ }, async (params) => {
2068
+ try {
2069
+ const client = await getClient();
2070
+ const guild = client.guilds.cache.get(params.guild_id);
2071
+ if (!guild) {
2072
+ return {
2073
+ isError: true,
2074
+ content: [{ type: "text", text: `Guild not found: ${params.guild_id}` }],
2075
+ };
2076
+ }
2077
+ const emoji = guild.emojis.cache.get(params.emoji_id);
2078
+ if (!emoji) {
2079
+ return {
2080
+ isError: true,
2081
+ content: [{ type: "text", text: `Emoji not found: ${params.emoji_id}` }],
2082
+ };
2083
+ }
2084
+ const name = emoji.name;
2085
+ await emoji.delete();
2086
+ return {
2087
+ content: [{ type: "text", text: `Deleted emoji :${name}:` }],
2088
+ };
2089
+ }
2090
+ catch (error) {
2091
+ return {
2092
+ isError: true,
2093
+ content: [{ type: "text", text: `Error deleting emoji: ${error.message}` }],
2094
+ };
2095
+ }
2096
+ });
2097
+ // ============================================================================
2098
+ // INVITE TOOLS
2099
+ // ============================================================================
2100
+ server.registerTool("discord_list_invites", {
2101
+ title: "List Invites",
2102
+ description: `List all active invites in a server.
2103
+
2104
+ Args:
2105
+ - guild_id (string): Discord server/guild ID
2106
+ - response_format ('json' | 'markdown'): Output format
2107
+
2108
+ Returns:
2109
+ List of invites with code, uses, max uses, expiration`,
2110
+ inputSchema: ListInvitesSchema,
2111
+ annotations: {
2112
+ readOnlyHint: true,
2113
+ destructiveHint: false,
2114
+ idempotentHint: true,
2115
+ openWorldHint: true,
2116
+ },
2117
+ }, async (params) => {
2118
+ try {
2119
+ const client = await getClient();
2120
+ const guild = client.guilds.cache.get(params.guild_id);
2121
+ if (!guild) {
2122
+ return {
2123
+ isError: true,
2124
+ content: [{ type: "text", text: `Guild not found: ${params.guild_id}` }],
2125
+ };
2126
+ }
2127
+ const invites = await guild.invites.fetch();
2128
+ const inviteList = invites.map(invite => ({
2129
+ code: invite.code,
2130
+ url: invite.url,
2131
+ channel: invite.channel?.name || 'Unknown',
2132
+ channelId: invite.channel?.id,
2133
+ inviter: invite.inviter?.username || 'Unknown',
2134
+ uses: invite.uses,
2135
+ maxUses: invite.maxUses || 'Unlimited',
2136
+ maxAge: invite.maxAge === 0 ? 'Never' : `${invite.maxAge} seconds`,
2137
+ temporary: invite.temporary,
2138
+ createdAt: invite.createdAt?.toISOString(),
2139
+ expiresAt: invite.expiresAt?.toISOString() || 'Never',
2140
+ }));
2141
+ const result = formatResponse(inviteList, params.response_format, (items) => items.map(i => `**${i.code}** - #${i.channel}\nUses: ${i.uses}/${i.maxUses} | Expires: ${i.expiresAt}\nCreated by: ${i.inviter}`).join('\n\n'));
2142
+ return {
2143
+ content: [{ type: "text", text: truncateIfNeeded(result) }],
2144
+ };
2145
+ }
2146
+ catch (error) {
2147
+ return {
2148
+ isError: true,
2149
+ content: [{ type: "text", text: `Error listing invites: ${error.message}` }],
2150
+ };
2151
+ }
2152
+ });
2153
+ server.registerTool("discord_create_invite", {
2154
+ title: "Create Invite",
2155
+ description: `Create an invite link for a channel.
2156
+
2157
+ Args:
2158
+ - channel_id (string): Discord channel ID
2159
+ - max_age (number): Duration in seconds (0 = never expires, max 604800 = 7 days)
2160
+ - max_uses (number): Max uses (0 = unlimited)
2161
+ - temporary (boolean): Kick members when they disconnect if not assigned a role
2162
+ - unique (boolean): Create new unique invite vs reuse existing
2163
+
2164
+ Returns:
2165
+ Created invite URL`,
2166
+ inputSchema: CreateInviteSchema,
2167
+ annotations: {
2168
+ readOnlyHint: false,
2169
+ destructiveHint: false,
2170
+ idempotentHint: false,
2171
+ openWorldHint: true,
2172
+ },
2173
+ }, async (params) => {
2174
+ try {
2175
+ const client = await getClient();
2176
+ const channel = client.channels.cache.get(params.channel_id);
2177
+ if (!channel || !('createInvite' in channel)) {
2178
+ return {
2179
+ isError: true,
2180
+ content: [{ type: "text", text: `Channel not found or cannot create invites: ${params.channel_id}` }],
2181
+ };
2182
+ }
2183
+ const textChannel = channel;
2184
+ const invite = await textChannel.createInvite({
2185
+ maxAge: params.max_age,
2186
+ maxUses: params.max_uses,
2187
+ temporary: params.temporary,
2188
+ unique: params.unique,
2189
+ });
2190
+ return {
2191
+ content: [{ type: "text", text: `Created invite: ${invite.url}\nCode: ${invite.code}\nMax uses: ${invite.maxUses || 'Unlimited'}\nExpires: ${invite.expiresAt?.toISOString() || 'Never'}` }],
2192
+ };
2193
+ }
2194
+ catch (error) {
2195
+ return {
2196
+ isError: true,
2197
+ content: [{ type: "text", text: `Error creating invite: ${error.message}` }],
2198
+ };
2199
+ }
2200
+ });
2201
+ server.registerTool("discord_delete_invite", {
2202
+ title: "Delete Invite",
2203
+ description: `Delete an invite by its code.
2204
+
2205
+ Args:
2206
+ - invite_code (string): The invite code to delete
2207
+
2208
+ Returns:
2209
+ Confirmation of deletion`,
2210
+ inputSchema: DeleteInviteSchema,
2211
+ annotations: {
2212
+ readOnlyHint: false,
2213
+ destructiveHint: true,
2214
+ idempotentHint: true,
2215
+ openWorldHint: true,
2216
+ },
2217
+ }, async (params) => {
2218
+ try {
2219
+ const client = await getClient();
2220
+ const invite = await client.fetchInvite(params.invite_code).catch(() => null);
2221
+ if (!invite) {
2222
+ return {
2223
+ isError: true,
2224
+ content: [{ type: "text", text: `Invite not found: ${params.invite_code}` }],
2225
+ };
2226
+ }
2227
+ await invite.delete();
2228
+ return {
2229
+ content: [{ type: "text", text: `Deleted invite: ${params.invite_code}` }],
2230
+ };
2231
+ }
2232
+ catch (error) {
2233
+ return {
2234
+ isError: true,
2235
+ content: [{ type: "text", text: `Error deleting invite: ${error.message}` }],
2236
+ };
2237
+ }
2238
+ });
2239
+ // ============================================================================
2240
+ // WEBHOOK TOOLS
2241
+ // ============================================================================
2242
+ server.registerTool("discord_list_webhooks", {
2243
+ title: "List Webhooks",
2244
+ description: `List webhooks in a guild or channel.
2245
+
2246
+ Args:
2247
+ - guild_id (string, optional): List all webhooks in the server
2248
+ - channel_id (string, optional): List webhooks for a specific channel
2249
+ - response_format ('json' | 'markdown'): Output format
2250
+
2251
+ Returns:
2252
+ List of webhooks with name, channel, and URL`,
2253
+ inputSchema: ListWebhooksSchema,
2254
+ annotations: {
2255
+ readOnlyHint: true,
2256
+ destructiveHint: false,
2257
+ idempotentHint: true,
2258
+ openWorldHint: true,
2259
+ },
2260
+ }, async (params) => {
2261
+ try {
2262
+ const client = await getClient();
2263
+ let webhooks;
2264
+ if (params.channel_id) {
2265
+ const channel = client.channels.cache.get(params.channel_id);
2266
+ if (!channel || !('fetchWebhooks' in channel)) {
2267
+ return {
2268
+ isError: true,
2269
+ content: [{ type: "text", text: `Channel not found or doesn't support webhooks: ${params.channel_id}` }],
2270
+ };
2271
+ }
2272
+ webhooks = await channel.fetchWebhooks();
2273
+ }
2274
+ else if (params.guild_id) {
2275
+ const guild = client.guilds.cache.get(params.guild_id);
2276
+ if (!guild) {
2277
+ return {
2278
+ isError: true,
2279
+ content: [{ type: "text", text: `Guild not found: ${params.guild_id}` }],
2280
+ };
2281
+ }
2282
+ webhooks = await guild.fetchWebhooks();
2283
+ }
2284
+ else {
2285
+ return {
2286
+ isError: true,
2287
+ content: [{ type: "text", text: `Must provide either guild_id or channel_id` }],
2288
+ };
2289
+ }
2290
+ const webhookList = webhooks.map(wh => ({
2291
+ id: wh.id,
2292
+ name: wh.name,
2293
+ channel: wh.channel?.id,
2294
+ url: wh.url,
2295
+ owner: wh.owner?.username || 'Unknown',
2296
+ avatar: wh.avatarURL(),
2297
+ }));
2298
+ const result = formatResponse(webhookList, params.response_format, (items) => items.map(w => `**${w.name}** (ID: ${w.id})\nChannel: <#${w.channel}>\nOwner: ${w.owner}`).join('\n\n'));
2299
+ return {
2300
+ content: [{ type: "text", text: truncateIfNeeded(result) }],
2301
+ };
2302
+ }
2303
+ catch (error) {
2304
+ return {
2305
+ isError: true,
2306
+ content: [{ type: "text", text: `Error listing webhooks: ${error.message}` }],
2307
+ };
2308
+ }
2309
+ });
2310
+ server.registerTool("discord_create_webhook", {
2311
+ title: "Create Webhook",
2312
+ description: `Create a webhook in a channel.
2313
+
2314
+ Args:
2315
+ - channel_id (string): Discord channel ID
2316
+ - name (string): Webhook name
2317
+ - avatar_url (string, optional): Avatar image URL
2318
+
2319
+ Returns:
2320
+ Created webhook details including URL`,
2321
+ inputSchema: CreateWebhookSchema,
2322
+ annotations: {
2323
+ readOnlyHint: false,
2324
+ destructiveHint: false,
2325
+ idempotentHint: false,
2326
+ openWorldHint: true,
2327
+ },
2328
+ }, async (params) => {
2329
+ try {
2330
+ const client = await getClient();
2331
+ const channel = client.channels.cache.get(params.channel_id);
2332
+ if (!channel || !('createWebhook' in channel)) {
2333
+ return {
2334
+ isError: true,
2335
+ content: [{ type: "text", text: `Channel not found or doesn't support webhooks: ${params.channel_id}` }],
2336
+ };
2337
+ }
2338
+ const webhook = await channel.createWebhook({
2339
+ name: params.name,
2340
+ avatar: params.avatar_url,
2341
+ });
2342
+ return {
2343
+ content: [{ type: "text", text: `Created webhook "${webhook.name}" (ID: ${webhook.id})\nURL: ${webhook.url}` }],
2344
+ };
2345
+ }
2346
+ catch (error) {
2347
+ return {
2348
+ isError: true,
2349
+ content: [{ type: "text", text: `Error creating webhook: ${error.message}` }],
2350
+ };
2351
+ }
2352
+ });
2353
+ server.registerTool("discord_edit_webhook", {
2354
+ title: "Edit Webhook",
2355
+ description: `Edit an existing webhook.
2356
+
2357
+ Args:
2358
+ - webhook_id (string): Discord webhook ID
2359
+ - name (string, optional): New webhook name
2360
+ - channel_id (string, optional): Move webhook to different channel
2361
+
2362
+ Returns:
2363
+ Updated webhook details`,
2364
+ inputSchema: EditWebhookSchema,
2365
+ annotations: {
2366
+ readOnlyHint: false,
2367
+ destructiveHint: false,
2368
+ idempotentHint: true,
2369
+ openWorldHint: true,
2370
+ },
2371
+ }, async (params) => {
2372
+ try {
2373
+ const client = await getClient();
2374
+ const webhook = await client.fetchWebhook(params.webhook_id).catch(() => null);
2375
+ if (!webhook) {
2376
+ return {
2377
+ isError: true,
2378
+ content: [{ type: "text", text: `Webhook not found: ${params.webhook_id}` }],
2379
+ };
2380
+ }
2381
+ const editOptions = {};
2382
+ if (params.name !== undefined)
2383
+ editOptions.name = params.name;
2384
+ if (params.channel_id !== undefined)
2385
+ editOptions.channel = params.channel_id;
2386
+ await webhook.edit(editOptions);
2387
+ return {
2388
+ content: [{ type: "text", text: `Updated webhook "${webhook.name}" (ID: ${webhook.id})` }],
2389
+ };
2390
+ }
2391
+ catch (error) {
2392
+ return {
2393
+ isError: true,
2394
+ content: [{ type: "text", text: `Error editing webhook: ${error.message}` }],
2395
+ };
2396
+ }
2397
+ });
2398
+ server.registerTool("discord_delete_webhook", {
2399
+ title: "Delete Webhook",
2400
+ description: `Delete a webhook.
2401
+
2402
+ Args:
2403
+ - webhook_id (string): Discord webhook ID
2404
+
2405
+ Returns:
2406
+ Confirmation of deletion`,
2407
+ inputSchema: DeleteWebhookSchema,
2408
+ annotations: {
2409
+ readOnlyHint: false,
2410
+ destructiveHint: true,
2411
+ idempotentHint: true,
2412
+ openWorldHint: true,
2413
+ },
2414
+ }, async (params) => {
2415
+ try {
2416
+ const client = await getClient();
2417
+ const webhook = await client.fetchWebhook(params.webhook_id).catch(() => null);
2418
+ if (!webhook) {
2419
+ return {
2420
+ isError: true,
2421
+ content: [{ type: "text", text: `Webhook not found: ${params.webhook_id}` }],
2422
+ };
2423
+ }
2424
+ const name = webhook.name;
2425
+ await webhook.delete();
2426
+ return {
2427
+ content: [{ type: "text", text: `Deleted webhook: ${name}` }],
2428
+ };
2429
+ }
2430
+ catch (error) {
2431
+ return {
2432
+ isError: true,
2433
+ content: [{ type: "text", text: `Error deleting webhook: ${error.message}` }],
2434
+ };
2435
+ }
2436
+ });
2437
+ // ============================================================================
2438
+ // AUDIT LOG TOOL
2439
+ // ============================================================================
2440
+ server.registerTool("discord_get_audit_log", {
2441
+ title: "Get Audit Log",
2442
+ description: `Get the audit log for a server. Shows who did what actions.
2443
+
2444
+ Common action types:
2445
+ 1=GuildUpdate, 10=ChannelCreate, 11=ChannelUpdate, 12=ChannelDelete,
2446
+ 20=MemberKick, 22=MemberBanAdd, 23=MemberBanRemove, 24=MemberUpdate, 25=MemberRoleUpdate,
2447
+ 30=RoleCreate, 31=RoleUpdate, 32=RoleDelete,
2448
+ 72=MessageDelete, 73=MessageBulkDelete
2449
+
2450
+ Args:
2451
+ - guild_id (string): Discord server/guild ID
2452
+ - user_id (string, optional): Filter by user who performed action
2453
+ - action_type (number, optional): Filter by action type
2454
+ - limit (number): Number of entries (1-100, default 20)
2455
+ - response_format ('json' | 'markdown'): Output format
2456
+
2457
+ Returns:
2458
+ Audit log entries with action, user, target, and changes`,
2459
+ inputSchema: GetAuditLogSchema,
2460
+ annotations: {
2461
+ readOnlyHint: true,
2462
+ destructiveHint: false,
2463
+ idempotentHint: true,
2464
+ openWorldHint: true,
2465
+ },
2466
+ }, async (params) => {
2467
+ try {
2468
+ const client = await getClient();
2469
+ const guild = client.guilds.cache.get(params.guild_id);
2470
+ if (!guild) {
2471
+ return {
2472
+ isError: true,
2473
+ content: [{ type: "text", text: `Guild not found: ${params.guild_id}` }],
2474
+ };
2475
+ }
2476
+ const fetchOptions = {
2477
+ limit: params.limit,
2478
+ };
2479
+ if (params.user_id)
2480
+ fetchOptions.user = params.user_id;
2481
+ if (params.action_type !== undefined)
2482
+ fetchOptions.type = params.action_type;
2483
+ const auditLogs = await guild.fetchAuditLogs(fetchOptions);
2484
+ const entries = auditLogs.entries.map(entry => ({
2485
+ id: entry.id,
2486
+ action: entry.action,
2487
+ actionType: entry.actionType,
2488
+ executor: entry.executor?.username || 'Unknown',
2489
+ executorId: entry.executor?.id,
2490
+ target: entry.target?.toString() || 'Unknown',
2491
+ reason: entry.reason || 'No reason',
2492
+ createdAt: entry.createdAt.toISOString(),
2493
+ changes: entry.changes.map(c => ({
2494
+ key: c.key,
2495
+ old: c.old,
2496
+ new: c.new,
2497
+ })),
2498
+ }));
2499
+ const result = formatResponse(entries, params.response_format, (items) => items.map(e => `**Action ${e.action}** by ${e.executor}\nTarget: ${e.target}\nReason: ${e.reason}\nTime: ${e.createdAt}`).join('\n\n'));
2500
+ return {
2501
+ content: [{ type: "text", text: truncateIfNeeded(result) }],
2502
+ };
2503
+ }
2504
+ catch (error) {
2505
+ return {
2506
+ isError: true,
2507
+ content: [{ type: "text", text: `Error fetching audit log: ${error.message}` }],
2508
+ };
2509
+ }
2510
+ });
2511
+ // ============================================================================
2512
+ // PRUNE MEMBERS TOOL
2513
+ // ============================================================================
2514
+ server.registerTool("discord_prune_members", {
2515
+ title: "Prune Members",
2516
+ description: `Remove inactive members from the server.
2517
+
2518
+ Args:
2519
+ - guild_id (string): Discord server/guild ID
2520
+ - days (number): Days of inactivity required (1-30)
2521
+ - include_roles (string[], optional): Role IDs to include in prune (by default only members without roles)
2522
+ - dry_run (boolean): If true, returns count without actually pruning (default true)
2523
+
2524
+ Returns:
2525
+ Number of members pruned (or would be pruned if dry_run)`,
2526
+ inputSchema: PruneMembersSchema,
2527
+ annotations: {
2528
+ readOnlyHint: false,
2529
+ destructiveHint: true,
2530
+ idempotentHint: false,
2531
+ openWorldHint: true,
2532
+ },
2533
+ }, async (params) => {
2534
+ try {
2535
+ const client = await getClient();
2536
+ const guild = client.guilds.cache.get(params.guild_id);
2537
+ if (!guild) {
2538
+ return {
2539
+ isError: true,
2540
+ content: [{ type: "text", text: `Guild not found: ${params.guild_id}` }],
2541
+ };
2542
+ }
2543
+ if (params.dry_run) {
2544
+ const count = await guild.members.prune({
2545
+ days: params.days,
2546
+ roles: params.include_roles,
2547
+ dry: true,
2548
+ });
2549
+ return {
2550
+ content: [{ type: "text", text: `Dry run: ${count} members would be pruned (inactive for ${params.days}+ days)` }],
2551
+ };
2552
+ }
2553
+ const pruned = await guild.members.prune({
2554
+ days: params.days,
2555
+ roles: params.include_roles,
2556
+ dry: false,
2557
+ });
2558
+ return {
2559
+ content: [{ type: "text", text: `Pruned ${pruned} members (inactive for ${params.days}+ days)` }],
2560
+ };
2561
+ }
2562
+ catch (error) {
2563
+ return {
2564
+ isError: true,
2565
+ content: [{ type: "text", text: `Error pruning members: ${error.message}` }],
2566
+ };
2567
+ }
2568
+ });
2569
+ // ============================================================================
2570
+ // STICKER TOOLS
2571
+ // ============================================================================
2572
+ server.registerTool("discord_list_stickers", {
2573
+ title: "List Stickers",
2574
+ description: `List all custom stickers in a server.
2575
+
2576
+ Args:
2577
+ - guild_id (string): Discord server/guild ID
2578
+ - response_format ('json' | 'markdown'): Output format
2579
+
2580
+ Returns:
2581
+ List of stickers with name, description, and format`,
2582
+ inputSchema: ListStickersSchema,
2583
+ annotations: {
2584
+ readOnlyHint: true,
2585
+ destructiveHint: false,
2586
+ idempotentHint: true,
2587
+ openWorldHint: true,
2588
+ },
2589
+ }, async (params) => {
2590
+ try {
2591
+ const client = await getClient();
2592
+ const guild = client.guilds.cache.get(params.guild_id);
2593
+ if (!guild) {
2594
+ return {
2595
+ isError: true,
2596
+ content: [{ type: "text", text: `Guild not found: ${params.guild_id}` }],
2597
+ };
2598
+ }
2599
+ const stickers = await guild.stickers.fetch();
2600
+ const stickerList = stickers.map(sticker => ({
2601
+ id: sticker.id,
2602
+ name: sticker.name,
2603
+ description: sticker.description,
2604
+ tags: sticker.tags,
2605
+ format: sticker.format,
2606
+ available: sticker.available,
2607
+ url: sticker.url,
2608
+ }));
2609
+ const result = formatResponse(stickerList, params.response_format, (items) => items.map(s => `**${s.name}** (ID: ${s.id})\nDescription: ${s.description || 'None'}\nTags: ${s.tags || 'None'}`).join('\n\n'));
2610
+ return {
2611
+ content: [{ type: "text", text: truncateIfNeeded(result) }],
2612
+ };
2613
+ }
2614
+ catch (error) {
2615
+ return {
2616
+ isError: true,
2617
+ content: [{ type: "text", text: `Error listing stickers: ${error.message}` }],
2618
+ };
2619
+ }
2620
+ });
2621
+ server.registerTool("discord_delete_sticker", {
2622
+ title: "Delete Sticker",
2623
+ description: `Delete a custom sticker from a server.
2624
+
2625
+ Args:
2626
+ - guild_id (string): Discord server/guild ID
2627
+ - sticker_id (string): Discord sticker ID
2628
+
2629
+ Returns:
2630
+ Confirmation of deletion`,
2631
+ inputSchema: DeleteStickerSchema,
2632
+ annotations: {
2633
+ readOnlyHint: false,
2634
+ destructiveHint: true,
2635
+ idempotentHint: true,
2636
+ openWorldHint: true,
2637
+ },
2638
+ }, async (params) => {
2639
+ try {
2640
+ const client = await getClient();
2641
+ const guild = client.guilds.cache.get(params.guild_id);
2642
+ if (!guild) {
2643
+ return {
2644
+ isError: true,
2645
+ content: [{ type: "text", text: `Guild not found: ${params.guild_id}` }],
2646
+ };
2647
+ }
2648
+ const sticker = await guild.stickers.fetch(params.sticker_id).catch(() => null);
2649
+ if (!sticker) {
2650
+ return {
2651
+ isError: true,
2652
+ content: [{ type: "text", text: `Sticker not found: ${params.sticker_id}` }],
2653
+ };
2654
+ }
2655
+ const name = sticker.name;
2656
+ await sticker.delete();
2657
+ return {
2658
+ content: [{ type: "text", text: `Deleted sticker: ${name}` }],
2659
+ };
2660
+ }
2661
+ catch (error) {
2662
+ return {
2663
+ isError: true,
2664
+ content: [{ type: "text", text: `Error deleting sticker: ${error.message}` }],
2665
+ };
2666
+ }
2667
+ });
2668
+ // ============================================================================
2669
+ // SCHEDULED EVENT TOOLS
2670
+ // ============================================================================
2671
+ server.registerTool("discord_list_events", {
2672
+ title: "List Scheduled Events",
2673
+ description: `List all scheduled events in a server.
2674
+
2675
+ Args:
2676
+ - guild_id (string): Discord server/guild ID
2677
+ - response_format ('json' | 'markdown'): Output format
2678
+
2679
+ Returns:
2680
+ List of events with name, time, location, and status`,
2681
+ inputSchema: ListEventsSchema,
2682
+ annotations: {
2683
+ readOnlyHint: true,
2684
+ destructiveHint: false,
2685
+ idempotentHint: true,
2686
+ openWorldHint: true,
2687
+ },
2688
+ }, async (params) => {
2689
+ try {
2690
+ const client = await getClient();
2691
+ const guild = client.guilds.cache.get(params.guild_id);
2692
+ if (!guild) {
2693
+ return {
2694
+ isError: true,
2695
+ content: [{ type: "text", text: `Guild not found: ${params.guild_id}` }],
2696
+ };
2697
+ }
2698
+ const events = await guild.scheduledEvents.fetch();
2699
+ const eventList = events.map(event => ({
2700
+ id: event.id,
2701
+ name: event.name,
2702
+ description: event.description,
2703
+ status: event.status,
2704
+ entityType: event.entityType,
2705
+ channel: event.channel?.name,
2706
+ channelId: event.channelId,
2707
+ location: event.entityMetadata?.location,
2708
+ scheduledStartTime: event.scheduledStartAt?.toISOString(),
2709
+ scheduledEndTime: event.scheduledEndAt?.toISOString(),
2710
+ userCount: event.userCount,
2711
+ creator: event.creator?.username,
2712
+ }));
2713
+ const result = formatResponse(eventList, params.response_format, (items) => items.map(e => `**${e.name}** (ID: ${e.id})\nStatus: ${e.status}\nStart: ${e.scheduledStartTime}\nLocation: ${e.location || e.channel || 'TBD'}\nAttending: ${e.userCount || 0}`).join('\n\n'));
2714
+ return {
2715
+ content: [{ type: "text", text: truncateIfNeeded(result) }],
2716
+ };
2717
+ }
2718
+ catch (error) {
2719
+ return {
2720
+ isError: true,
2721
+ content: [{ type: "text", text: `Error listing events: ${error.message}` }],
2722
+ };
2723
+ }
2724
+ });
2725
+ server.registerTool("discord_create_event", {
2726
+ title: "Create Scheduled Event",
2727
+ description: `Create a scheduled event in a server.
2728
+
2729
+ Args:
2730
+ - guild_id (string): Discord server/guild ID
2731
+ - name (string): Event name
2732
+ - description (string, optional): Event description
2733
+ - scheduled_start_time (string): ISO8601 timestamp for start
2734
+ - scheduled_end_time (string, optional): ISO8601 timestamp for end
2735
+ - entity_type ('stage' | 'voice' | 'external'): Type of event
2736
+ - channel_id (string, optional): Channel ID for stage/voice events
2737
+ - location (string, optional): Location for external events
2738
+
2739
+ Returns:
2740
+ Created event details`,
2741
+ inputSchema: CreateEventSchema,
2742
+ annotations: {
2743
+ readOnlyHint: false,
2744
+ destructiveHint: false,
2745
+ idempotentHint: false,
2746
+ openWorldHint: true,
2747
+ },
2748
+ }, async (params) => {
2749
+ try {
2750
+ const client = await getClient();
2751
+ const guild = client.guilds.cache.get(params.guild_id);
2752
+ if (!guild) {
2753
+ return {
2754
+ isError: true,
2755
+ content: [{ type: "text", text: `Guild not found: ${params.guild_id}` }],
2756
+ };
2757
+ }
2758
+ const entityTypeMap = {
2759
+ stage: GuildScheduledEventEntityType.StageInstance,
2760
+ voice: GuildScheduledEventEntityType.Voice,
2761
+ external: GuildScheduledEventEntityType.External,
2762
+ };
2763
+ const createOptions = {
2764
+ name: params.name,
2765
+ scheduledStartTime: new Date(params.scheduled_start_time),
2766
+ privacyLevel: GuildScheduledEventPrivacyLevel.GuildOnly,
2767
+ entityType: entityTypeMap[params.entity_type],
2768
+ };
2769
+ if (params.description)
2770
+ createOptions.description = params.description;
2771
+ if (params.scheduled_end_time)
2772
+ createOptions.scheduledEndTime = new Date(params.scheduled_end_time);
2773
+ if (params.channel_id)
2774
+ createOptions.channel = params.channel_id;
2775
+ if (params.location)
2776
+ createOptions.entityMetadata = { location: params.location };
2777
+ const event = await guild.scheduledEvents.create(createOptions);
2778
+ return {
2779
+ content: [{ type: "text", text: `Created event "${event.name}" (ID: ${event.id})\nStart: ${event.scheduledStartAt?.toISOString()}` }],
2780
+ };
2781
+ }
2782
+ catch (error) {
2783
+ return {
2784
+ isError: true,
2785
+ content: [{ type: "text", text: `Error creating event: ${error.message}` }],
2786
+ };
2787
+ }
2788
+ });
2789
+ server.registerTool("discord_delete_event", {
2790
+ title: "Delete Scheduled Event",
2791
+ description: `Delete a scheduled event from a server.
2792
+
2793
+ Args:
2794
+ - guild_id (string): Discord server/guild ID
2795
+ - event_id (string): Discord scheduled event ID
2796
+
2797
+ Returns:
2798
+ Confirmation of deletion`,
2799
+ inputSchema: DeleteEventSchema,
2800
+ annotations: {
2801
+ readOnlyHint: false,
2802
+ destructiveHint: true,
2803
+ idempotentHint: true,
2804
+ openWorldHint: true,
2805
+ },
2806
+ }, async (params) => {
2807
+ try {
2808
+ const client = await getClient();
2809
+ const guild = client.guilds.cache.get(params.guild_id);
2810
+ if (!guild) {
2811
+ return {
2812
+ isError: true,
2813
+ content: [{ type: "text", text: `Guild not found: ${params.guild_id}` }],
2814
+ };
2815
+ }
2816
+ const event = await guild.scheduledEvents.fetch(params.event_id).catch(() => null);
2817
+ if (!event) {
2818
+ return {
2819
+ isError: true,
2820
+ content: [{ type: "text", text: `Event not found: ${params.event_id}` }],
2821
+ };
2822
+ }
2823
+ const name = event.name;
2824
+ await event.delete();
2825
+ return {
2826
+ content: [{ type: "text", text: `Deleted event: ${name}` }],
2827
+ };
2828
+ }
2829
+ catch (error) {
2830
+ return {
2831
+ isError: true,
2832
+ content: [{ type: "text", text: `Error deleting event: ${error.message}` }],
2833
+ };
2834
+ }
2835
+ });
2836
+ // ============================================================================
2837
+ // SERVER STARTUP
2838
+ // ============================================================================
2839
+ async function runStdio() {
2840
+ const transport = new StdioServerTransport();
2841
+ await server.connect(transport);
2842
+ console.error("Discord MCP server running on stdio");
2843
+ }
2844
+ async function runHTTP() {
2845
+ const app = express();
2846
+ app.use(express.json());
2847
+ app.post("/mcp", async (req, res) => {
2848
+ const transport = new StreamableHTTPServerTransport({
2849
+ sessionIdGenerator: undefined,
2850
+ enableJsonResponse: true,
2851
+ });
2852
+ res.on("close", () => transport.close());
2853
+ await server.connect(transport);
2854
+ await transport.handleRequest(req, res, req.body);
2855
+ });
2856
+ const port = parseInt(process.env.PORT || "3000");
2857
+ app.listen(port, () => {
2858
+ console.error(`Discord MCP server running on http://localhost:${port}/mcp`);
2859
+ });
2860
+ }
2861
+ // Choose transport based on environment
2862
+ const transport = process.env.TRANSPORT || "stdio";
2863
+ if (transport === "http") {
2864
+ runHTTP().catch((error) => {
2865
+ console.error("Server error:", error);
2866
+ process.exit(1);
2867
+ });
2868
+ }
2869
+ else {
2870
+ runStdio().catch((error) => {
2871
+ console.error("Server error:", error);
2872
+ process.exit(1);
2873
+ });
2874
+ }