Merge pull request #6 from shaskola/main

feat: add Discord category management (create/edit/delete)
This commit is contained in:
Barry Yip 2025-05-21 15:12:48 +08:00 committed by GitHub
commit 5d1ddf6b87
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 250 additions and 61 deletions

7
.gitignore vendored
View File

@ -4,6 +4,13 @@ npm-debug.log
yarn-debug.log
yarn-error.log
# Logs
logs
*.log
# Internal AI memory/log
knowledge.md
# Environment Variables
.env
.env.local

100
package-lock.json generated
View File

@ -9,10 +9,10 @@
"version": "1.2.0",
"license": "MIT",
"dependencies": {
"discord.js": "^14.18.0",
"discord.js": "^14.19.3",
"dotenv": "^16.5.0",
"express": "^5.1.0",
"zod": "^3.24.2"
"zod": "^3.25.13"
},
"bin": {
"mcp-discord": "build/index.js"
@ -20,7 +20,7 @@
"devDependencies": {
"@modelcontextprotocol/sdk": "^1.11.0",
"@types/express": "^5.0.1",
"@types/node": "^20.17.26",
"@types/node": "^20.17.50",
"ts-node": "^10.9.2",
"typescript": "^5.8.3"
}
@ -39,15 +39,15 @@
}
},
"node_modules/@discordjs/builders": {
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.10.1.tgz",
"integrity": "sha512-OWo1fY4ztL1/M/DUyRPShB4d/EzVfuUvPTRRHRIt/YxBrUYSz0a+JicD5F5zHFoNs2oTuWavxCOVFV1UljHTng==",
"version": "1.11.2",
"resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.11.2.tgz",
"integrity": "sha512-F1WTABdd8/R9D1icJzajC4IuLyyS8f3rTOz66JsSI3pKvpCAtsMBweu8cyNYsIyvcrKAVn9EPK+Psoymq+XC0A==",
"license": "Apache-2.0",
"dependencies": {
"@discordjs/formatters": "^0.6.0",
"@discordjs/formatters": "^0.6.1",
"@discordjs/util": "^1.1.1",
"@sapphire/shapeshift": "^4.0.0",
"discord-api-types": "^0.37.119",
"discord-api-types": "^0.38.1",
"fast-deep-equal": "^3.1.3",
"ts-mixer": "^6.0.4",
"tslib": "^2.6.3"
@ -69,12 +69,12 @@
}
},
"node_modules/@discordjs/formatters": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.6.0.tgz",
"integrity": "sha512-YIruKw4UILt/ivO4uISmrGq2GdMY6EkoTtD0oS0GvkJFRZbTSdPhzYiUILbJ/QslsvC9H9nTgGgnarnIl4jMfw==",
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.6.1.tgz",
"integrity": "sha512-5cnX+tASiPCqCWtFcFslxBVUaCetB0thvM/JyavhbXInP1HJIEU+Qv/zMrnuwSsX3yWH2lVXNJZeDK3EiP4HHg==",
"license": "Apache-2.0",
"dependencies": {
"discord-api-types": "^0.37.114"
"discord-api-types": "^0.38.1"
},
"engines": {
"node": ">=16.11.0"
@ -84,9 +84,9 @@
}
},
"node_modules/@discordjs/rest": {
"version": "2.4.3",
"resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.4.3.tgz",
"integrity": "sha512-+SO4RKvWsM+y8uFHgYQrcTl/3+cY02uQOH7/7bKbVZsTfrfpoE62o5p+mmV+s7FVhTX82/kQUGGbu4YlV60RtA==",
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.5.0.tgz",
"integrity": "sha512-PWhchxTzpn9EV3vvPRpwS0EE2rNYB9pvzDU/eLLW3mByJl0ZHZjHI2/wA8EbH2gRMQV7nu+0FoDF84oiPl8VAQ==",
"license": "Apache-2.0",
"dependencies": {
"@discordjs/collection": "^2.1.1",
@ -94,7 +94,7 @@
"@sapphire/async-queue": "^1.5.3",
"@sapphire/snowflake": "^3.5.3",
"@vladfrangu/async_event_emitter": "^2.4.6",
"discord-api-types": "^0.37.119",
"discord-api-types": "^0.38.1",
"magic-bytes.js": "^1.10.0",
"tslib": "^2.6.3",
"undici": "6.21.1"
@ -131,18 +131,18 @@
}
},
"node_modules/@discordjs/ws": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@discordjs/ws/-/ws-1.2.1.tgz",
"integrity": "sha512-PBvenhZG56a6tMWF/f4P6f4GxZKJTBG95n7aiGSPTnodmz4N5g60t79rSIAq7ywMbv8A4jFtexMruH+oe51aQQ==",
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@discordjs/ws/-/ws-1.2.2.tgz",
"integrity": "sha512-dyfq7yn0wO0IYeYOs3z79I6/HumhmKISzFL0Z+007zQJMtAFGtt3AEoq1nuLXtcunUE5YYYQqgKvybXukAK8/w==",
"license": "Apache-2.0",
"dependencies": {
"@discordjs/collection": "^2.1.0",
"@discordjs/rest": "^2.4.3",
"@discordjs/rest": "^2.5.0",
"@discordjs/util": "^1.1.0",
"@sapphire/async-queue": "^1.5.2",
"@types/ws": "^8.5.10",
"@vladfrangu/async_event_emitter": "^2.2.4",
"discord-api-types": "^0.37.119",
"discord-api-types": "^0.38.1",
"tslib": "^2.6.2",
"ws": "^8.17.0"
},
@ -337,9 +337,9 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "20.17.26",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.26.tgz",
"integrity": "sha512-x9T6TLS76RIBGB0X81k+9697cNZel+f/v+BR8gzKNqISC3MhHHWoHY6XIEDY0E8psIJmCEMXqxjw7Np1u/mysA==",
"version": "20.17.50",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.50.tgz",
"integrity": "sha512-Mxiq0ULv/zo1OzOhwPqOA13I81CV/W3nvd3ChtQZRT5Cwz3cr0FKo/wMSsbTqL3EXpaBAEQhva2B8ByRkOIh9A==",
"license": "MIT",
"dependencies": {
"undici-types": "~6.19.2"
@ -383,9 +383,9 @@
}
},
"node_modules/@types/ws": {
"version": "8.18.0",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.0.tgz",
"integrity": "sha512-8svvI3hMyvN0kKCJMvTJP/x6Y/EoQbepff882wL+Sn5QsXb3etnamgrJq4isrBxSJj5L2AuXcI0+bgkoAXGUJw==",
"version": "8.18.1",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
"license": "MIT",
"dependencies": {
"@types/node": "*"
@ -617,27 +617,31 @@
}
},
"node_modules/discord-api-types": {
"version": "0.37.119",
"resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.119.tgz",
"integrity": "sha512-WasbGFXEB+VQWXlo6IpW3oUv73Yuau1Ig4AZF/m13tXcTKnMpc/mHjpztIlz4+BM9FG9BHQkEXiPto3bKduQUg==",
"license": "MIT"
"version": "0.38.8",
"resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.8.tgz",
"integrity": "sha512-xuRXPD44FcbKHrQK15FS1HFlMRNJtsaZou/SVws18vQ7zHqmlxyDktMkZpyvD6gE2ctGOVYC/jUyoMMAyBWfcw==",
"license": "MIT",
"workspaces": [
"scripts/actions/documentation"
]
},
"node_modules/discord.js": {
"version": "14.18.0",
"resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.18.0.tgz",
"integrity": "sha512-SvU5kVUvwunQhN2/+0t55QW/1EHfB1lp0TtLZUSXVHDmyHTrdOj5LRKdR0zLcybaA15F+NtdWuWmGOX9lE+CAw==",
"version": "14.19.3",
"resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.19.3.tgz",
"integrity": "sha512-lncTRk0k+8Q5D3nThnODBR8fR8x2fM798o8Vsr40Krx0DjPwpZCuxxTcFMrXMQVOqM1QB9wqWgaXPg3TbmlHqA==",
"license": "Apache-2.0",
"dependencies": {
"@discordjs/builders": "^1.10.1",
"@discordjs/builders": "^1.11.2",
"@discordjs/collection": "1.5.3",
"@discordjs/formatters": "^0.6.0",
"@discordjs/rest": "^2.4.3",
"@discordjs/formatters": "^0.6.1",
"@discordjs/rest": "^2.5.0",
"@discordjs/util": "^1.1.1",
"@discordjs/ws": "^1.2.1",
"@discordjs/ws": "^1.2.2",
"@sapphire/snowflake": "3.5.3",
"discord-api-types": "^0.37.119",
"discord-api-types": "^0.38.1",
"fast-deep-equal": "3.1.3",
"lodash.snakecase": "4.1.1",
"magic-bytes.js": "^1.10.0",
"tslib": "^2.6.3",
"undici": "6.21.1"
},
@ -1007,9 +1011,9 @@
"license": "MIT"
},
"node_modules/magic-bytes.js": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.10.0.tgz",
"integrity": "sha512-/k20Lg2q8LE5xiaaSkMXk4sfvI+9EGEykFS4b0CHHGWqDYU0bGUFSwchNOMA56D7TCs9GwVTkqe9als1/ns8UQ==",
"version": "1.12.1",
"resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.12.1.tgz",
"integrity": "sha512-ThQLOhN86ZkJ7qemtVRGYM+gRgR8GEXNli9H/PMvpnZsE44Xfh3wx9kGJaldg314v85m+bFW6WBMaVHJc/c3zA==",
"license": "MIT"
},
"node_modules/make-error": {
@ -1563,9 +1567,9 @@
"license": "ISC"
},
"node_modules/ws": {
"version": "8.18.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz",
"integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==",
"version": "8.18.2",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz",
"integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
@ -1594,9 +1598,9 @@
}
},
"node_modules/zod": {
"version": "3.24.2",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz",
"integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==",
"version": "3.25.13",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.13.tgz",
"integrity": "sha512-Q8mvk2iWi7rTDfpQBsu4ziE7A6AxgzJ5hzRyRYQkoV3A3niYsXVwDaP1Kbz3nWav6S+VZ6k2OznFn8ZyDHvIrg==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"

View File

@ -19,14 +19,14 @@
"devDependencies": {
"@modelcontextprotocol/sdk": "^1.11.0",
"@types/express": "^5.0.1",
"@types/node": "^20.17.26",
"@types/node": "^20.17.50",
"ts-node": "^10.9.2",
"typescript": "^5.8.3"
},
"dependencies": {
"discord.js": "^14.18.0",
"discord.js": "^14.19.3",
"dotenv": "^16.5.0",
"express": "^5.1.0",
"zod": "^3.24.2"
"zod": "^3.25.13"
}
}

