Skip to content

Commit 1338f99

Browse files
authored
Merge branch 'main' into feat/roles-and-permissions
2 parents f5e6859 + 53ed667 commit 1338f99

File tree

13 files changed

+986
-204
lines changed

13 files changed

+986
-204
lines changed

Dockerfile

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ RUN npm run build
1919
# Expose HTTP port
2020
EXPOSE 8080
2121

22+
# entrypoint to run the MCP server
23+
ENTRYPOINT ["node", "build/app.js"]
2224
# Default command to run the MCP server with HTTP transport
23-
# Use ENTRYPOINT + CMD for flexibility
24-
ENTRYPOINT ["node", "build/index.js"]
25-
CMD ["--transport", "stdio", "--port", "8080"]
25+
CMD ["--transport", "http", "--port", "8080"]

Makefile

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
.PHONY: help
2+
.DEFAULT_GOAL := help
3+
4+
# initialise .env file for all targets if it exists
5+
-include .env
6+
7+
help: ## Show this help.
8+
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
9+
10+
# Document each target that should appear in help using a comment placed after the target's colon, starting with '##' (as shown above).
11+
# Targets without such a comment will not appear in the help output.
12+
13+
run-inspector: ## Run the MCP inspector.
14+
@npx @modelcontextprotocol/inspector -e DISCORD_TOKEN=$(DISCORD_TOKEN)

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,9 @@ You can use Docker containers with both Claude and Cursor:
293293

294294
- `discord_create_text_channel`: Create a text channel
295295
- `discord_delete_channel`: Delete a channel
296+
- `discord_create_category`: Create a channel category
297+
- `discord_edit_category`: Edit a channel category
298+
- `discord_delete_category`: Delete a channel category
296299

297300
### Forum Functions
298301

package-lock.json

Lines changed: 203 additions & 115 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111
"test": "echo \"Error: no test specified\" && exit 1",
1212
"build": "tsc",
1313
"start": "node build/index.js",
14+
"start-app": "node build/app.js",
1415
"dev": "node --loader ts-node/esm src/index.ts",
16+
"dev-app": "node --loader ts-node/esm src/app.ts",
1517
"test-api": "node test-api.js",
1618
"prepublishOnly": "npm run build"
1719
},

src/app.ts

Lines changed: 344 additions & 0 deletions
Large diffs are not rendered by default.

src/notifications.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { Server } from '@modelcontextprotocol/sdk/server'
2+
3+
export type Level = 'debug' | 'info' | 'notice' | 'warning' | 'error' | 'critical' | 'alert' | 'emergency';
4+
5+
const levelPriority: Record<Level, number> = {
6+
debug: 0,
7+
info: 1,
8+
notice: 2,
9+
warning: 3,
10+
error: 4,
11+
critical: 5,
12+
alert: 6,
13+
emergency: 7,
14+
};
15+
16+
export let currentLevel: Level = 'info';
17+
18+
export function setLevel(level: Level) {
19+
if (!levelPriority.hasOwnProperty(level)) return false;
20+
currentLevel = level;
21+
return true;
22+
}
23+
24+
export function log(server: Server, message: string, level: Level = 'info') {
25+
if (levelPriority[level] < levelPriority[currentLevel]) return;
26+
server.sendLoggingMessage(
27+
{
28+
level: level,
29+
logger: 'mcp-discord',
30+
data: { text: message },
31+
}
32+
);
33+
}
34+
35+
export function info(server: Server, message: string) {
36+
log(server, message, 'info');
37+
}
38+
39+
export function warning(server: Server, message: string) {
40+
log(server, message, 'warning');
41+
}
42+
43+
export function error(server: Server, message: string) {
44+
log(server, message, 'error');
45+
}

src/schemas.ts

Lines changed: 140 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,38 @@
11
import { z } from "zod";
22

33
export const DiscordLoginSchema = z.object({
4-
token: z.string().optional()
4+
token: z.string({ description: "The bot token to use for login." }).optional()
5+
}, {
6+
description: "Login to Discord using a bot token. If no token is provided, the bot will attempt to use the token from the environment variable DISCORD_TOKEN."
57
});
68

79
export const SendMessageSchema = z.object({
8-
channelId: z.string(),
9-
message: z.string(),
10-
replyToMessageId: z.string().optional()
10+
channelId: z.string({ description: "The ID of the channel to send the message to." }),
11+
message: z.string({ description: "The content of the message to send." }),
12+
replyToMessageId: z.string({ description: "The ID of the message to reply to, if any." }).optional()
13+
}, {
14+
description: "Send a message to a specified channel, optionally as a reply to another message."
1115
});
1216

