import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
import { OverwriteType, PermissionFlagsBits, GuildChannel } from 'discord.js';
import { getDiscordClient } from '../utils/discord-client.js';
import { withErrorHandling } from '../utils/error-handler.js';
export function registerPermissionTools(server: McpServer): void {
// Get channel permission overwrites
server.tool(
'get_channel_permissions',
'Get all permission overwrites for a channel',
{
guildId: z.string().describe('The ID of the server (guild)'),
channelId: z.string().describe('The ID of the channel'),
},
async ({ guildId, channelId }) => {
const result = await withErrorHandling(async () => {
const client = await getDiscordClient();
const guild = await client.guilds.fetch(guildId);
const channel = await guild.channels.fetch(channelId);
if (!channel) throw new Error('Channel not found');
const overwrites = (channel as GuildChannel).permissionOverwrites.cache.map((overwrite) => ({
id: overwrite.id,
type: overwrite.type === OverwriteType.Role ? 'role' : 'member',
allow: overwrite.allow.toArray(),
deny: overwrite.deny.toArray(),
}));
// Get role/member names for context
const resolvedOverwrites = await Promise.all(
overwrites.map(async (ow) => {
let name = 'Unknown';
if (ow.type === 'role') {
const role = await guild.roles.fetch(ow.id);
name = role?.name ?? 'Unknown Role';
} else {
try {
const member = await guild.members.fetch(ow.id);
name = member.displayName;
} catch {
name = 'Unknown Member';
}
}
return { ...ow, name };
})
);
return {
channelId,
channelName: channel.name,
overwrites: resolvedOverwrites,
};
});
if (!result.success) {
return { content: [{ type: 'text', text: result.error }], isError: true };
}
return { content: [{ type: 'text', text: JSON.stringify(result.data, null, 2) }] };
}
);
// Set channel permission overwrite
server.tool(
'set_channel_permission',
'Set permission overwrite for a role or member on a channel',
{
guildId: z.string().describe('The ID of the server (guild)'),
channelId: z.string().describe('The ID of the channel'),
targetId: z.string().describe('The ID of the role or member'),
targetType: z.enum(['role', 'member']).describe('Whether the target is a role or member'),
allow: z.array(z.string()).optional().describe('Array of permission names to allow'),
deny: z.array(z.string()).optional().describe('Array of permission names to deny'),
reason: z.string().optional().describe('Reason for the change'),
},
async ({ guildId, channelId, targetId, targetType, allow, deny, reason }) => {
const result = await withErrorHandling(async () => {
const client = await getDiscordClient();
const guild = await client.guilds.fetch(guildId);
const channel = await guild.channels.fetch(channelId);
if (!channel) throw new Error('Channel not found');
const guildChannel = channel as GuildChannel;
// Build permission overwrite options
const overwriteOptions: Record<string, boolean> = {};
if (allow) {
for (const perm of allow) {
if (perm in PermissionFlagsBits) {
overwriteOptions[perm] = true;
}
}
}
if (deny) {
for (const perm of deny) {
if (perm in PermissionFlagsBits) {
overwriteOptions[perm] = false;
}
}
}
await guildChannel.permissionOverwrites.edit(targetId, overwriteOptions, {
reason,
type: targetType === 'role' ? OverwriteType.Role : OverwriteType.Member,
});
return {
channelId,
channelName: channel.name,
targetId,
targetType,
message: 'Permission overwrite set successfully',
};
});
if (!result.success) {
return { content: [{ type: 'text', text: result.error }], isError: true };
}
return { content: [{ type: 'text', text: JSON.stringify(result.data, null, 2) }] };
}
);
// Delete channel permission overwrite
server.tool(
'delete_channel_permission',
'Delete a permission overwrite for a role or member on a channel',
{
guildId: z.string().describe('The ID of the server (guild)'),
channelId: z.string().describe('The ID of the channel'),
targetId: z.string().describe('The ID of the role or member'),
reason: z.string().optional().describe('Reason for the deletion'),
},
async ({ guildId, channelId, targetId, reason }) => {
const result = await withErrorHandling(async () => {
const client = await getDiscordClient();
const guild = await client.guilds.fetch(guildId);
const channel = await guild.channels.fetch(channelId);
if (!channel) throw new Error('Channel not found');
const guildChannel = channel as GuildChannel;
await guildChannel.permissionOverwrites.delete(targetId, reason);
return {
channelId,
channelName: channel.name,
targetId,
message: 'Permission overwrite deleted successfully',
};
});
if (!result.success) {
return { content: [{ type: 'text', text: result.error }], isError: true };
}
return { content: [{ type: 'text', text: JSON.stringify(result.data, null, 2) }] };
}
);
// List available permissions
server.tool(
'list_permissions',
'List all available Discord permission names that can be used for roles and channel overwrites',
{},
async () => {
const permissions = Object.keys(PermissionFlagsBits).filter(
(key) => isNaN(Number(key))
);
const categorized = {
general: [
'Administrator', 'ViewAuditLog', 'ViewGuildInsights', 'ManageGuild',
'ManageRoles', 'ManageChannels', 'KickMembers', 'BanMembers',
'CreateInstantInvite', 'ChangeNickname', 'ManageNicknames',
'ManageEmojisAndStickers', 'ManageWebhooks', 'ManageGuildExpressions',
'ViewChannel',
],
text: [
'SendMessages', 'SendMessagesInThreads', 'CreatePublicThreads',
'CreatePrivateThreads', 'EmbedLinks', 'AttachFiles', 'AddReactions',
'UseExternalEmojis', 'UseExternalStickers', 'MentionEveryone',
'ManageMessages', 'ManageThreads', 'ReadMessageHistory',
'SendTTSMessages', 'UseApplicationCommands', 'SendVoiceMessages',
],
voice: [
'Connect', 'Speak', 'Stream', 'UseEmbeddedActivities',
'UseSoundboard', 'UseExternalSounds', 'UseVAD', 'PrioritySpeaker',
'MuteMembers', 'DeafenMembers', 'MoveMembers',
],
stage: [
'RequestToSpeak',
],
events: [
'CreateEvents', 'ManageEvents',
],
monetization: [
'CreateGuildExpressions', 'ViewCreatorMonetizationAnalytics',
],
};
return {
content: [
{
type: 'text',
text: JSON.stringify({
message: 'Available Discord permissions',
permissions,
categorized,
}, null, 2),
},
],
};
}
);
// Sync channel permissions with parent category
server.tool(
'sync_channel_permissions',
'Sync a channel\'s permissions with its parent category',
{
guildId: z.string().describe('The ID of the server (guild)'),
channelId: z.string().describe('The ID of the channel'),
},
async ({ guildId, channelId }) => {
const result = await withErrorHandling(async () => {
const client = await getDiscordClient();
const guild = await client.guilds.fetch(guildId);
const channel = await guild.channels.fetch(channelId);
if (!channel) throw new Error('Channel not found');
const guildChannel = channel as GuildChannel;
if (!guildChannel.parent) {
throw new Error('Channel does not have a parent category');
}
await guildChannel.lockPermissions();
return {
channelId,
channelName: channel.name,
parentId: guildChannel.parent.id,
parentName: guildChannel.parent.name,
message: 'Channel permissions synced with parent category',
};
});
if (!result.success) {
return { content: [{ type: 'text', text: result.error }], isError: true };
}
return { content: [{ type: 'text', text: JSON.stringify(result.data, null, 2) }] };
}
);
}