From 470842e396527f7462d83c12eab25a343105149f Mon Sep 17 00:00:00 2001 From: Sooty <7614538+SootyOwl@users.noreply.github.com> Date: Wed, 15 Oct 2025 16:03:09 +0100 Subject: [PATCH] feat(server): add search messages handler and schema - Implement searchMessagesHandler to search for messages in a Discord server. - Add SearchMessagesSchema for input validation. - Update toolList to include discord_search_messages tool. - Modify transport and tools to handle new search messages functionality. --- src/schemas.ts | 19 +++++++++++++++- src/server.ts | 8 ++++++- src/toolList.ts | 23 ++++++++++++++++++++ src/tools/server.ts | 53 +++++++++++++++++++++++++++++++++++++++++++++ src/tools/tools.ts | 6 ++++- src/transport.ts | 12 +++++++--- 6 files changed, 115 insertions(+), 6 deletions(-) create mode 100644 src/tools/server.ts diff --git a/src/schemas.ts b/src/schemas.ts index 68a58ef..f7cadf0 100644 --- a/src/schemas.ts +++ b/src/schemas.ts @@ -131,4 +131,21 @@ export const DeleteWebhookSchema = z.object({ reason: z.string().optional() }); -export const ListServersSchema = z.object({}); \ No newline at end of file +export const ListServersSchema = z.object({}); + +export const SearchMessagesSchema = z.object({ + guildId: z.string().min(1, "guildId is required"), + // Optional filters + authorId: z.string().optional(), + mentions: z.string().optional(), + has: z.enum(['link','embed','file','poll','image','video','sound','sticker','snapshot']).optional(), + maxId: z.string().optional(), + minId: z.string().optional(), + channelId: z.string().optional(), + pinned: z.boolean().optional(), + authorType: z.enum(['user','bot','webhook']).optional(), + sortBy: z.enum(['timestamp','relevance']).optional(), + sortOrder: z.enum(['desc','asc']).optional(), + limit: z.number().min(1).max(100).default(25).optional(), + offset: z.number().min(0).default(0).optional() +}); \ No newline at end of file diff --git a/src/server.ts b/src/server.ts index 4b328fe..d086f86 100644 --- a/src/server.ts +++ b/src/server.ts @@ -30,7 +30,8 @@ import { createCategoryHandler, editCategoryHandler, deleteCategoryHandler, - listServersHandler + listServersHandler, + searchMessagesHandler } from './tools/tools.js'; import { MCPTransport } from './transport.js'; import { info, error } from './logger.js'; @@ -184,6 +185,11 @@ export class DiscordMCPServer { toolResponse = await listServersHandler(args, this.toolContext); return toolResponse; + case "discord_search_messages": + this.logClientState("before discord_search_messages handler"); + toolResponse = await searchMessagesHandler(args, this.toolContext); + return toolResponse; + default: throw new Error(`Unknown tool: ${name}`); } diff --git a/src/toolList.ts b/src/toolList.ts index 5d70a28..7b4bd7b 100644 --- a/src/toolList.ts +++ b/src/toolList.ts @@ -301,5 +301,28 @@ export const toolList = [ properties: {}, required: [] } + }, + { + name: "discord_search_messages", + description: "Searches for messages in a Discord server", + inputSchema: { + type: "object", + properties: { + guildId: { type: "string", description: "The ID of the Discord server (guild) to search within" }, + authorId: { type: "string", description: "Filter messages by a specific user ID" }, + mentions: { type: "string", description: "Filter messages that mention a specific user ID" }, + has: { type: "string", description: "Filter messages that contain specific content types (e.g., link, embed, file, poll, image, video, sound, sticker, snapshot)" }, + maxId: { type: "string", description: "Filter messages with IDs less than this value (messages before this ID)" }, + minId: { type: "string", description: "Filter messages with IDs greater than this value (messages after this ID)" }, + channelId: { type: "string", description: "Filter messages within a specific channel ID" }, + pinned: { type: "boolean", description: "Filter messages based on whether they are pinned" }, + authorType: { type: "string", description: "Filter messages by author type (user, bot, webhook)" }, + sortBy: { type: "string", description: "Sort results by 'timestamp' or 'relevance'" }, + sortOrder: { type: "string", description: "Sort order: 'desc' for descending or 'asc' for ascending" }, + limit: { type: "number", description: "Maximum number of messages to return (default 25, max 100)" }, + offset: { type: "number", description: "Number of messages to skip (for pagination)" } + }, + required: ["guildId"] + } } ]; \ No newline at end of file diff --git a/src/tools/server.ts b/src/tools/server.ts new file mode 100644 index 0000000..556421c --- /dev/null +++ b/src/tools/server.ts @@ -0,0 +1,53 @@ +import { handleDiscordError } from "../errorHandler.js"; +import { SearchMessagesSchema } from "../schemas.js"; +import { ToolContext, ToolResponse } from "./types.js"; + + +// Search server messages handler +export async function searchMessagesHandler( + args: unknown, + context: ToolContext +): Promise { + const { guildId, authorId, mentions, has, maxId, minId, channelId, pinned, authorType, sortBy, sortOrder, limit, offset } = SearchMessagesSchema.parse(args); + try { + if (!context.client.isReady()) { + return { + content: [{ type: "text", text: "Discord client not logged in." }], + 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 + }; + } + + // Note: Discord.js does not support guild message search natively. + // This requires direct API calls or using a library that supports it. + // Here we will construct the API request using context.client.rest + const params = new URLSearchParams(); + if (authorId) params.append('author_id', authorId); + if (mentions) params.append('mentions', mentions); + if (has) params.append('has', has); + if (maxId) params.append('max_id', maxId); + if (minId) params.append('min_id', minId); + if (channelId) params.append('channel_id', channelId); + if (typeof pinned === 'boolean') params.append('pinned', String(pinned)); + if (authorType) params.append('author_type', authorType); + if (sortBy) params.append('sort_by', sortBy); + if (sortOrder) params.append('sort_order', sortOrder); + params.append('limit', String(limit || 25)); + params.append('offset', String(offset || 0)); + + const response = await context.client.rest.get(`/guilds/${guildId}/messages/search?${params.toString()}`); + + return { + content: [{ type: "text", text: JSON.stringify(response, null, 2) }] + }; + } catch (error) { + return handleDiscordError(error); + } +} diff --git a/src/tools/tools.ts b/src/tools/tools.ts index 1134a2d..8312fb7 100644 --- a/src/tools/tools.ts +++ b/src/tools/tools.ts @@ -20,6 +20,9 @@ import { deleteCategoryHandler, listServersHandler } from './channel.js'; +import { + searchMessagesHandler +} from "./server.js"; import { addReactionHandler, addMultipleReactionsHandler, @@ -57,7 +60,8 @@ export { createCategoryHandler, editCategoryHandler, deleteCategoryHandler, - listServersHandler + listServersHandler, + searchMessagesHandler }; // Export common types diff --git a/src/transport.ts b/src/transport.ts index 79bfa9d..1be5716 100644 --- a/src/transport.ts +++ b/src/transport.ts @@ -27,7 +27,8 @@ import { editCategoryHandler, createCategoryHandler, deleteCategoryHandler, - listServersHandler + listServersHandler, + searchMessagesHandler } from './tools/tools.js'; import { Client, GatewayIntentBits } from "discord.js"; import { info, error } from './logger.js'; @@ -215,6 +216,7 @@ export class StreamableHttpTransport implements MCPTransport { case 'discord_edit_category': case 'discord_delete_category': case 'discord_list_servers': + case 'discord_search_messages': // Check if client is logged in if (!this.toolContext!.client.isReady()) { error(`Client not ready for method ${method}, client state: ${JSON.stringify({ @@ -338,8 +340,9 @@ export class StreamableHttpTransport implements MCPTransport { break; case 'discord_list_servers': result = await listServersHandler(params, this.toolContext!); - break; - + case 'discord_search_messages': + result = await searchMessagesHandler(params, this.toolContext!); + break;break; } break; @@ -507,6 +510,9 @@ export class StreamableHttpTransport implements MCPTransport { case 'discord_list_servers': result = await listServersHandler(toolArgs, this.toolContext!); break; + case 'discord_search_messages': + result = await searchMessagesHandler(toolArgs, this.toolContext!); + break; default: return res.status(400).json({