1317
export const GetForumChannelsSchema = z.object({
14-
guildId: z.string()
18+
guildId: z.string({ description: "The ID of the server (guild) to get forum channels from." })
19+
}, {
20+
description: "Get all forum channels in a specified server (guild)."
1521
});
1622

1723
export const CreateForumPostSchema = z.object({
18-
forumChannelId: z.string(),
19-
title: z.string(),
20-
content: z.string(),
21-
tags: z.array(z.string()).optional()
24+
forumChannelId: z.string({ description: "The ID of the forum channel where the thread will be created." }),
25+
title: z.string({ description: "The title of the forum post (thread)." }),
26+
content: z.string({ description: "The body content of the forum post." }),
27+
tags: z.array(z.string({ description: "A tag to attach to the forum post." })).optional()
28+
}, {
29+
description: "Create a new forum post (thread) in a specified forum channel."
2230
});
2331

2432
export const GetForumPostSchema = z.object({
25-
threadId: z.string()
33+
threadId: z.string({ description: "The ID of the forum thread to retrieve." })
34+
}, {
35+
description: "Get details of a specific forum post (thread) by its ID."
2636
});
2737

2838
export const ListForumThreadsSchema = z.object({
@@ -32,8 +42,10 @@ export const ListForumThreadsSchema = z.object({
3242
});
3343

3444
export const ReplyToForumSchema = z.object({
35-
threadId: z.string(),
36-
message: z.string()
45+
threadId: z.string({ description: "The ID of the forum thread to reply to." }),
46+
message: z.string({ description: "The content of the reply message." })
47+
}, {
48+
description: "Reply to a specific forum post (thread) by its ID."
3749
});
3850

3951
export const CreateTextChannelSchema = z.object({
@@ -46,100 +58,150 @@ export const CreateTextChannelSchema = z.object({
4658

4759
// Category schemas
4860
export const CreateCategorySchema = z.object({
49-
guildId: z.string(),
50-
name: z.string(),
51-
position: z.number().optional(),
52-
reason: z.string().optional()
61+
guildId: z.string({ description: "The ID of the server (guild) where the category will be created." }),
62+
name: z.string({ description: "The name of the category to create." }),
63+
position: z.number({ description: "Optional sorting position index for the category." }).optional(),
64+
reason: z.string({ description: "Optional reason for audit logs when creating the category." }).optional()
65+
}, {
66+
description: "Create a new category in a specified server (guild)."
5367
});
5468

5569
export const EditCategorySchema = z.object({
56-
categoryId: z.string(),
57-
name: z.string().optional(),
58-
position: z.number().optional(),
59-
reason: z.string().optional()
70+
categoryId: z.string({ description: "The ID of the category to edit." }),
71+
name: z.string({ description: "New name for the category (optional)." }).optional(),
72+
position: z.number({ description: "New position index for the category (optional)." }).optional(),
73+
reason: z.string({ description: "Optional reason for audit logs when editing the category." }).optional()
74+
}, {
75+
description: "Edit an existing category's properties."
6076
});
6177

6278
export const DeleteCategorySchema = z.object({
63-
categoryId: z.string(),
64-
reason: z.string().optional()
79+
categoryId: z.string({ description: "The ID of the category to delete." }),
80+
reason: z.string({ description: "Optional reason for audit logs when deleting the category." }).optional()
81+
}, {
82+
description: "Delete a category by its ID."
6583
});
6684

6785
export const DeleteChannelSchema = z.object({
68-
channelId: z.string(),
69-
reason: z.string().optional()
86+
channelId: z.string({ description: "The ID of the channel to delete." }),
87+
reason: z.string({ description: "Optional reason for audit logs when deleting the channel." }).optional()
88+
}, {
89+
description: "Delete a channel by its ID."
7090
});
7191

7292
export const ReadMessagesSchema = z.object({
73-
channelId: z.string(),
74-
limit: z.number().min(1).max(100).optional().default(50)
93+
channelId: z.string({ description: "The ID of the channel to read messages from." }),
94+
limit: z.number({ description: "How many recent messages to fetch (1-100)." }).min(1).max(100).optional().default(50)
95+
}, {
96+
description: "Read recent messages from a specified channel."
7597
});
7698

7799
export const GetServerInfoSchema = z.object({
78-
guildId: z.string()
100+
guildId: z.string({ description: "The ID of the server (guild) to get information for." })
101+
}, {
102+
description: "Get information about a specific server (guild) by its ID."
79103
});
80104

81105
export const AddReactionSchema = z.object({
82-
channelId: z.string(),
83-
messageId: z.string(),
84-
emoji: z.string()
106+
channelId: z.string({ description: "The ID of the channel containing the message to react to." }),
107+
messageId: z.string({ description: "The ID of the message to add a reaction to." }),
108+
emoji: z.string({ description: "The emoji to use for the reaction (unicode or custom)." })
109+
}, {
110+
description: "Add a reaction to a specific message in a channel."
85111
});
86112

87113
export const AddMultipleReactionsSchema = z.object({
88-
channelId: z.string(),
89-
messageId: z.string(),
90-
emojis: z.array(z.string())
114+
channelId: z.string({ description: "The ID of the channel containing the message to react to." }),
115+
messageId: z.string({ description: "The ID of the message to add reactions to." }),
116+
emojis: z.array(z.string({ description: "An emoji to add (unicode or custom)." }))
117+
}, {
118+
description: "Add multiple reactions to a specific message in a channel."
91119
});
92120

93121
export const RemoveReactionSchema = z.object({
94-
channelId: z.string(),
95-
messageId: z.string(),
96-
emoji: z.string(),
97-
userId: z.string().optional()
122+
channelId: z.string({ description: "The ID of the channel containing the message to modify reactions on." }),
123+
messageId: z.string({ description: "The ID of the message to remove the reaction from." }),
124+
emoji: z.string({ description: "The emoji reaction to remove." }),
125+
userId: z.string({ description: "Optional ID of the user whose reaction should be removed; if omitted, removes the current bot's reaction." }).optional()
126+
}, {
127+
description: "Remove a reaction from a specific message in a channel."
98128
});
99129

100130
export const DeleteForumPostSchema = z.object({
131+
threadId: z.string({ description: "The ID of the forum thread to delete." }),
132+
reason: z.string({ description: "Optional reason for audit logs when deleting the forum post." }).optional()
133+
}, {
134+
description: "Delete a forum post (thread) by its ID."
135+
});
136+
137+
export const GetForumTagsSchema = z.object({
138+
forumChannelId: z.string()
139+
});
140+
141+
export const UpdateForumPostSchema = z.object({
101142
threadId: z.string(),
102-
reason: z.string().optional()
143+
name: z.string().optional(),
144+
tags: z.array(z.string()).optional(),
145+
archived: z.boolean().optional(),
146+
locked: z.boolean().optional()
103147
});
104148

105-
export const DeleteMessageSchema = z.object({
149+
export const EditMessageSchema = z.object({
106150
channelId: z.string(),
107151
messageId: z.string(),
108-
reason: z.string().optional()
152+
content: z.string()
153+
});
154+
155+
export const DeleteMessageSchema = z.object({
156+
channelId: z.string({ description: "The ID of the channel containing the message to delete." }),
157+
messageId: z.string({ description: "The ID of the message to delete." }),
158+
reason: z.string({ description: "Optional reason for audit logs when deleting the message." }).optional()
159+
}, {
160+
description: "Delete a message by its ID in a specified channel."
109161
});
110162

111163
export const CreateWebhookSchema = z.object({
112-
channelId: z.string(),
113-
name: z.string(),
114-
avatar: z.string().optional(),
115-
reason: z.string().optional()
164+
channelId: z.string({ description: "The ID of the channel to create the webhook in." }),
165+
name: z.string({ description: "The name to assign to the webhook." }),
166+
avatar: z.string({ description: "Optional avatar URL or data for the webhook." }).optional(),
167+
reason: z.string({ description: "Optional reason for audit logs when creating the webhook." }).optional()
168+
}, {
169+
description: "Create a webhook in a specified channel."
116170
});
117171

118172
export const SendWebhookMessageSchema = z.object({
119-
webhookId: z.string(),
120-
webhookToken: z.string(),
121-
content: z.string(),
122-
username: z.string().optional(),
123-
avatarURL: z.string().optional(),
124-
threadId: z.string().optional()
173+
webhookId: z.string({ description: "The ID of the webhook to send the message with." }),
174+
webhookToken: z.string({ description: "The token for the webhook (used for authentication)." }),
175+
content: z.string({ description: "The message content to send via the webhook." }),
176+
username: z.string({ description: "Optional username to display for the webhook message." }).optional(),
177+
avatarURL: z.string({ description: "Optional avatar URL to display for the webhook message." }).optional(),
178+
threadId: z.string({ description: "Optional ID of the thread to post the webhook message into." }).optional()
179+
}, {
180+
description: "Send a message using a webhook."
125181
});
126182

127183
export const EditWebhookSchema = z.object({
128-
webhookId: z.string(),
129-
webhookToken: z.string().optional(),
130-
name: z.string().optional(),
131-
avatar: z.string().optional(),
132-
channelId: z.string().optional(),
133-
reason: z.string().optional()
184+
webhookId: z.string({ description: "The ID of the webhook to edit." }),
185+
webhookToken: z.string({ description: "Optional token for the webhook if required to authorize edits." }).optional(),
186+
name: z.string({ description: "Optional new name for the webhook." }).optional(),
187+
avatar: z.string({ description: "Optional new avatar URL or data for the webhook." }).optional(),
188+
channelId: z.string({ description: "Optional channel ID to move the webhook to." }).optional(),
189+
reason: z.string({ description: "Optional reason for audit logs when editing the webhook." }).optional()
190+
}, {
191+
description: "Edit a webhook's properties."
134192
});
135193

136194
export const DeleteWebhookSchema = z.object({
137-
webhookId: z.string(),
138-
webhookToken: z.string().optional(),
139-
reason: z.string().optional()
195+
webhookId: z.string({ description: "The ID of the webhook to delete." }),
196+
webhookToken: z.string({ description: "Optional token for the webhook if required for deletion." }).optional(),
197+
reason: z.string({ description: "Optional reason for audit logs when deleting the webhook." }).optional()
198+
}, {
199+
description: "Delete a webhook by its ID and token."
140200
});
141201

142-
export const ListServersSchema = z.object({});
202+
export const ListServersSchema = z.object({}, {
203+
description: "List all servers (guilds) the bot is a member of."
204+
});
143205

144206
// Role schemas
145207
export const ListRolesSchema = z.object({
@@ -224,19 +286,21 @@ export const CreateVoiceChannelSchema = z.object({
224286
});
225287

226288
export const SearchMessagesSchema = z.object({
227-
guildId: z.string().min(1, "guildId is required"),
228-
// Optional filters
229-
content: z.string().optional(),
230-
authorId: z.string().optional(),
231-
mentions: z.string().optional(),
232-
has: z.enum(['link','embed','file','poll','image','video','sound','sticker','snapshot']).optional(),
233-
maxId: z.string().optional(),
234-
minId: z.string().optional(),
235-
channelId: z.string().optional(),
236-
pinned: z.boolean().optional(),
237-
authorType: z.enum(['user','bot','webhook']).optional(),
238-
sortBy: z.enum(['timestamp','relevance']).optional(),
239-
sortOrder: z.enum(['desc','asc']).optional(),
240-
limit: z.number().min(1).max(100).default(25).optional(),
241-
offset: z.number().min(0).default(0).optional()
242-
});
289+
guildId: z.string({ description: "The ID of the server (guild) to search in." }).min(1, "guildId is required"),
290+
// Optional filters
291+
content: z.string({ description: "Search for messages that contain this text." }).optional(),
292+
authorId: z.string({ description: "Filter messages to those authored by this user ID." }).optional(),
293+
mentions: z.string({ description: "Filter messages that mention a specific user or role ID." }).optional(),
294+
has: z.enum(['link', 'embed', 'file', 'poll', 'image', 'video', 'sound', 'sticker', 'snapshot'], { description: "Filter messages that contain a specific type of content." }).optional(),
295+
maxId: z.string({ description: "Only include messages with IDs less than or equal to this (pagination)." }).optional(),
296+
minId: z.string({ description: "Only include messages with IDs greater than or equal to this (pagination)." }).optional(),
297+
channelId: z.string({ description: "If provided, restrict search to a specific channel ID." }).optional(),
298+
pinned: z.boolean({ description: "If true, only include pinned messages; if false, only include unpinned; if omitted, include both." }).optional(),
299+
authorType: z.enum(['user', 'bot', 'webhook'], { description: "Filter by the type of author (user, bot, or webhook)." }).optional(),
300+
sortBy: z.enum(['timestamp', 'relevance'], { description: "Field to sort search results by." }).optional(),
301+
sortOrder: z.enum(['desc', 'asc'], { description: "Sort direction for results." }).optional(),
302+
limit: z.number({ description: "Number of results to return (1-100)." }).min(1).max(100).default(25).optional(),
303+
offset: z.number({ description: "Number of results to skip (for pagination)." }).min(0).default(0).optional()
304+
}, {
305+
description: "Search messages in a server with various filters."
306+
});

0 commit comments

Comments
 (0)