import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { DiscordClientManager } from "../discord/client.js";
import { Logger } from "../utils/logger.js";
import { z } from "zod";
import {
PermissionDeniedError,
GuildNotFoundError,
} from "../errors/discord.js";
import { PermissionFlagsBits, PermissionsBitField } from "discord.js";
export function registerRoleTools(
server: McpServer,
discordManager: DiscordClientManager,
logger: Logger,
) {
// Create Role Tool
server.registerTool(
"create_role",
{
title: "Create Server Role",
description: "Create a new role in the server with specified permissions",
inputSchema: {
guildId: z.string().describe("Server/Guild ID"),
name: z.string().max(100).describe("Role name"),
color: z
.number()
.int()
.min(0)
.max(0xffffff)
.optional()
.describe("Role color as hex integer (e.g., 0x00ff00 for green)"),
hoist: z
.boolean()
.optional()
.describe("Display role members separately in sidebar"),
mentionable: z
.boolean()
.optional()
.describe("Allow anyone to @mention this role"),
permissions: z
.array(z.string())
.optional()
.describe(
'Array of permission names (e.g., ["SendMessages", "ManageMessages"])',
),
reason: z
.string()
.optional()
.describe("Reason for role creation (shown in audit log)"),
},
outputSchema: {
success: z.boolean(),
role: z
.object({
id: z.string(),
name: z.string(),
color: z.number(),
position: z.number(),
permissions: z.array(z.string()),
})
.optional(),
error: z.string().optional(),
},
},
async ({
guildId,
name,
color,
hoist,
mentionable,
permissions,
reason,
}) => {
try {
const client = discordManager.getClient();
const guild = await client.guilds.fetch(guildId).catch(() => null);
if (!guild) {
throw new GuildNotFoundError(guildId);
}
// Check bot permissions
const botMember = await guild.members.fetchMe();
if (!botMember.permissions.has(PermissionFlagsBits.ManageRoles)) {
throw new PermissionDeniedError("ManageRoles", guildId);
}
// Build permissions bitfield
let permissionsBitField: bigint | undefined;
if (permissions && permissions.length > 0) {
permissionsBitField = new PermissionsBitField(permissions as any)
.bitfield;
}
// Create role
const role = await guild.roles.create({
name,
color,
hoist,
mentionable,
permissions: permissionsBitField,
reason,
});
const roleData = {
id: role.id,
name: role.name,
color: role.color,
position: role.position,
permissions: role.permissions.toArray(),
};
const output = {
success: true,
role: roleData,
};
logger.info("Role created", { guildId, roleId: role.id, name });
return {
content: [
{
type: "text" as const,
text: `Successfully created role "${name}" (ID: ${role.id})`,
},
],
structuredContent: output,
};
} catch (error: any) {
logger.error("Failed to create role", {
error: error.message,
guildId,
name,
});
const output = {
success: false,
error: error.message,
};
return {
content: [
{
type: "text" as const,
text: `Failed to create role: ${error.message}`,
},
],
structuredContent: output,
isError: true,
};
}
},
);
// Delete Role Tool
server.registerTool(
"delete_role",
{
title: "Delete Server Role",
description: "Delete a role from the server",
inputSchema: {
guildId: z.string().describe("Server/Guild ID"),
roleId: z.string().describe("Role ID to delete"),
reason: z
.string()
.optional()
.describe("Reason for role deletion (shown in audit log)"),
},
outputSchema: {
success: z.boolean(),
roleId: z.string().optional(),
roleName: z.string().optional(),
error: z.string().optional(),
},
},
async ({ guildId, roleId, reason }) => {
try {
const client = discordManager.getClient();
const guild = await client.guilds.fetch(guildId).catch(() => null);
if (!guild) {
throw new GuildNotFoundError(guildId);
}
// Check bot permissions
const botMember = await guild.members.fetchMe();
if (!botMember.permissions.has(PermissionFlagsBits.ManageRoles)) {
throw new PermissionDeniedError("ManageRoles", guildId);
}
// Fetch role
const role = await guild.roles.fetch(roleId).catch(() => null);
if (!role) {
throw new Error(`Role ${roleId} not found in server`);
}
// Check if it's @everyone
if (role.id === guild.id) {
throw new Error("Cannot delete @everyone role");
}
// Check role hierarchy
if (role.position >= botMember.roles.highest.position) {
throw new Error(
"Cannot delete this role - it is equal to or higher than the bot's highest role",
);
}
const roleName = role.name;
await role.delete(reason);
const output = {
success: true,
roleId,
roleName,
};
logger.info("Role deleted", { guildId, roleId, roleName, reason });
return {
content: [
{
type: "text" as const,
text: `Successfully deleted role "${roleName}"`,
},
],
structuredContent: output,
};
} catch (error: any) {
logger.error("Failed to delete role", {
error: error.message,
guildId,
roleId,
});
const output = {
success: false,
error: error.message,
};
return {
content: [
{
type: "text" as const,
text: `Failed to delete role: ${error.message}`,
},
],
structuredContent: output,
isError: true,
};
}
},
);
// Modify Role Tool
server.registerTool(
"modify_role",
{
title: "Modify Server Role",
description:
"Update a role's name, color, permissions, or other settings",
inputSchema: {
guildId: z.string().describe("Server/Guild ID"),
roleId: z.string().describe("Role ID to modify"),
name: z.string().max(100).optional().describe("New role name"),
color: z
.number()
.int()
.min(0)
.max(0xffffff)
.optional()
.describe("New role color as hex integer"),
hoist: z
.boolean()
.optional()
.describe("Display role members separately in sidebar"),
mentionable: z
.boolean()
.optional()
.describe("Allow anyone to @mention this role"),
permissions: z
.array(z.string())
.optional()
.describe(
"Array of permission names to set (replaces all permissions)",
),
reason: z
.string()
.optional()
.describe("Reason for modification (shown in audit log)"),
},
outputSchema: {
success: z.boolean(),
role: z
.object({
id: z.string(),
name: z.string(),
color: z.number(),
permissions: z.array(z.string()),
})
.optional(),
error: z.string().optional(),
},
},
async ({
guildId,
roleId,
name,
color,
hoist,
mentionable,
permissions,
reason,
}) => {
try {
const client = discordManager.getClient();
const guild = await client.guilds.fetch(guildId).catch(() => null);
if (!guild) {
throw new GuildNotFoundError(guildId);
}
// Check bot permissions
const botMember = await guild.members.fetchMe();
if (!botMember.permissions.has(PermissionFlagsBits.ManageRoles)) {
throw new PermissionDeniedError("ManageRoles", guildId);
}
// Fetch role
const role = await guild.roles.fetch(roleId).catch(() => null);
if (!role) {
throw new Error(`Role ${roleId} not found in server`);
}
// Check if it's @everyone and trying to change name
if (role.id === guild.id && name) {
throw new Error("Cannot rename @everyone role");
}
// Check role hierarchy
if (role.position >= botMember.roles.highest.position) {
throw new Error(
"Cannot modify this role - it is equal to or higher than the bot's highest role",
);
}
// Build update options
const updateOptions: any = {};
if (name !== undefined) updateOptions.name = name;
if (color !== undefined) updateOptions.color = color;
if (hoist !== undefined) updateOptions.hoist = hoist;
if (mentionable !== undefined) updateOptions.mentionable = mentionable;
if (permissions !== undefined) {
updateOptions.permissions = new PermissionsBitField(
permissions as any,
).bitfield;
}
if (reason !== undefined) updateOptions.reason = reason;
// Update role
const updatedRole = await role.edit(updateOptions);
const roleData = {
id: updatedRole.id,
name: updatedRole.name,
color: updatedRole.color,
permissions: updatedRole.permissions.toArray(),
};
const output = {
success: true,
role: roleData,
};
logger.info("Role modified", { guildId, roleId, reason });
return {
content: [
{
type: "text" as const,
text: `Successfully modified role "${updatedRole.name}"`,
},
],
structuredContent: output,
};
} catch (error: any) {
logger.error("Failed to modify role", {
error: error.message,
guildId,
roleId,
});
const output = {
success: false,
error: error.message,
};
return {
content: [
{
type: "text" as const,
text: `Failed to modify role: ${error.message}`,
},
],
structuredContent: output,
isError: true,
};
}
},
);
// List Roles Tool
server.registerTool(
"list_roles",
{
title: "List Server Roles",
description: "Get all roles in the server with their permissions",
inputSchema: {
guildId: z.string().describe("Server/Guild ID"),
includeEveryone: z
.boolean()
.optional()
.default(false)
.describe("Include @everyone role in results"),
},
outputSchema: {
success: z.boolean(),
roles: z
.array(
z.object({
id: z.string(),
name: z.string(),
color: z.number(),
position: z.number(),
memberCount: z.number(),
permissions: z.array(z.string()),
hoist: z.boolean(),
mentionable: z.boolean(),
managed: z.boolean(),
}),
)
.optional(),
totalCount: z.number().optional(),
error: z.string().optional(),
},
},
async ({ guildId, includeEveryone = false }) => {
try {
const client = discordManager.getClient();
const guild = await client.guilds.fetch(guildId).catch(() => null);
if (!guild) {
throw new GuildNotFoundError(guildId);
}
// Fetch all roles
const roles = await guild.roles.fetch();
// Filter out @everyone if requested
let roleList = Array.from(roles.values());
if (!includeEveryone) {
roleList = roleList.filter((r) => r.id !== guild.id);
}
// Sort by position (highest first)
roleList.sort((a, b) => b.position - a.position);
const rolesData = roleList.map((role) => ({
id: role.id,
name: role.name,
color: role.color,
position: role.position,
memberCount: role.members.size,
permissions: role.permissions.toArray(),
hoist: role.hoist,
mentionable: role.mentionable,
managed: role.managed,
}));
const output = {
success: true,
roles: rolesData,
totalCount: rolesData.length,
};
logger.info("Roles listed", { guildId, count: rolesData.length });
return {
content: [
{
type: "text" as const,
text: `Found ${rolesData.length} role(s) in server`,
},
],
structuredContent: output,
};
} catch (error: any) {
logger.error("Failed to list roles", {
error: error.message,
guildId,
});
const output = {
success: false,
error: error.message,
};
return {
content: [
{
type: "text" as const,
text: `Failed to list roles: ${error.message}`,
},
],
structuredContent: output,
isError: true,
};
}
},
);
// Get Role Info Tool
server.registerTool(
"get_role_info",
{
title: "Get Role Information",
description: "Get detailed information about a specific role",
inputSchema: {
guildId: z.string().describe("Server/Guild ID"),
roleId: z.string().describe("Role ID to get info for"),
},
outputSchema: {
success: z.boolean(),
role: z
.object({
id: z.string(),
name: z.string(),
color: z.number(),
position: z.number(),
memberCount: z.number(),
permissions: z.array(z.string()),
hoist: z.boolean(),
mentionable: z.boolean(),
managed: z.boolean(),
createdAt: z.string(),
})
.optional(),
error: z.string().optional(),
},
},
async ({ guildId, roleId }) => {
try {
const client = discordManager.getClient();
const guild = await client.guilds.fetch(guildId).catch(() => null);
if (!guild) {
throw new GuildNotFoundError(guildId);
}
// Fetch role
const role = await guild.roles.fetch(roleId).catch(() => null);
if (!role) {
throw new Error(`Role ${roleId} not found in server`);
}
const roleData = {
id: role.id,
name: role.name,
color: role.color,
position: role.position,
memberCount: role.members.size,
permissions: role.permissions.toArray(),
hoist: role.hoist,
mentionable: role.mentionable,
managed: role.managed,
createdAt: role.createdAt.toISOString(),
};
const output = {
success: true,
role: roleData,
};
logger.info("Role info retrieved", { guildId, roleId });
return {
content: [
{
type: "text" as const,
text: `Role: ${role.name}\nMembers: ${roleData.memberCount}\nPermissions: ${roleData.permissions.length}`,
},
],
structuredContent: output,
};
} catch (error: any) {
logger.error("Failed to get role info", {
error: error.message,
guildId,
roleId,
});
const output = {
success: false,
error: error.message,
};
return {
content: [
{
type: "text" as const,
text: `Failed to get role info: ${error.message}`,
},
],
structuredContent: output,
isError: true,
};
}
},
);
}