Merge pull request #16 from SootyOwl/feat/add-message-search
Add search messages tool
This commit is contained in:
commit
306f6abe81
|
|
@ -14,6 +14,7 @@ MCP-Discord provides the following Discord-related functionalities:
|
|||
- Login to Discord bot
|
||||
- List servers the bot is a member of
|
||||
- Get server information
|
||||
- Search messages in a server
|
||||
- Read/delete channel messages
|
||||
- Send messages to specified channels (using either channel IDs or channel names)
|
||||
- Retrieve forum channel lists
|
||||
|
|
@ -288,6 +289,7 @@ You can use Docker containers with both Claude and Cursor:
|
|||
|
||||
### Messages and Reactions
|
||||
|
||||
- `discord_search_messages`: Search messages in a server
|
||||
- `discord_read_messages`: Read channel messages
|
||||
- `discord_add_reaction`: Add a reaction to a message
|
||||
- `discord_add_multiple_reactions`: Add multiple reactions to a message
|
||||
|
|
|
|||
|
|
@ -132,3 +132,21 @@ export const DeleteWebhookSchema = z.object({
|
|||
});
|
||||
|
||||
export const ListServersSchema = z.object({});
|
||||
|
||||
export const SearchMessagesSchema = z.object({
|
||||
guildId: z.string().min(1, "guildId is required"),
|
||||
// Optional filters
|
||||
content: z.string().optional(),
|
||||
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()
|
||||
});
|
||||
|
|
@ -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}`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -301,5 +301,29 @@ 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" },
|
||||
content: { type: "string", description: "Search for messages containing specific text" },
|
||||
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)", enum: ["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)", enum: ["user", "bot", "webhook"] },
|
||||
sortBy: { type: "string", description: "Sort results by 'timestamp' or 'relevance'", enum: ["timestamp", "relevance"] },
|
||||
sortOrder: { type: "string", description: "Sort order: 'desc' for descending or 'asc' for ascending", enum: ["desc", "asc"] },
|
||||
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"]
|
||||
}
|
||||
}
|
||||
];
|
||||
|
|
@ -5,11 +5,9 @@ import {
|
|||
CreateTextChannelSchema,
|
||||
DeleteChannelSchema,
|
||||
ReadMessagesSchema,
|
||||
GetServerInfoSchema,
|
||||
CreateCategorySchema,
|
||||
EditCategorySchema,
|
||||
DeleteCategorySchema,
|
||||
ListServersSchema
|
||||
DeleteCategorySchema
|
||||
} from "../schemas.js";
|
||||
import { handleDiscordError } from "../errorHandler.js";
|
||||
|
||||
|
|
@ -261,135 +259,3 @@ export async function readMessagesHandler(
|
|||
return handleDiscordError(error);
|
||||
}
|
||||
}
|
||||
|
||||
// Server information handler
|
||||
export async function getServerInfoHandler(
|
||||
args: unknown,
|
||||
context: ToolContext
|
||||
): Promise<ToolResponse> {
|
||||
const { guildId } = GetServerInfoSchema.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
|
||||
};
|
||||
}
|
||||
|
||||
// Fetch additional server data
|
||||
await guild.fetch();
|
||||
|
||||
// Fetch channel information
|
||||
const channels = await guild.channels.fetch();
|
||||
|
||||
// Categorize channels by type
|
||||
const channelsByType = {
|
||||
text: channels.filter(c => c?.type === ChannelType.GuildText).size,
|
||||
voice: channels.filter(c => c?.type === ChannelType.GuildVoice).size,
|
||||
category: channels.filter(c => c?.type === ChannelType.GuildCategory).size,
|
||||
forum: channels.filter(c => c?.type === ChannelType.GuildForum).size,
|
||||
announcement: channels.filter(c => c?.type === ChannelType.GuildAnnouncement).size,
|
||||
stage: channels.filter(c => c?.type === ChannelType.GuildStageVoice).size,
|
||||
total: channels.size
|
||||
};
|
||||
|
||||
// Get detailed information for all channels
|
||||
const channelDetails = channels.map(channel => {
|
||||
if (!channel) return null;
|
||||
|
||||
return {
|
||||
id: channel.id,
|
||||
name: channel.name,
|
||||
type: ChannelType[channel.type] || channel.type,
|
||||
categoryId: channel.parentId,
|
||||
position: channel.position,
|
||||
// Only add topic for text channels
|
||||
topic: 'topic' in channel ? channel.topic : null,
|
||||
};
|
||||
}).filter(c => c !== null); // Filter out null values
|
||||
|
||||
// Group channels by type
|
||||
const groupedChannels = {
|
||||
text: channelDetails.filter(c => c.type === ChannelType[ChannelType.GuildText] || c.type === ChannelType.GuildText),
|
||||
voice: channelDetails.filter(c => c.type === ChannelType[ChannelType.GuildVoice] || c.type === ChannelType.GuildVoice),
|
||||
category: channelDetails.filter(c => c.type === ChannelType[ChannelType.GuildCategory] || c.type === ChannelType.GuildCategory),
|
||||
forum: channelDetails.filter(c => c.type === ChannelType[ChannelType.GuildForum] || c.type === ChannelType.GuildForum),
|
||||
announcement: channelDetails.filter(c => c.type === ChannelType[ChannelType.GuildAnnouncement] || c.type === ChannelType.GuildAnnouncement),
|
||||
stage: channelDetails.filter(c => c.type === ChannelType[ChannelType.GuildStageVoice] || c.type === ChannelType.GuildStageVoice),
|
||||
all: channelDetails
|
||||
};
|
||||
|
||||
// Get member count
|
||||
const approximateMemberCount = guild.approximateMemberCount || "unknown";
|
||||
|
||||
// Format guild information
|
||||
const guildInfo = {
|
||||
id: guild.id,
|
||||
name: guild.name,
|
||||
description: guild.description,
|
||||
icon: guild.iconURL(),
|
||||
owner: guild.ownerId,
|
||||
createdAt: guild.createdAt,
|
||||
memberCount: approximateMemberCount,
|
||||
channels: {
|
||||
count: channelsByType,
|
||||
details: groupedChannels
|
||||
},
|
||||
features: guild.features,
|
||||
premium: {
|
||||
tier: guild.premiumTier,
|
||||
subscriptions: guild.premiumSubscriptionCount
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify(guildInfo, null, 2) }]
|
||||
};
|
||||
} catch (error) {
|
||||
return handleDiscordError(error);
|
||||
}
|
||||
}
|
||||
|
||||
// List servers handler
|
||||
export async function listServersHandler(
|
||||
args: unknown,
|
||||
context: ToolContext
|
||||
): Promise<ToolResponse> {
|
||||
ListServersSchema.parse(args);
|
||||
try {
|
||||
if (!context.client.isReady()) {
|
||||
return {
|
||||
content: [{ type: "text", text: "Discord client not logged in." }],
|
||||
isError: true
|
||||
};
|
||||
}
|
||||
|
||||
const guilds = await context.client.guilds.fetch();
|
||||
|
||||
if (guilds.size === 0) {
|
||||
return {
|
||||
content: [{ type: "text", text: "No servers found. The bot is not a member of any servers." }]
|
||||
};
|
||||
}
|
||||
|
||||
const guildsInfo = guilds.map(guild => ({
|
||||
id: guild.id,
|
||||
name: guild.name,
|
||||
icon: guild.iconURL()
|
||||
}));
|
||||
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify(guildsInfo, null, 2) }]
|
||||
};
|
||||
} catch (error) {
|
||||
return handleDiscordError(error);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,189 @@
|
|||
import { ChannelType } from "discord.js";
|
||||
import { handleDiscordError } from "../errorHandler.js";
|
||||
import { GetServerInfoSchema, ListServersSchema, SearchMessagesSchema } from "../schemas.js";
|
||||
import { ToolContext, ToolResponse } from "./types.js";
|
||||
|
||||
|
||||
// Search server messages handler
|
||||
export async function searchMessagesHandler(
|
||||
args: unknown,
|
||||
context: ToolContext
|
||||
): Promise<ToolResponse> {
|
||||
const { guildId, content, 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 (content) params.append('content', content);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// List servers handler
|
||||
export async function listServersHandler(
|
||||
args: unknown,
|
||||
context: ToolContext
|
||||
): Promise<ToolResponse> {
|
||||
ListServersSchema.parse(args);
|
||||
try {
|
||||
if (!context.client.isReady()) {
|
||||
return {
|
||||
content: [{ type: "text", text: "Discord client not logged in." }],
|
||||
isError: true
|
||||
};
|
||||
}
|
||||
|
||||
const guilds = await context.client.guilds.fetch();
|
||||
|
||||
if (guilds.size === 0) {
|
||||
return {
|
||||
content: [{ type: "text", text: "No servers found. The bot is not a member of any servers." }]
|
||||
};
|
||||
}
|
||||
|
||||
const guildsInfo = guilds.map(guild => ({
|
||||
id: guild.id,
|
||||
name: guild.name,
|
||||
icon: guild.iconURL()
|
||||
}));
|
||||
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify(guildsInfo, null, 2) }]
|
||||
};
|
||||
} catch (error) {
|
||||
return handleDiscordError(error);
|
||||
}
|
||||
}
|
||||
|
||||
// Server information handler
|
||||
export async function getServerInfoHandler(
|
||||
args: unknown,
|
||||
context: ToolContext
|
||||
): Promise<ToolResponse> {
|
||||
const { guildId } = GetServerInfoSchema.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
|
||||
};
|
||||
}
|
||||
|
||||
// Fetch additional server data
|
||||
await guild.fetch();
|
||||
|
||||
// Fetch channel information
|
||||
const channels = await guild.channels.fetch();
|
||||
|
||||
// Categorize channels by type
|
||||
const channelsByType = {
|
||||
text: channels.filter(c => c?.type === ChannelType.GuildText).size,
|
||||
voice: channels.filter(c => c?.type === ChannelType.GuildVoice).size,
|
||||
category: channels.filter(c => c?.type === ChannelType.GuildCategory).size,
|
||||
forum: channels.filter(c => c?.type === ChannelType.GuildForum).size,
|
||||
announcement: channels.filter(c => c?.type === ChannelType.GuildAnnouncement).size,
|
||||
stage: channels.filter(c => c?.type === ChannelType.GuildStageVoice).size,
|
||||
total: channels.size
|
||||
};
|
||||
|
||||
// Get detailed information for all channels
|
||||
const channelDetails = channels.map(channel => {
|
||||
if (!channel) return null;
|
||||
|
||||
return {
|
||||
id: channel.id,
|
||||
name: channel.name,
|
||||
type: ChannelType[channel.type] || channel.type,
|
||||
categoryId: channel.parentId,
|
||||
position: channel.position,
|
||||
// Only add topic for text channels
|
||||
topic: 'topic' in channel ? channel.topic : null,
|
||||
};
|
||||
}).filter(c => c !== null); // Filter out null values
|
||||
|
||||
|
||||
// Group channels by type
|
||||
const groupedChannels = {
|
||||
text: channelDetails.filter(c => c.type === ChannelType[ChannelType.GuildText] || c.type === ChannelType.GuildText),
|
||||
voice: channelDetails.filter(c => c.type === ChannelType[ChannelType.GuildVoice] || c.type === ChannelType.GuildVoice),
|
||||
category: channelDetails.filter(c => c.type === ChannelType[ChannelType.GuildCategory] || c.type === ChannelType.GuildCategory),
|
||||
forum: channelDetails.filter(c => c.type === ChannelType[ChannelType.GuildForum] || c.type === ChannelType.GuildForum),
|
||||
announcement: channelDetails.filter(c => c.type === ChannelType[ChannelType.GuildAnnouncement] || c.type === ChannelType.GuildAnnouncement),
|
||||
stage: channelDetails.filter(c => c.type === ChannelType[ChannelType.GuildStageVoice] || c.type === ChannelType.GuildStageVoice),
|
||||
all: channelDetails
|
||||
};
|
||||
|
||||
// Get member count
|
||||
const approximateMemberCount = guild.approximateMemberCount || "unknown";
|
||||
|
||||
// Format guild information
|
||||
const guildInfo = {
|
||||
id: guild.id,
|
||||
name: guild.name,
|
||||
description: guild.description,
|
||||
icon: guild.iconURL(),
|
||||
owner: guild.ownerId,
|
||||
createdAt: guild.createdAt,
|
||||
memberCount: approximateMemberCount,
|
||||
channels: {
|
||||
count: channelsByType,
|
||||
details: groupedChannels
|
||||
},
|
||||
features: guild.features,
|
||||
premium: {
|
||||
tier: guild.premiumTier,
|
||||
subscriptions: guild.premiumSubscriptionCount
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify(guildInfo, null, 2) }]
|
||||
};
|
||||
} catch (error) {
|
||||
return handleDiscordError(error);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -14,12 +14,15 @@ import {
|
|||
createTextChannelHandler,
|
||||
deleteChannelHandler,
|
||||
readMessagesHandler,
|
||||
getServerInfoHandler,
|
||||
createCategoryHandler,
|
||||
editCategoryHandler,
|
||||
deleteCategoryHandler,
|
||||
listServersHandler
|
||||
deleteCategoryHandler
|
||||
} from './channel.js';
|
||||
import {
|
||||
getServerInfoHandler,
|
||||
listServersHandler,
|
||||
searchMessagesHandler
|
||||
} from "./server.js";
|
||||
import {
|
||||
addReactionHandler,
|
||||
addMultipleReactionsHandler,
|
||||
|
|
@ -57,7 +60,8 @@ export {
|
|||
createCategoryHandler,
|
||||
editCategoryHandler,
|
||||
deleteCategoryHandler,
|
||||
listServersHandler
|
||||
listServersHandler,
|
||||
searchMessagesHandler
|
||||
};
|
||||
|
||||
// Export common types
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
@ -214,6 +215,8 @@ export class StreamableHttpTransport implements MCPTransport {
|
|||
case 'discord_create_category':
|
||||
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({
|
||||
|
|
@ -335,7 +338,11 @@ export class StreamableHttpTransport implements MCPTransport {
|
|||
case 'discord_delete_category':
|
||||
result = await deleteCategoryHandler(params, this.toolContext!);
|
||||
break;
|
||||
|
||||
case 'discord_list_servers':
|
||||
result = await listServersHandler(params, this.toolContext!);
|
||||
case 'discord_search_messages':
|
||||
result = await searchMessagesHandler(params, this.toolContext!);
|
||||
break;break;
|
||||
}
|
||||
break;
|
||||
|
||||
|
|
@ -500,6 +507,13 @@ export class StreamableHttpTransport implements MCPTransport {
|
|||
result = await deleteCategoryHandler(toolArgs, this.toolContext!);
|
||||
break;
|
||||
|
||||
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({
|
||||
jsonrpc: '2.0',
|
||||
|
|
|
|||
Loading…
Reference in New Issue