View File

@ -32,7 +32,28 @@ export const ReplyToForumSchema = z.object({
export const CreateTextChannelSchema = z.object({
guildId: z.string(),
channelName: z.string(),
topic: z.string().optional()
topic: z.string().optional(),
reason: z.string().optional()
});
// Category schemas
export const CreateCategorySchema = z.object({
guildId: z.string(),
name: z.string(),
position: z.number().optional(),
reason: z.string().optional()
});
export const EditCategorySchema = z.object({
categoryId: z.string(),
name: z.string().optional(),
position: z.number().optional(),
reason: z.string().optional()
});
export const DeleteCategorySchema = z.object({
categoryId: z.string(),
reason: z.string().optional()
});
export const DeleteChannelSchema = z.object({

View File

@ -26,7 +26,10 @@ import {
createWebhookHandler,
sendWebhookMessageHandler,
editWebhookHandler,
deleteWebhookHandler
deleteWebhookHandler,
createCategoryHandler,
editCategoryHandler,
deleteCategoryHandler
} from './tools/tools.js';
import { MCPTransport } from './transport.js';
import { info, error } from './logger.js';
@ -71,6 +74,15 @@ export class DiscordMCPServer {
try {
let toolResponse;
switch (name) {
case "discord_create_category":
toolResponse = await createCategoryHandler(args, this.toolContext);
return toolResponse;
case "discord_edit_category":
toolResponse = await editCategoryHandler(args, this.toolContext);
return toolResponse;
case "discord_delete_category":
toolResponse = await deleteCategoryHandler(args, this.toolContext);
return toolResponse;
case "discord_login":
toolResponse = await loginHandler(args, this.toolContext);
// Check the client state after login

View File

@ -1,4 +1,44 @@
export const toolList = [
{
name: "discord_create_category",
description: "Creates a new category in a Discord server.",
inputSchema: {
type: "object",
properties: {
guildId: { type: "string" },
name: { type: "string" },
position: { type: "number" },
reason: { type: "string" }
},
required: ["guildId", "name"]
}
},
{
name: "discord_edit_category",
description: "Edits an existing Discord category (name and position).",
inputSchema: {
type: "object",
properties: {
categoryId: { type: "string" },
name: { type: "string" },
position: { type: "number" },
reason: { type: "string" }
},
required: ["categoryId"]
}
},
{
name: "discord_delete_category",
description: "Deletes a Discord category by ID.",
inputSchema: {
type: "object",
properties: {
categoryId: { type: "string" },
reason: { type: "string" }
},
required: ["categoryId"]
}
},
{
name: "discord_login",
description: "Logs in to Discord using the configured token",

View File

@ -5,16 +5,113 @@ import {
CreateTextChannelSchema,
DeleteChannelSchema,
ReadMessagesSchema,
GetServerInfoSchema
GetServerInfoSchema,
CreateCategorySchema,
EditCategorySchema,
DeleteCategorySchema
} from "../schemas.js";
import { handleDiscordError } from "../errorHandler.js";
// Category creation handler
export async function createCategoryHandler(
args: unknown,
context: ToolContext
): Promise<ToolResponse> {
const { guildId, name, position, reason } = CreateCategorySchema.parse(args);
try {
if (!context.client.isReady()) {
return {
content: [{ type: "text", text: "Discord client not logged in. Please use discord_login tool first." }],
isError: true
};
}
const guild = await context.client.guilds.fetch(guildId);
if (!guild) {
return {
content: [{ type: "text", text: `Cannot find guild with ID: ${guildId}` }],
isError: true
};
}
const options: any = { name, type: ChannelType.GuildCategory };
if (typeof position === "number") options.position = position;
if (reason) options.reason = reason;
const category = await guild.channels.create(options);
return {
content: [{ type: "text", text: `Successfully created category "${name}" with ID: ${category.id}` }]
};
} catch (error) {
return handleDiscordError(error);
}
}
// Category edit handler
export async function editCategoryHandler(
args: unknown,
context: ToolContext
): Promise<ToolResponse> {
const { categoryId, name, position, reason } = EditCategorySchema.parse(args);
try {
if (!context.client.isReady()) {
return {
content: [{ type: "text", text: "Discord client not logged in. Please use discord_login tool first." }],
isError: true
};
}
const category = await context.client.channels.fetch(categoryId);
if (!category || category.type !== ChannelType.GuildCategory) {
return {
content: [{ type: "text", text: `Cannot find category with ID: ${categoryId}` }],
isError: true
};
}
const update: any = {};
if (name) update.name = name;
if (typeof position === "number") update.position = position;
if (reason) update.reason = reason;
await category.edit(update);
return {
content: [{ type: "text", text: `Successfully edited category with ID: ${categoryId}` }]
};
} catch (error) {
return handleDiscordError(error);
}
}
// Category deletion handler
export async function deleteCategoryHandler(
args: unknown,
context: ToolContext
): Promise<ToolResponse> {
const { categoryId, reason } = DeleteCategorySchema.parse(args);
try {
if (!context.client.isReady()) {
return {
content: [{ type: "text", text: "Discord client not logged in. Please use discord_login tool first." }],
isError: true
};
}
const category = await context.client.channels.fetch(categoryId);
if (!category || category.type !== ChannelType.GuildCategory) {
return {
content: [{ type: "text", text: `Cannot find category with ID: ${categoryId}` }],
isError: true
};
}
await category.delete(reason || "Category deleted via API");
return {
content: [{ type: "text", text: `Successfully deleted category with ID: ${categoryId}` }]
};
} catch (error) {
return handleDiscordError(error);
}
}
// Text channel creation handler
export async function createTextChannelHandler(
args: unknown,
context: ToolContext
): Promise<ToolResponse> {
const { guildId, channelName, topic } = CreateTextChannelSchema.parse(args);
const { guildId, channelName, topic, reason } = CreateTextChannelSchema.parse(args);
try {
if (!context.client.isReady()) {
return {
@ -32,11 +129,13 @@ export async function createTextChannelHandler(
}
// Create the text channel
const channel = await guild.channels.create({
const channelOptions: any = {
name: channelName,
type: ChannelType.GuildText,
topic: topic
});
type: ChannelType.GuildText
};
if (topic) channelOptions.topic = topic;
if (reason) channelOptions.reason = reason;
const channel = await guild.channels.create(channelOptions);
return {
content: [{

View File

@ -14,7 +14,10 @@ import {
createTextChannelHandler,
deleteChannelHandler,
readMessagesHandler,
getServerInfoHandler
getServerInfoHandler,
createCategoryHandler,
editCategoryHandler,
deleteCategoryHandler
} from './channel.js';
import {
addReactionHandler,
@ -49,7 +52,10 @@ export {
createWebhookHandler,
sendWebhookMessageHandler,
editWebhookHandler,
deleteWebhookHandler
deleteWebhookHandler,
createCategoryHandler,
editCategoryHandler,
deleteCategoryHandler
};
// Export common types