Slack Search MCP Server
by takuya0206
Verified
#!/usr/bin/env bun
import {
McpServer,
ResourceTemplate,
} from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { WebClient, ErrorCode as SlackErrorCode } from "@slack/web-api";
import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
// Get Slack API token from environment variables
const SLACK_TOKEN = process.env.SLACK_TOKEN;
if (!SLACK_TOKEN) {
console.error("Error: SLACK_TOKEN environment variable is required");
process.exit(1);
}
// Create Slack Web Client
const slack = new WebClient(SLACK_TOKEN);
// Create an MCP server
const server = new McpServer({
name: "slack-search-mcp",
version: "1.0.0",
});
// Validate Slack token on startup
async function validateSlackToken() {
try {
await slack.auth.test();
console.error("Successfully connected to Slack API");
} catch (error: any) {
console.error("Failed to connect to Slack API:", error);
process.exit(1);
}
}
// Common schemas
const tokenSchema = z.string().describe("Slack API token");
// Common error handling function
function handleSlackError(error: any): never {
console.error("Slack API error:", error);
if (error.code === SlackErrorCode.PlatformError) {
throw new McpError(
ErrorCode.InternalError,
`Slack API error: ${error.data?.error || "Unknown error"}`
);
} else if (error.code === SlackErrorCode.RequestError) {
throw new McpError(
ErrorCode.InternalError,
"Network error when connecting to Slack API"
);
} else if (error.code === SlackErrorCode.RateLimitedError) {
throw new McpError(
ErrorCode.InternalError,
"Rate limited by Slack API"
);
} else if (error.code === SlackErrorCode.HTTPError) {
throw new McpError(
ErrorCode.InternalError,
`HTTP error: ${error.statusCode}`
);
} else {
throw new McpError(
ErrorCode.InternalError,
`Unexpected error: ${error.message || "Unknown error"}`
);
}
}
// Tool: get_users
server.tool(
"get_users",
"Get a list of users in the Slack workspace",
{
token: tokenSchema.optional(),
limit: z.number().min(1).max(1000).optional().describe("Maximum number of users to return"),
cursor: z.string().optional().describe("Pagination cursor for fetching next page"),
},
async ({ token = SLACK_TOKEN, limit = 100, cursor }) => {
try {
const response = await slack.users.list({
token,
limit,
cursor,
});
return {
content: [
{
type: "text",
text: JSON.stringify({
users: response.members,
next_cursor: response.response_metadata?.next_cursor,
has_more: !!response.response_metadata?.next_cursor,
}, null, 2),
},
],
};
} catch (error: any) {
handleSlackError(error);
}
}
);
// Tool: get_channels
server.tool(
"get_channels",
"Get a list of channels in the Slack workspace",
{
token: tokenSchema.optional(),
limit: z.number().min(1).max(1000).optional().describe("Maximum number of channels to return"),
cursor: z.string().optional().describe("Pagination cursor for fetching next page"),
exclude_archived: z.boolean().optional().describe("Exclude archived channels"),
types: z.string().optional().describe("Types of channels to include (public_channel, private_channel, mpim, im)"),
},
async ({ token = SLACK_TOKEN, limit = 100, cursor, exclude_archived = true, types = "public_channel,private_channel" }) => {
try {
const response = await slack.conversations.list({
token,
limit,
cursor,
exclude_archived,
types,
});
return {
content: [
{
type: "text",
text: JSON.stringify({
channels: response.channels,
next_cursor: response.response_metadata?.next_cursor,
has_more: !!response.response_metadata?.next_cursor,
}, null, 2),
},
],
};
} catch (error: any) {
handleSlackError(error);
}
}
);
// Tool: get_channel_messages
server.tool(
"get_channel_messages",
"Get messages from a specific channel",
{
token: tokenSchema.optional(),
channel: z.string().describe("Channel ID"),
limit: z.number().min(1).max(1000).optional().describe("Maximum number of messages to return"),
oldest: z.string().optional().describe("Start of time range (Unix timestamp)"),
latest: z.string().optional().describe("End of time range (Unix timestamp)"),
inclusive: z.boolean().optional().describe("Include messages with timestamps matching oldest or latest"),
cursor: z.string().optional().describe("Pagination cursor for fetching next page"),
},
async ({ token = SLACK_TOKEN, channel, limit = 100, oldest, latest, inclusive, cursor }) => {
try {
// Validate channel ID format
if (!channel.match(/^[A-Z0-9]+$/i)) {
throw new McpError(
ErrorCode.InvalidParams,
"Invalid channel ID format"
);
}
const response = await slack.conversations.history({
token,
channel,
limit,
oldest,
latest,
inclusive,
cursor,
});
return {
content: [
{
type: "text",
text: JSON.stringify({
messages: response.messages,
has_more: response.has_more,
next_cursor: response.response_metadata?.next_cursor,
}, null, 2),
},
],
};
} catch (error: any) {
handleSlackError(error);
}
}
);
// Tool: get_thread_replies
server.tool(
"get_thread_replies",
"Get replies in a thread",
{
token: tokenSchema.optional(),
channel: z.string().describe("Channel ID"),
thread_ts: z.string().describe("Timestamp of the parent message"),
limit: z.number().min(1).max(1000).optional().describe("Maximum number of replies to return"),
oldest: z.string().optional().describe("Start of time range (Unix timestamp)"),
latest: z.string().optional().describe("End of time range (Unix timestamp)"),
inclusive: z.boolean().optional().describe("Include messages with timestamps matching oldest or latest"),
cursor: z.string().optional().describe("Pagination cursor for fetching next page"),
},
async ({ token = SLACK_TOKEN, channel, thread_ts, limit = 100, oldest, latest, inclusive, cursor }) => {
try {
// Validate channel ID format
if (!channel.match(/^[A-Z0-9]+$/i)) {
throw new McpError(
ErrorCode.InvalidParams,
"Invalid channel ID format"
);
}
// Validate thread_ts format (Unix timestamp)
if (!thread_ts.match(/^\d+\.\d+$/)) {
throw new McpError(
ErrorCode.InvalidParams,
"Invalid thread_ts format. Expected Unix timestamp (e.g., 1234567890.123456)"
);
}
const response = await slack.conversations.replies({
token,
channel,
ts: thread_ts,
limit,
oldest,
latest,
inclusive,
cursor,
});
return {
content: [
{
type: "text",
text: JSON.stringify({
messages: response.messages,
has_more: response.has_more,
next_cursor: response.response_metadata?.next_cursor,
}, null, 2),
},
],
};
} catch (error: any) {
handleSlackError(error);
}
}
);
// Tool: search_messages
server.tool(
"search_messages",
"Search for messages in Slack",
{
token: tokenSchema.optional(),
query: z.string().describe("Search query"),
sort: z.enum(["score", "timestamp"]).optional().describe("Sort by relevance or timestamp"),
sort_dir: z.enum(["asc", "desc"]).optional().describe("Sort direction"),
highlight: z.boolean().optional().describe("Whether to highlight the matches"),
count: z.number().min(1).max(100).optional().describe("Number of results to return per page"),
page: z.number().min(1).optional().describe("Page number of results to return"),
},
async ({ token = SLACK_TOKEN, query, sort = "score", sort_dir = "desc", highlight = true, count = 20, page = 1 }) => {
try {
const response = await slack.search.messages({
token,
query,
sort,
sort_dir,
highlight,
count,
page,
});
return {
content: [
{
type: "text",
text: JSON.stringify({
messages: response.messages,
pagination: response.messages?.pagination,
total: response.messages?.total,
}, null, 2),
},
],
};
} catch (error: any) {
handleSlackError(error);
}
}
);
// Resource: all_users
server.resource(
"all_users",
new ResourceTemplate("allusers://", { list: undefined }),
async (uri) => {
try {
// Get all users (handle pagination internally)
const allUsers: any[] = [];
let cursor;
let hasMore = true;
while (hasMore) {
const response = await slack.users.list({
token: SLACK_TOKEN,
limit: 1000,
cursor,
});
if (response.members) {
allUsers.push(...response.members);
}
cursor = response.response_metadata?.next_cursor;
hasMore = !!cursor;
}
return {
contents: [
{
uri: uri.href,
text: JSON.stringify(allUsers, null, 2),
mimeType: "application/json",
},
],
};
} catch (error: any) {
console.error("Error fetching all users:", error);
throw new McpError(
ErrorCode.InternalError,
`Failed to fetch all users: ${error.message || "Unknown error"}`
);
}
}
);
// Resource: all_channels
server.resource(
"all_channels",
new ResourceTemplate("allchannels://", { list: undefined }),
async (uri) => {
try {
// Get all channels (handle pagination internally)
const allChannels: any[] = [];
let cursor;
let hasMore = true;
while (hasMore) {
const response = await slack.conversations.list({
token: SLACK_TOKEN,
limit: 1000,
cursor,
types: "public_channel,private_channel",
});
if (response.channels) {
allChannels.push(...response.channels);
}
cursor = response.response_metadata?.next_cursor;
hasMore = !!cursor;
}
return {
contents: [
{
uri: uri.href,
text: JSON.stringify(allChannels, null, 2),
mimeType: "application/json",
},
],
};
} catch (error: any) {
console.error("Error fetching all channels:", error);
throw new McpError(
ErrorCode.InternalError,
`Failed to fetch all channels: ${error.message || "Unknown error"}`
);
}
}
);
async function main() {
try {
// Validate Slack token before starting the server
await validateSlackToken();
// Start receiving messages on stdin and sending messages on stdout
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Slack Search MCP server running on stdio");
} catch (error: any) {
console.error("Failed to start MCP server:", error);
process.exit(1);
}
}
if (import.meta.main) {
main();
}