@quadslab.io/discord-mcp 1.0.0
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/LICENSE +21 -0
- package/README.md +474 -0
- package/dist/discord-client.d.ts +57 -0
- package/dist/discord-client.js +188 -0
- package/dist/discord-client.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +135 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp-server.d.ts +15 -0
- package/dist/mcp-server.js +65 -0
- package/dist/mcp-server.js.map +1 -0
- package/dist/tools/automod.d.ts +6 -0
- package/dist/tools/automod.js +376 -0
- package/dist/tools/automod.js.map +1 -0
- package/dist/tools/channels.d.ts +6 -0
- package/dist/tools/channels.js +840 -0
- package/dist/tools/channels.js.map +1 -0
- package/dist/tools/emojis.d.ts +6 -0
- package/dist/tools/emojis.js +295 -0
- package/dist/tools/emojis.js.map +1 -0
- package/dist/tools/events.d.ts +6 -0
- package/dist/tools/events.js +304 -0
- package/dist/tools/events.js.map +1 -0
- package/dist/tools/forums.d.ts +6 -0
- package/dist/tools/forums.js +276 -0
- package/dist/tools/forums.js.map +1 -0
- package/dist/tools/guild.d.ts +6 -0
- package/dist/tools/guild.js +88 -0
- package/dist/tools/guild.js.map +1 -0
- package/dist/tools/index.d.ts +13 -0
- package/dist/tools/index.js +142 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/members.d.ts +6 -0
- package/dist/tools/members.js +628 -0
- package/dist/tools/members.js.map +1 -0
- package/dist/tools/messages.d.ts +6 -0
- package/dist/tools/messages.js +688 -0
- package/dist/tools/messages.js.map +1 -0
- package/dist/tools/reactions.d.ts +6 -0
- package/dist/tools/reactions.js +137 -0
- package/dist/tools/reactions.js.map +1 -0
- package/dist/tools/roles.d.ts +6 -0
- package/dist/tools/roles.js +469 -0
- package/dist/tools/roles.js.map +1 -0
- package/dist/tools/server.d.ts +6 -0
- package/dist/tools/server.js +607 -0
- package/dist/tools/server.js.map +1 -0
- package/dist/tools/stage.d.ts +6 -0
- package/dist/tools/stage.js +153 -0
- package/dist/tools/stage.js.map +1 -0
- package/dist/tools/threads.d.ts +6 -0
- package/dist/tools/threads.js +331 -0
- package/dist/tools/threads.js.map +1 -0
- package/dist/tools/utils.d.ts +21 -0
- package/dist/tools/utils.js +315 -0
- package/dist/tools/utils.js.map +1 -0
- package/dist/tools/webhooks.d.ts +6 -0
- package/dist/tools/webhooks.js +195 -0
- package/dist/tools/webhooks.js.map +1 -0
- package/package.json +48 -0
|
@@ -0,0 +1,840 @@
|
|
|
1
|
+
import { getGuild, refreshServerCache } from '../discord-client.js';
|
|
2
|
+
import { ChannelType, PermissionFlagsBits, PermissionsBitField, OverwriteType } from 'discord.js';
|
|
3
|
+
import { smartFindChannel, smartFindCategory, smartFindRole, smartFindMember, smartFindTextChannel } from './utils.js';
|
|
4
|
+
/**
|
|
5
|
+
* Channel management tools
|
|
6
|
+
*/
|
|
7
|
+
export const channelTools = [
|
|
8
|
+
{
|
|
9
|
+
name: 'list_channels',
|
|
10
|
+
description: 'List all channels in the Discord server, organized by category. Returns channel names, IDs, types, and categories.',
|
|
11
|
+
inputSchema: {
|
|
12
|
+
type: 'object',
|
|
13
|
+
properties: {},
|
|
14
|
+
required: [],
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
name: 'create_text_channel',
|
|
19
|
+
description: 'Create a new text channel in the Discord server. Category name is fuzzy-matched.',
|
|
20
|
+
inputSchema: {
|
|
21
|
+
type: 'object',
|
|
22
|
+
properties: {
|
|
23
|
+
name: {
|
|
24
|
+
type: 'string',
|
|
25
|
+
description: 'The name for the new text channel (will be lowercased and spaces replaced with hyphens)',
|
|
26
|
+
},
|
|
27
|
+
category: {
|
|
28
|
+
type: 'string',
|
|
29
|
+
description: 'The category name or ID to place the channel in (fuzzy matched, optional)',
|
|
30
|
+
},
|
|
31
|
+
topic: {
|
|
32
|
+
type: 'string',
|
|
33
|
+
description: 'The topic/description for the channel (optional)',
|
|
34
|
+
},
|
|
35
|
+
nsfw: {
|
|
36
|
+
type: 'boolean',
|
|
37
|
+
description: 'Whether the channel is NSFW (default: false)',
|
|
38
|
+
},
|
|
39
|
+
reason: {
|
|
40
|
+
type: 'string',
|
|
41
|
+
description: 'The reason for creating this channel (shown in audit log)',
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
required: ['name'],
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: 'create_voice_channel',
|
|
49
|
+
description: 'Create a new voice channel in the Discord server. Category name is fuzzy-matched.',
|
|
50
|
+
inputSchema: {
|
|
51
|
+
type: 'object',
|
|
52
|
+
properties: {
|
|
53
|
+
name: {
|
|
54
|
+
type: 'string',
|
|
55
|
+
description: 'The name for the new voice channel',
|
|
56
|
+
},
|
|
57
|
+
category: {
|
|
58
|
+
type: 'string',
|
|
59
|
+
description: 'The category name or ID to place the channel in (fuzzy matched, optional)',
|
|
60
|
+
},
|
|
61
|
+
userLimit: {
|
|
62
|
+
type: 'number',
|
|
63
|
+
description: 'Maximum number of users allowed in the voice channel (0 for unlimited)',
|
|
64
|
+
},
|
|
65
|
+
bitrate: {
|
|
66
|
+
type: 'number',
|
|
67
|
+
description: 'The bitrate of the voice channel in bits per second (e.g., 64000)',
|
|
68
|
+
},
|
|
69
|
+
reason: {
|
|
70
|
+
type: 'string',
|
|
71
|
+
description: 'The reason for creating this channel (shown in audit log)',
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
required: ['name'],
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
name: 'create_category',
|
|
79
|
+
description: 'Create a new category (channel group) in the Discord server.',
|
|
80
|
+
inputSchema: {
|
|
81
|
+
type: 'object',
|
|
82
|
+
properties: {
|
|
83
|
+
name: {
|
|
84
|
+
type: 'string',
|
|
85
|
+
description: 'The name for the new category',
|
|
86
|
+
},
|
|
87
|
+
reason: {
|
|
88
|
+
type: 'string',
|
|
89
|
+
description: 'The reason for creating this category (shown in audit log)',
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
required: ['name'],
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
name: 'delete_channel',
|
|
97
|
+
description: 'Delete a channel from the Discord server. Channel name is fuzzy-matched.',
|
|
98
|
+
inputSchema: {
|
|
99
|
+
type: 'object',
|
|
100
|
+
properties: {
|
|
101
|
+
channel: {
|
|
102
|
+
type: 'string',
|
|
103
|
+
description: 'The channel name or ID to delete (fuzzy matched)',
|
|
104
|
+
},
|
|
105
|
+
reason: {
|
|
106
|
+
type: 'string',
|
|
107
|
+
description: 'The reason for deleting this channel (shown in audit log)',
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
required: ['channel'],
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
name: 'set_channel_permissions',
|
|
115
|
+
description: 'Set permission overwrites for a role or user on a channel. Channel, role, and member names are fuzzy-matched.',
|
|
116
|
+
inputSchema: {
|
|
117
|
+
type: 'object',
|
|
118
|
+
properties: {
|
|
119
|
+
channel: {
|
|
120
|
+
type: 'string',
|
|
121
|
+
description: 'The channel name or ID (fuzzy matched)',
|
|
122
|
+
},
|
|
123
|
+
target: {
|
|
124
|
+
type: 'string',
|
|
125
|
+
description: 'The role or member name/ID to set permissions for (fuzzy matched)',
|
|
126
|
+
},
|
|
127
|
+
targetType: {
|
|
128
|
+
type: 'string',
|
|
129
|
+
description: 'Whether the target is a "role" or "member"',
|
|
130
|
+
enum: ['role', 'member'],
|
|
131
|
+
},
|
|
132
|
+
allow: {
|
|
133
|
+
type: 'array',
|
|
134
|
+
description: 'Permission names to allow (e.g., ["SendMessages", "ViewChannel", "AddReactions"])',
|
|
135
|
+
items: { type: 'string' },
|
|
136
|
+
},
|
|
137
|
+
deny: {
|
|
138
|
+
type: 'array',
|
|
139
|
+
description: 'Permission names to deny (e.g., ["SendMessages", "ManageMessages"])',
|
|
140
|
+
items: { type: 'string' },
|
|
141
|
+
},
|
|
142
|
+
reason: {
|
|
143
|
+
type: 'string',
|
|
144
|
+
description: 'The reason for this change (shown in audit log)',
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
required: ['channel', 'target', 'targetType'],
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
name: 'view_channel_permissions',
|
|
152
|
+
description: 'View all permission overwrites on a channel, showing which roles/users have specific allows and denies.',
|
|
153
|
+
inputSchema: {
|
|
154
|
+
type: 'object',
|
|
155
|
+
properties: {
|
|
156
|
+
channel: {
|
|
157
|
+
type: 'string',
|
|
158
|
+
description: 'The channel name or ID (fuzzy matched)',
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
required: ['channel'],
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
name: 'lock_channel',
|
|
166
|
+
description: 'Lock a channel by denying SendMessages for @everyone. Quick shortcut for permission management.',
|
|
167
|
+
inputSchema: {
|
|
168
|
+
type: 'object',
|
|
169
|
+
properties: {
|
|
170
|
+
channel: {
|
|
171
|
+
type: 'string',
|
|
172
|
+
description: 'The channel name or ID (fuzzy matched)',
|
|
173
|
+
},
|
|
174
|
+
reason: {
|
|
175
|
+
type: 'string',
|
|
176
|
+
description: 'The reason for locking (shown in audit log)',
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
required: ['channel'],
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
name: 'unlock_channel',
|
|
184
|
+
description: 'Unlock a channel by removing the SendMessages deny for @everyone.',
|
|
185
|
+
inputSchema: {
|
|
186
|
+
type: 'object',
|
|
187
|
+
properties: {
|
|
188
|
+
channel: {
|
|
189
|
+
type: 'string',
|
|
190
|
+
description: 'The channel name or ID (fuzzy matched)',
|
|
191
|
+
},
|
|
192
|
+
reason: {
|
|
193
|
+
type: 'string',
|
|
194
|
+
description: 'The reason for unlocking (shown in audit log)',
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
required: ['channel'],
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
name: 'set_slowmode',
|
|
202
|
+
description: 'Set the slowmode (rate limit) on a text channel. Members must wait this many seconds between messages.',
|
|
203
|
+
inputSchema: {
|
|
204
|
+
type: 'object',
|
|
205
|
+
properties: {
|
|
206
|
+
channel: {
|
|
207
|
+
type: 'string',
|
|
208
|
+
description: 'The channel name or ID (fuzzy matched)',
|
|
209
|
+
},
|
|
210
|
+
seconds: {
|
|
211
|
+
type: 'number',
|
|
212
|
+
description: 'Slowmode duration in seconds (0 to disable, max 21600 = 6 hours)',
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
required: ['channel', 'seconds'],
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
name: 'create_forum_channel',
|
|
220
|
+
description: 'Create a new forum channel in the Discord server. Category name is fuzzy-matched.',
|
|
221
|
+
inputSchema: {
|
|
222
|
+
type: 'object',
|
|
223
|
+
properties: {
|
|
224
|
+
name: {
|
|
225
|
+
type: 'string',
|
|
226
|
+
description: 'The name for the new forum channel',
|
|
227
|
+
},
|
|
228
|
+
category: {
|
|
229
|
+
type: 'string',
|
|
230
|
+
description: 'The category name or ID to place the channel in (fuzzy matched, optional)',
|
|
231
|
+
},
|
|
232
|
+
topic: {
|
|
233
|
+
type: 'string',
|
|
234
|
+
description: 'The guidelines/topic for the forum channel (optional)',
|
|
235
|
+
},
|
|
236
|
+
reason: {
|
|
237
|
+
type: 'string',
|
|
238
|
+
description: 'The reason for creating this channel (shown in audit log)',
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
required: ['name'],
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
name: 'reorder_channels',
|
|
246
|
+
description: 'Reorder channels within a category by providing the desired order.',
|
|
247
|
+
inputSchema: {
|
|
248
|
+
type: 'object',
|
|
249
|
+
properties: {
|
|
250
|
+
category: {
|
|
251
|
+
type: 'string',
|
|
252
|
+
description: 'The category name or ID containing the channels (fuzzy matched)',
|
|
253
|
+
},
|
|
254
|
+
channelOrder: {
|
|
255
|
+
type: 'array',
|
|
256
|
+
description: 'Array of channel names or IDs in the desired order (top to bottom)',
|
|
257
|
+
items: { type: 'string' },
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
required: ['category', 'channelOrder'],
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
name: 'set_voice_region',
|
|
265
|
+
description: 'Set the voice region for a voice channel. Use null/empty to set to automatic.',
|
|
266
|
+
inputSchema: {
|
|
267
|
+
type: 'object',
|
|
268
|
+
properties: {
|
|
269
|
+
channel: {
|
|
270
|
+
type: 'string',
|
|
271
|
+
description: 'The voice channel name or ID (fuzzy matched)',
|
|
272
|
+
},
|
|
273
|
+
region: {
|
|
274
|
+
type: 'string',
|
|
275
|
+
description: 'The voice region ID (e.g., "us-east", "europe", "brazil", "japan"). Use "auto" for automatic.',
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
required: ['channel', 'region'],
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
name: 'follow_announcement_channel',
|
|
283
|
+
description: 'Follow an announcement channel so its published messages are cross-posted to a target channel in this server.',
|
|
284
|
+
inputSchema: {
|
|
285
|
+
type: 'object',
|
|
286
|
+
properties: {
|
|
287
|
+
announcementChannel: {
|
|
288
|
+
type: 'string',
|
|
289
|
+
description: 'The announcement channel name or ID to follow (fuzzy matched)',
|
|
290
|
+
},
|
|
291
|
+
targetChannel: {
|
|
292
|
+
type: 'string',
|
|
293
|
+
description: 'The text channel name or ID where announcements will be posted (fuzzy matched)',
|
|
294
|
+
},
|
|
295
|
+
},
|
|
296
|
+
required: ['announcementChannel', 'targetChannel'],
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
name: 'clone_channel',
|
|
301
|
+
description: 'Clone a channel with all its permissions and settings. Optionally give it a new name.',
|
|
302
|
+
inputSchema: {
|
|
303
|
+
type: 'object',
|
|
304
|
+
properties: {
|
|
305
|
+
channel: {
|
|
306
|
+
type: 'string',
|
|
307
|
+
description: 'The channel name or ID to clone (fuzzy matched)',
|
|
308
|
+
},
|
|
309
|
+
name: {
|
|
310
|
+
type: 'string',
|
|
311
|
+
description: 'New name for the cloned channel (optional, defaults to original name)',
|
|
312
|
+
},
|
|
313
|
+
reason: {
|
|
314
|
+
type: 'string',
|
|
315
|
+
description: 'The reason for cloning (shown in audit log)',
|
|
316
|
+
},
|
|
317
|
+
},
|
|
318
|
+
required: ['channel'],
|
|
319
|
+
},
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
name: 'modify_channel',
|
|
323
|
+
description: 'Modify an existing channel\'s properties such as name, topic, or category. Channel and category names are fuzzy-matched.',
|
|
324
|
+
inputSchema: {
|
|
325
|
+
type: 'object',
|
|
326
|
+
properties: {
|
|
327
|
+
channel: {
|
|
328
|
+
type: 'string',
|
|
329
|
+
description: 'The channel name or ID to modify (fuzzy matched)',
|
|
330
|
+
},
|
|
331
|
+
name: {
|
|
332
|
+
type: 'string',
|
|
333
|
+
description: 'New name for the channel',
|
|
334
|
+
},
|
|
335
|
+
topic: {
|
|
336
|
+
type: 'string',
|
|
337
|
+
description: 'New topic/description for the channel (text channels only)',
|
|
338
|
+
},
|
|
339
|
+
category: {
|
|
340
|
+
type: 'string',
|
|
341
|
+
description: 'Category name or ID to move the channel to (fuzzy matched, use "none" to remove from category)',
|
|
342
|
+
},
|
|
343
|
+
nsfw: {
|
|
344
|
+
type: 'boolean',
|
|
345
|
+
description: 'Whether the channel is NSFW (text channels only)',
|
|
346
|
+
},
|
|
347
|
+
userLimit: {
|
|
348
|
+
type: 'number',
|
|
349
|
+
description: 'Maximum users in voice channel (voice channels only)',
|
|
350
|
+
},
|
|
351
|
+
position: {
|
|
352
|
+
type: 'number',
|
|
353
|
+
description: 'New position for the channel',
|
|
354
|
+
},
|
|
355
|
+
reason: {
|
|
356
|
+
type: 'string',
|
|
357
|
+
description: 'The reason for modifying this channel (shown in audit log)',
|
|
358
|
+
},
|
|
359
|
+
},
|
|
360
|
+
required: ['channel'],
|
|
361
|
+
},
|
|
362
|
+
},
|
|
363
|
+
];
|
|
364
|
+
export async function executeChannelTool(name, args) {
|
|
365
|
+
switch (name) {
|
|
366
|
+
case 'list_channels':
|
|
367
|
+
return await listChannels();
|
|
368
|
+
case 'create_text_channel':
|
|
369
|
+
return await createTextChannel(args);
|
|
370
|
+
case 'create_voice_channel':
|
|
371
|
+
return await createVoiceChannel(args);
|
|
372
|
+
case 'create_category':
|
|
373
|
+
return await createCategory(args);
|
|
374
|
+
case 'delete_channel':
|
|
375
|
+
return await deleteChannel(args);
|
|
376
|
+
case 'modify_channel':
|
|
377
|
+
return await modifyChannel(args);
|
|
378
|
+
case 'set_channel_permissions':
|
|
379
|
+
return await setChannelPermissions(args);
|
|
380
|
+
case 'view_channel_permissions':
|
|
381
|
+
return await viewChannelPermissions(args);
|
|
382
|
+
case 'lock_channel':
|
|
383
|
+
return await lockChannel(args);
|
|
384
|
+
case 'unlock_channel':
|
|
385
|
+
return await unlockChannel(args);
|
|
386
|
+
case 'set_slowmode':
|
|
387
|
+
return await setSlowmode(args);
|
|
388
|
+
case 'clone_channel':
|
|
389
|
+
return await cloneChannel(args);
|
|
390
|
+
case 'create_forum_channel':
|
|
391
|
+
return await createForumChannel(args);
|
|
392
|
+
case 'reorder_channels':
|
|
393
|
+
return await reorderChannels(args);
|
|
394
|
+
case 'set_voice_region':
|
|
395
|
+
return await setVoiceRegion(args);
|
|
396
|
+
case 'follow_announcement_channel':
|
|
397
|
+
return await followAnnouncementChannel(args);
|
|
398
|
+
default:
|
|
399
|
+
throw new Error(`Unknown channel tool: ${name}`);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
function getChannelTypeName(type) {
|
|
403
|
+
switch (type) {
|
|
404
|
+
case ChannelType.GuildText: return 'text';
|
|
405
|
+
case ChannelType.GuildVoice: return 'voice';
|
|
406
|
+
case ChannelType.GuildCategory: return 'category';
|
|
407
|
+
case ChannelType.GuildAnnouncement: return 'announcement';
|
|
408
|
+
case ChannelType.GuildStageVoice: return 'stage';
|
|
409
|
+
case ChannelType.GuildForum: return 'forum';
|
|
410
|
+
default: return 'unknown';
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
async function listChannels() {
|
|
414
|
+
const guild = await getGuild();
|
|
415
|
+
// Get categories
|
|
416
|
+
const categories = guild.channels.cache
|
|
417
|
+
.filter(c => c.type === ChannelType.GuildCategory);
|
|
418
|
+
// Organize channels by category
|
|
419
|
+
const organized = {};
|
|
420
|
+
// Add uncategorized section
|
|
421
|
+
organized['uncategorized'] = {
|
|
422
|
+
id: 'none',
|
|
423
|
+
name: 'Uncategorized',
|
|
424
|
+
channels: [],
|
|
425
|
+
};
|
|
426
|
+
// Add categories sorted by position
|
|
427
|
+
const sortedCategories = [...categories.values()].sort((a, b) => a.position - b.position);
|
|
428
|
+
for (const cat of sortedCategories) {
|
|
429
|
+
organized[cat.id] = {
|
|
430
|
+
id: cat.id,
|
|
431
|
+
name: cat.name,
|
|
432
|
+
channels: [],
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
// Sort channels into categories
|
|
436
|
+
const nonCategoryChannels = guild.channels.cache
|
|
437
|
+
.filter(c => c.type !== ChannelType.GuildCategory)
|
|
438
|
+
.values();
|
|
439
|
+
for (const channel of nonCategoryChannels) {
|
|
440
|
+
const guildChannel = channel;
|
|
441
|
+
const categoryId = guildChannel.parentId ?? 'uncategorized';
|
|
442
|
+
const category = organized[categoryId] ?? organized['uncategorized'];
|
|
443
|
+
category.channels.push({
|
|
444
|
+
id: guildChannel.id,
|
|
445
|
+
name: guildChannel.name,
|
|
446
|
+
type: getChannelTypeName(guildChannel.type),
|
|
447
|
+
position: guildChannel.position,
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
// Sort channels within each category by position
|
|
451
|
+
for (const cat of Object.values(organized)) {
|
|
452
|
+
cat.channels.sort((a, b) => a.position - b.position);
|
|
453
|
+
}
|
|
454
|
+
// Remove empty uncategorized if no channels
|
|
455
|
+
if (organized['uncategorized'].channels.length === 0) {
|
|
456
|
+
delete organized['uncategorized'];
|
|
457
|
+
}
|
|
458
|
+
return JSON.stringify({
|
|
459
|
+
totalChannels: guild.channels.cache.size,
|
|
460
|
+
categories: Object.values(organized),
|
|
461
|
+
}, null, 2);
|
|
462
|
+
}
|
|
463
|
+
async function createTextChannel(args) {
|
|
464
|
+
const guild = await getGuild();
|
|
465
|
+
const name = args['name'];
|
|
466
|
+
const categoryIdentifier = args['category'];
|
|
467
|
+
const topic = args['topic'];
|
|
468
|
+
const nsfw = args['nsfw'];
|
|
469
|
+
const reason = args['reason'];
|
|
470
|
+
let parent = null;
|
|
471
|
+
if (categoryIdentifier) {
|
|
472
|
+
parent = await smartFindCategory(categoryIdentifier);
|
|
473
|
+
}
|
|
474
|
+
const channel = await guild.channels.create({
|
|
475
|
+
name,
|
|
476
|
+
type: ChannelType.GuildText,
|
|
477
|
+
parent: parent ?? undefined,
|
|
478
|
+
topic,
|
|
479
|
+
nsfw: nsfw ?? false,
|
|
480
|
+
reason: reason ?? 'Created via MCP',
|
|
481
|
+
});
|
|
482
|
+
// Refresh cache so the new channel is immediately findable
|
|
483
|
+
await refreshServerCache();
|
|
484
|
+
return JSON.stringify({
|
|
485
|
+
success: true,
|
|
486
|
+
message: `Text channel "#${channel.name}" created successfully`,
|
|
487
|
+
channel: {
|
|
488
|
+
id: channel.id,
|
|
489
|
+
name: channel.name,
|
|
490
|
+
type: 'text',
|
|
491
|
+
category: parent?.name ?? null,
|
|
492
|
+
},
|
|
493
|
+
}, null, 2);
|
|
494
|
+
}
|
|
495
|
+
async function createVoiceChannel(args) {
|
|
496
|
+
const guild = await getGuild();
|
|
497
|
+
const name = args['name'];
|
|
498
|
+
const categoryIdentifier = args['category'];
|
|
499
|
+
const userLimit = args['userLimit'];
|
|
500
|
+
const bitrate = args['bitrate'];
|
|
501
|
+
const reason = args['reason'];
|
|
502
|
+
let parent = null;
|
|
503
|
+
if (categoryIdentifier) {
|
|
504
|
+
parent = await smartFindCategory(categoryIdentifier);
|
|
505
|
+
}
|
|
506
|
+
const channel = await guild.channels.create({
|
|
507
|
+
name,
|
|
508
|
+
type: ChannelType.GuildVoice,
|
|
509
|
+
parent: parent ?? undefined,
|
|
510
|
+
userLimit: userLimit ?? 0,
|
|
511
|
+
bitrate: bitrate ?? 64000,
|
|
512
|
+
reason: reason ?? 'Created via MCP',
|
|
513
|
+
});
|
|
514
|
+
// Refresh cache so the new channel is immediately findable
|
|
515
|
+
await refreshServerCache();
|
|
516
|
+
return JSON.stringify({
|
|
517
|
+
success: true,
|
|
518
|
+
message: `Voice channel "${channel.name}" created successfully`,
|
|
519
|
+
channel: {
|
|
520
|
+
id: channel.id,
|
|
521
|
+
name: channel.name,
|
|
522
|
+
type: 'voice',
|
|
523
|
+
category: parent?.name ?? null,
|
|
524
|
+
userLimit: channel.userLimit,
|
|
525
|
+
},
|
|
526
|
+
}, null, 2);
|
|
527
|
+
}
|
|
528
|
+
async function createCategory(args) {
|
|
529
|
+
const guild = await getGuild();
|
|
530
|
+
const name = args['name'];
|
|
531
|
+
const reason = args['reason'];
|
|
532
|
+
const category = await guild.channels.create({
|
|
533
|
+
name,
|
|
534
|
+
type: ChannelType.GuildCategory,
|
|
535
|
+
reason: reason ?? 'Created via MCP',
|
|
536
|
+
});
|
|
537
|
+
// Refresh cache so the new category is immediately findable
|
|
538
|
+
await refreshServerCache();
|
|
539
|
+
return JSON.stringify({
|
|
540
|
+
success: true,
|
|
541
|
+
message: `Category "${category.name}" created successfully`,
|
|
542
|
+
category: {
|
|
543
|
+
id: category.id,
|
|
544
|
+
name: category.name,
|
|
545
|
+
},
|
|
546
|
+
}, null, 2);
|
|
547
|
+
}
|
|
548
|
+
async function deleteChannel(args) {
|
|
549
|
+
const channelIdentifier = args['channel'];
|
|
550
|
+
const reason = args['reason'];
|
|
551
|
+
const channel = await smartFindChannel(channelIdentifier);
|
|
552
|
+
const channelName = channel.name;
|
|
553
|
+
const channelType = getChannelTypeName(channel.type);
|
|
554
|
+
await channel.delete(reason ?? 'Deleted via MCP');
|
|
555
|
+
// Refresh cache so the deleted channel is removed from lookups
|
|
556
|
+
await refreshServerCache();
|
|
557
|
+
return JSON.stringify({
|
|
558
|
+
success: true,
|
|
559
|
+
message: `${channelType} channel "${channelName}" deleted successfully`,
|
|
560
|
+
}, null, 2);
|
|
561
|
+
}
|
|
562
|
+
async function modifyChannel(args) {
|
|
563
|
+
const channelIdentifier = args['channel'];
|
|
564
|
+
const channel = await smartFindChannel(channelIdentifier);
|
|
565
|
+
const updates = {};
|
|
566
|
+
if (args['name'] !== undefined)
|
|
567
|
+
updates['name'] = args['name'];
|
|
568
|
+
if (args['topic'] !== undefined && channel.isTextBased())
|
|
569
|
+
updates['topic'] = args['topic'];
|
|
570
|
+
if (args['nsfw'] !== undefined && channel.isTextBased())
|
|
571
|
+
updates['nsfw'] = args['nsfw'];
|
|
572
|
+
if (args['userLimit'] !== undefined && channel.isVoiceBased())
|
|
573
|
+
updates['userLimit'] = args['userLimit'];
|
|
574
|
+
if (args['position'] !== undefined)
|
|
575
|
+
updates['position'] = args['position'];
|
|
576
|
+
// Handle category change
|
|
577
|
+
if (args['category'] !== undefined) {
|
|
578
|
+
if (args['category'] === 'none') {
|
|
579
|
+
updates['parent'] = null;
|
|
580
|
+
}
|
|
581
|
+
else {
|
|
582
|
+
const category = await smartFindCategory(args['category']);
|
|
583
|
+
updates['parent'] = category.id;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
const reason = args['reason'] ?? 'Modified via MCP';
|
|
587
|
+
await channel.edit({ ...updates, reason });
|
|
588
|
+
return JSON.stringify({
|
|
589
|
+
success: true,
|
|
590
|
+
message: `Channel "${channel.name}" modified successfully`,
|
|
591
|
+
channel: {
|
|
592
|
+
id: channel.id,
|
|
593
|
+
name: channel.name,
|
|
594
|
+
type: getChannelTypeName(channel.type),
|
|
595
|
+
},
|
|
596
|
+
}, null, 2);
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* Resolve permission flag names to PermissionFlagsBits values
|
|
600
|
+
*/
|
|
601
|
+
function resolvePermissions(names) {
|
|
602
|
+
const result = {};
|
|
603
|
+
for (const name of names) {
|
|
604
|
+
if (name in PermissionFlagsBits) {
|
|
605
|
+
result[name] = true;
|
|
606
|
+
}
|
|
607
|
+
else {
|
|
608
|
+
throw new Error(`Unknown permission: "${name}". Valid permissions include: ${Object.keys(PermissionFlagsBits).slice(0, 10).join(', ')}...`);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
return result;
|
|
612
|
+
}
|
|
613
|
+
async function setChannelPermissions(args) {
|
|
614
|
+
const guild = await getGuild();
|
|
615
|
+
const channelIdentifier = args['channel'];
|
|
616
|
+
const targetIdentifier = args['target'];
|
|
617
|
+
const targetType = args['targetType'];
|
|
618
|
+
const allowPerms = args['allow'];
|
|
619
|
+
const denyPerms = args['deny'];
|
|
620
|
+
const reason = args['reason'];
|
|
621
|
+
const channel = await smartFindChannel(channelIdentifier);
|
|
622
|
+
// Resolve target
|
|
623
|
+
let targetId;
|
|
624
|
+
let targetName;
|
|
625
|
+
if (targetType === 'role') {
|
|
626
|
+
const role = await smartFindRole(targetIdentifier);
|
|
627
|
+
targetId = role.id;
|
|
628
|
+
targetName = `@${role.name}`;
|
|
629
|
+
}
|
|
630
|
+
else {
|
|
631
|
+
const member = await smartFindMember(targetIdentifier);
|
|
632
|
+
targetId = member.id;
|
|
633
|
+
targetName = `@${member.displayName}`;
|
|
634
|
+
}
|
|
635
|
+
// Build overwrite object
|
|
636
|
+
const overwrite = {};
|
|
637
|
+
if (allowPerms) {
|
|
638
|
+
for (const perm of allowPerms) {
|
|
639
|
+
if (!(perm in PermissionFlagsBits)) {
|
|
640
|
+
throw new Error(`Unknown permission: "${perm}". Valid: ${Object.keys(PermissionFlagsBits).slice(0, 10).join(', ')}...`);
|
|
641
|
+
}
|
|
642
|
+
overwrite[perm] = true;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
if (denyPerms) {
|
|
646
|
+
for (const perm of denyPerms) {
|
|
647
|
+
if (!(perm in PermissionFlagsBits)) {
|
|
648
|
+
throw new Error(`Unknown permission: "${perm}". Valid: ${Object.keys(PermissionFlagsBits).slice(0, 10).join(', ')}...`);
|
|
649
|
+
}
|
|
650
|
+
overwrite[perm] = false;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
await channel.permissionOverwrites.edit(targetId, overwrite, { reason: reason ?? 'Permissions updated via MCP' });
|
|
654
|
+
return JSON.stringify({
|
|
655
|
+
success: true,
|
|
656
|
+
message: `Permissions updated for ${targetName} on #${channel.name}`,
|
|
657
|
+
channel: { id: channel.id, name: channel.name },
|
|
658
|
+
target: { id: targetId, name: targetName, type: targetType },
|
|
659
|
+
allowed: allowPerms ?? [],
|
|
660
|
+
denied: denyPerms ?? [],
|
|
661
|
+
}, null, 2);
|
|
662
|
+
}
|
|
663
|
+
async function viewChannelPermissions(args) {
|
|
664
|
+
const guild = await getGuild();
|
|
665
|
+
const channelIdentifier = args['channel'];
|
|
666
|
+
const channel = await smartFindChannel(channelIdentifier);
|
|
667
|
+
const overwrites = channel.permissionOverwrites.cache.map(overwrite => {
|
|
668
|
+
let targetName;
|
|
669
|
+
let targetType;
|
|
670
|
+
if (overwrite.type === OverwriteType.Role) {
|
|
671
|
+
const role = guild.roles.cache.get(overwrite.id);
|
|
672
|
+
targetName = role ? `@${role.name}` : overwrite.id;
|
|
673
|
+
targetType = 'role';
|
|
674
|
+
}
|
|
675
|
+
else {
|
|
676
|
+
const member = guild.members.cache.get(overwrite.id);
|
|
677
|
+
targetName = member ? `@${member.displayName}` : overwrite.id;
|
|
678
|
+
targetType = 'member';
|
|
679
|
+
}
|
|
680
|
+
const allowed = new PermissionsBitField(overwrite.allow).toArray();
|
|
681
|
+
const denied = new PermissionsBitField(overwrite.deny).toArray();
|
|
682
|
+
return {
|
|
683
|
+
targetId: overwrite.id,
|
|
684
|
+
targetName,
|
|
685
|
+
targetType,
|
|
686
|
+
allowed,
|
|
687
|
+
denied,
|
|
688
|
+
};
|
|
689
|
+
});
|
|
690
|
+
return JSON.stringify({
|
|
691
|
+
channel: { id: channel.id, name: channel.name, type: getChannelTypeName(channel.type) },
|
|
692
|
+
permissionOverwrites: [...overwrites],
|
|
693
|
+
}, null, 2);
|
|
694
|
+
}
|
|
695
|
+
async function lockChannel(args) {
|
|
696
|
+
const guild = await getGuild();
|
|
697
|
+
const channelIdentifier = args['channel'];
|
|
698
|
+
const reason = args['reason'];
|
|
699
|
+
const channel = await smartFindChannel(channelIdentifier);
|
|
700
|
+
await channel.permissionOverwrites.edit(guild.id, {
|
|
701
|
+
SendMessages: false,
|
|
702
|
+
}, { reason: reason ?? 'Channel locked via MCP' });
|
|
703
|
+
return JSON.stringify({
|
|
704
|
+
success: true,
|
|
705
|
+
message: `Channel #${channel.name} has been locked (SendMessages denied for @everyone)`,
|
|
706
|
+
}, null, 2);
|
|
707
|
+
}
|
|
708
|
+
async function unlockChannel(args) {
|
|
709
|
+
const guild = await getGuild();
|
|
710
|
+
const channelIdentifier = args['channel'];
|
|
711
|
+
const reason = args['reason'];
|
|
712
|
+
const channel = await smartFindChannel(channelIdentifier);
|
|
713
|
+
await channel.permissionOverwrites.edit(guild.id, {
|
|
714
|
+
SendMessages: null,
|
|
715
|
+
}, { reason: reason ?? 'Channel unlocked via MCP' });
|
|
716
|
+
return JSON.stringify({
|
|
717
|
+
success: true,
|
|
718
|
+
message: `Channel #${channel.name} has been unlocked (SendMessages deny removed for @everyone)`,
|
|
719
|
+
}, null, 2);
|
|
720
|
+
}
|
|
721
|
+
async function setSlowmode(args) {
|
|
722
|
+
const channelIdentifier = args['channel'];
|
|
723
|
+
const seconds = args['seconds'];
|
|
724
|
+
if (seconds < 0 || seconds > 21600) {
|
|
725
|
+
throw new Error('Slowmode must be between 0 (disabled) and 21600 seconds (6 hours)');
|
|
726
|
+
}
|
|
727
|
+
const channel = await smartFindChannel(channelIdentifier);
|
|
728
|
+
if (!channel.isTextBased()) {
|
|
729
|
+
throw new Error(`Cannot set slowmode on ${getChannelTypeName(channel.type)} channel "${channel.name}"`);
|
|
730
|
+
}
|
|
731
|
+
await channel.setRateLimitPerUser(seconds);
|
|
732
|
+
return JSON.stringify({
|
|
733
|
+
success: true,
|
|
734
|
+
message: seconds === 0
|
|
735
|
+
? `Slowmode disabled on #${channel.name}`
|
|
736
|
+
: `Slowmode set to ${seconds} seconds on #${channel.name}`,
|
|
737
|
+
channel: { id: channel.id, name: channel.name },
|
|
738
|
+
slowmodeSeconds: seconds,
|
|
739
|
+
}, null, 2);
|
|
740
|
+
}
|
|
741
|
+
async function cloneChannel(args) {
|
|
742
|
+
const channelIdentifier = args['channel'];
|
|
743
|
+
const newName = args['name'];
|
|
744
|
+
const reason = args['reason'];
|
|
745
|
+
const channel = await smartFindChannel(channelIdentifier);
|
|
746
|
+
const cloned = await channel.clone({
|
|
747
|
+
name: newName ?? channel.name,
|
|
748
|
+
reason: reason ?? 'Cloned via MCP',
|
|
749
|
+
});
|
|
750
|
+
await refreshServerCache();
|
|
751
|
+
return JSON.stringify({
|
|
752
|
+
success: true,
|
|
753
|
+
message: `Channel #${channel.name} cloned as #${cloned.name}`,
|
|
754
|
+
original: { id: channel.id, name: channel.name },
|
|
755
|
+
cloned: { id: cloned.id, name: cloned.name, type: getChannelTypeName(cloned.type) },
|
|
756
|
+
}, null, 2);
|
|
757
|
+
}
|
|
758
|
+
async function createForumChannel(args) {
|
|
759
|
+
const guild = await getGuild();
|
|
760
|
+
const name = args['name'];
|
|
761
|
+
const categoryIdentifier = args['category'];
|
|
762
|
+
const topic = args['topic'];
|
|
763
|
+
const reason = args['reason'];
|
|
764
|
+
let parent = null;
|
|
765
|
+
if (categoryIdentifier) {
|
|
766
|
+
parent = await smartFindCategory(categoryIdentifier);
|
|
767
|
+
}
|
|
768
|
+
const channel = await guild.channels.create({
|
|
769
|
+
name,
|
|
770
|
+
type: ChannelType.GuildForum,
|
|
771
|
+
parent: parent ?? undefined,
|
|
772
|
+
topic,
|
|
773
|
+
reason: reason ?? 'Created via MCP',
|
|
774
|
+
});
|
|
775
|
+
await refreshServerCache();
|
|
776
|
+
return JSON.stringify({
|
|
777
|
+
success: true,
|
|
778
|
+
message: `Forum channel "#${channel.name}" created successfully`,
|
|
779
|
+
channel: {
|
|
780
|
+
id: channel.id,
|
|
781
|
+
name: channel.name,
|
|
782
|
+
type: 'forum',
|
|
783
|
+
category: parent?.name ?? null,
|
|
784
|
+
},
|
|
785
|
+
}, null, 2);
|
|
786
|
+
}
|
|
787
|
+
async function reorderChannels(args) {
|
|
788
|
+
const guild = await getGuild();
|
|
789
|
+
const categoryIdentifier = args['category'];
|
|
790
|
+
const channelOrder = args['channelOrder'];
|
|
791
|
+
const category = await smartFindCategory(categoryIdentifier);
|
|
792
|
+
// Resolve channel names/IDs to actual channels
|
|
793
|
+
const positions = [];
|
|
794
|
+
for (let i = 0; i < channelOrder.length; i++) {
|
|
795
|
+
const ch = await smartFindChannel(channelOrder[i]);
|
|
796
|
+
positions.push({ channel: ch.id, position: i });
|
|
797
|
+
}
|
|
798
|
+
await guild.channels.setPositions(positions);
|
|
799
|
+
return JSON.stringify({
|
|
800
|
+
success: true,
|
|
801
|
+
message: `Reordered ${positions.length} channels in "${category.name}"`,
|
|
802
|
+
category: { id: category.id, name: category.name },
|
|
803
|
+
newOrder: channelOrder,
|
|
804
|
+
}, null, 2);
|
|
805
|
+
}
|
|
806
|
+
async function setVoiceRegion(args) {
|
|
807
|
+
const channelIdentifier = args['channel'];
|
|
808
|
+
const region = args['region'];
|
|
809
|
+
const channel = await smartFindChannel(channelIdentifier);
|
|
810
|
+
if (channel.type !== ChannelType.GuildVoice && channel.type !== ChannelType.GuildStageVoice) {
|
|
811
|
+
throw new Error(`"${channel.name}" is not a voice channel`);
|
|
812
|
+
}
|
|
813
|
+
const rtcRegion = region === 'auto' ? null : region;
|
|
814
|
+
await channel.setRTCRegion(rtcRegion);
|
|
815
|
+
return JSON.stringify({
|
|
816
|
+
success: true,
|
|
817
|
+
message: rtcRegion
|
|
818
|
+
? `Voice region for "${channel.name}" set to "${rtcRegion}"`
|
|
819
|
+
: `Voice region for "${channel.name}" set to automatic`,
|
|
820
|
+
channel: { id: channel.id, name: channel.name },
|
|
821
|
+
region: rtcRegion ?? 'automatic',
|
|
822
|
+
}, null, 2);
|
|
823
|
+
}
|
|
824
|
+
async function followAnnouncementChannel(args) {
|
|
825
|
+
const announcementIdentifier = args['announcementChannel'];
|
|
826
|
+
const targetIdentifier = args['targetChannel'];
|
|
827
|
+
const announcementChannel = await smartFindTextChannel(announcementIdentifier);
|
|
828
|
+
const targetChannel = await smartFindTextChannel(targetIdentifier);
|
|
829
|
+
if (announcementChannel.type !== ChannelType.GuildAnnouncement) {
|
|
830
|
+
throw new Error(`#${announcementChannel.name} is not an announcement channel`);
|
|
831
|
+
}
|
|
832
|
+
await announcementChannel.addFollower(targetChannel, 'Followed via MCP');
|
|
833
|
+
return JSON.stringify({
|
|
834
|
+
success: true,
|
|
835
|
+
message: `#${targetChannel.name} is now following announcements from #${announcementChannel.name}`,
|
|
836
|
+
source: { id: announcementChannel.id, name: announcementChannel.name },
|
|
837
|
+
target: { id: targetChannel.id, name: targetChannel.name },
|
|
838
|
+
}, null, 2);
|
|
839
|
+
}
|
|
840
|
+
//# sourceMappingURL=channels.js.map
|