We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/nikhilchintawar/slack-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server
import { z } from 'zod';
import { getSlackClient } from '../utils/slack-client.js';
import { getAllowedChannelsForSearch, validateChannel } from '../utils/config.js';
import type { SearchResult, SlackMessage } from '../types.js';
export const searchMessagesSchema = z.object({
query: z.string().min(1).describe('Search query string (case-insensitive text match)'),
channel: z
.string()
.optional()
.describe(
'Channel ID or name to search in. If not provided, searches all configured SLACK_CHANNELS.'
),
after: z
.string()
.optional()
.describe('Search messages after this date/time (YYYY-MM-DD or ISO 8601)'),
before: z
.string()
.optional()
.describe('Search messages before this date/time (YYYY-MM-DD or ISO 8601)'),
limit: z
.number()
.min(1)
.max(100)
.default(20)
.describe('Maximum number of matching messages to return'),
});
export type SearchMessagesInput = z.infer<typeof searchMessagesSchema>;
interface SlackHistoryMessage {
ts: string;
text?: string;
user?: string;
thread_ts?: string;
reply_count?: number;
}
function parseTimestamp(value: string): string {
if (/^\d+(\.\d+)?$/.test(value)) {
return value;
}
const date = new Date(value);
if (!isNaN(date.getTime())) {
return (date.getTime() / 1000).toString();
}
throw new Error(`Invalid timestamp format: ${value}`);
}
function matchesQuery(text: string, query: string): boolean {
const lowerText = text.toLowerCase();
const lowerQuery = query.toLowerCase();
// Support simple OR queries with |
if (lowerQuery.includes('|')) {
const terms = lowerQuery.split('|').map((t) => t.trim());
return terms.some((term) => lowerText.includes(term));
}
// Support AND queries with multiple words
const terms = lowerQuery.split(/\s+/).filter(Boolean);
return terms.every((term) => lowerText.includes(term));
}
function mapMessage(
msg: SlackHistoryMessage,
channelId: string,
channelName?: string
): SlackMessage {
const ts = msg.ts;
const timestamp = parseFloat(ts) * 1000;
return {
ts,
text: msg.text ?? '',
user: msg.user,
channel: channelId,
channelName,
timestamp,
threadTs: msg.thread_ts,
replyCount: msg.reply_count,
};
}
async function searchInChannel(
client: ReturnType<typeof getSlackClient>,
channelId: string,
channelName: string | undefined,
query: string,
oldest?: string,
latest?: string,
limit?: number
): Promise<SlackMessage[]> {
const messages: SlackMessage[] = [];
let cursor: string | undefined;
const maxMessages = limit ?? 20;
const params: {
channel: string;
limit: number;
oldest?: string;
latest?: string;
cursor?: string;
} = {
channel: channelId,
limit: 200, // Fetch more to filter
};
if (oldest) {
params.oldest = parseTimestamp(oldest);
}
if (latest) {
params.latest = parseTimestamp(latest);
}
// Fetch up to 1000 messages max to search through
let totalFetched = 0;
const maxFetch = 1000;
do {
if (cursor) {
params.cursor = cursor;
}
const response = await client.conversations.history(params);
if (response.messages) {
for (const msg of response.messages) {
totalFetched++;
const histMsg = msg as SlackHistoryMessage;
if (histMsg.text && matchesQuery(histMsg.text, query)) {
messages.push(mapMessage(histMsg, channelId, channelName));
if (messages.length >= maxMessages) {
return messages;
}
}
}
}
cursor = response.response_metadata?.next_cursor;
} while (cursor && messages.length < maxMessages && totalFetched < maxFetch);
return messages;
}
async function resolveChannelId(
client: ReturnType<typeof getSlackClient>,
channelNameOrId: string
): Promise<{ id: string; name: string } | null> {
// If it looks like a channel ID, use it directly
if (channelNameOrId.startsWith('C') || channelNameOrId.startsWith('G')) {
try {
const info = await client.conversations.info({ channel: channelNameOrId });
if (info.channel) {
return {
id: channelNameOrId,
name: (info.channel as { name?: string }).name ?? channelNameOrId,
};
}
} catch {
// Fall through to name lookup
}
}
// Otherwise, try to find by name
const cleanName = channelNameOrId.replace(/^#/, '');
let cursor: string | undefined;
do {
const response = await client.conversations.list({
types: 'public_channel,private_channel',
limit: 200,
cursor,
});
if (response.channels) {
for (const channel of response.channels) {
if (channel.name === cleanName && channel.id) {
return { id: channel.id, name: channel.name };
}
}
}
cursor = response.response_metadata?.next_cursor;
} while (cursor);
return null;
}
export async function searchMessages(
input: SearchMessagesInput
): Promise<SearchResult> {
const client = getSlackClient();
const allowedChannels = getAllowedChannelsForSearch();
// Determine which channels to search
let channelsToSearch: { id: string; name: string }[] = [];
if (input.channel) {
// Validate and resolve single channel
validateChannel(input.channel);
const resolved = await resolveChannelId(client, input.channel);
if (!resolved) {
throw new Error(`Channel not found: ${input.channel}`);
}
channelsToSearch = [resolved];
} else if (allowedChannels && allowedChannels.length > 0) {
// Search all allowed channels
for (const ch of allowedChannels) {
const resolved = await resolveChannelId(client, ch);
if (resolved) {
channelsToSearch.push(resolved);
}
}
} else {
throw new Error(
'No channel specified and SLACK_CHANNELS not configured. ' +
'Provide a channel parameter or set SLACK_CHANNELS environment variable.'
);
}
if (channelsToSearch.length === 0) {
throw new Error('No valid channels found to search.');
}
// Search each channel and combine results
const allMessages: SlackMessage[] = [];
const perChannelLimit = Math.ceil(input.limit / channelsToSearch.length);
for (const channel of channelsToSearch) {
if (allMessages.length >= input.limit) break;
const remaining = input.limit - allMessages.length;
const messages = await searchInChannel(
client,
channel.id,
channel.name,
input.query,
input.after,
input.before,
Math.min(perChannelLimit, remaining)
);
allMessages.push(...messages);
}
// Sort by timestamp descending
allMessages.sort((a, b) => b.timestamp - a.timestamp);
// Trim to limit
const results = allMessages.slice(0, input.limit);
return {
query: input.query,
total: results.length,
messages: results,
};
}