# Directory Structure ``` ├── .cursor │ └── rules │ ├── bun-file.mdc │ ├── bun-glob.mdc │ ├── bun-test.mdc │ ├── bun-utils.mdc │ └── mcp.mdc ├── .cursorrules ├── .gitignore ├── bun.lock ├── CLAUDE.md ├── index.ts ├── package.json ├── README.md ├── spec.txt └── tsconfig.json ``` # Files -------------------------------------------------------------------------------- /.cursorrules: -------------------------------------------------------------------------------- ``` ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` # dependencies (bun install) node_modules # output out dist *.tgz # code coverage coverage *.lcov # logs logs _.log report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json # dotenv environment variable files .env .env.development.local .env.test.local .env.production.local .env.local # caches .eslintcache .cache *.tsbuildinfo # IntelliJ based IDEs .idea # Finder (MacOS) folder config .DS_Store ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown # Slack Search MCP Server A Model Context Protocol (MCP) server that provides tools and resources to access Slack's search functionality. This server allows LLMs to search and retrieve users, channels, messages, and more from a Slack workspace. ## Features ### Tools 1. `get_users` - Get a list of users in the Slack workspace 2. `get_channels` - Get a list of channels in the Slack workspace 3. `get_channel_messages` - Get messages from a specific channel 4. `get_thread_replies` - Get replies in a thread 5. `search_messages` - Search for messages in Slack ### Resources 1. `allusers://` - Get all users in the Slack workspace 2. `allchannels://` - Get all channels in the Slack workspace ## Requirements - [Bun](https://bun.sh/) runtime - Slack API token with appropriate permissions ## Installation 1. Clone the repository 2. Install dependencies: ```bash bun install ``` ## Usage 1. Set the Slack API token as an environment variable: ```bash export SLACK_TOKEN=xoxb-your-token-here ``` 2. Run the server: ```bash bun run index.ts ``` Or use the compiled version: ```bash ./dist/slack_search_function_mcp ``` ## Building To build the executable: ```bash bun run build ``` This will create a compiled executable in the `dist` directory. ## MCP Configuration To use this server with an MCP-enabled LLM, add it to your MCP configuration: ```json { "mcpServers": { "slack": { "command": "/path/to/dist/slack_search_function_mcp", "env": { "SLACK_TOKEN": "xoxb-your-token-here" } } } } ``` ## Tool Examples ### Get Users ```json { "name": "get_users", "arguments": { "limit": 10 } } ``` ### Get Channels ```json { "name": "get_channels", "arguments": { "limit": 10, "exclude_archived": true } } ``` ### Get Channel Messages ```json { "name": "get_channel_messages", "arguments": { "channel": "C01234ABCDE", "limit": 10 } } ``` ### Get Thread Replies ```json { "name": "get_thread_replies", "arguments": { "channel": "C01234ABCDE", "thread_ts": "1234567890.123456", "limit": 10 } } ``` ### Search Messages ```json { "name": "search_messages", "arguments": { "query": "important announcement", "sort": "timestamp", "sort_dir": "desc", "count": 10 } } ``` ## Resource Examples ### Get All Users ``` allusers:// ``` ### Get All Channels ``` allchannels:// ``` ## Error Handling The server includes comprehensive error handling for: - Invalid or missing Slack API token - API rate limits - Network errors - Invalid parameters - Authentication failures ## Security - The Slack API token is never logged or exposed in responses - The token is securely passed via environment variables ``` -------------------------------------------------------------------------------- /CLAUDE.md: -------------------------------------------------------------------------------- ```markdown # CLAUDE.md - Guidelines for Bun MCP Server ## Implementation Rules - See [mcp.mdc](.cursor/rules/mcp.mdc) at first and learn how to implement a MCP Server. - Generate a appropriate package name and overwrite package.json - Some examples using `setRequestHandler()` to implement a tool or resource, but it's deprecated. Use `server.tool()` or `server.resource()` instead. - You can add a thrid party package if needed to meet the requirements. - You can use Bun runtime features. Bun provides many built-in functions. See [bun-file.mdc](.cursor/rules/bun-file.mdc), [bun-test.mdc](.cursor/rules/bun-test.mdc), [bun-glob.mdc](.cursor/rules/bun-glob.mdc), [bun-utils.mdc](.cursor/rules/bun-utils.mdc) ## Build Commands - `bun run build` - Build the MCP server executable - `bun run show-package-name` - Display the package name for installation - Install with: `cp dist/$npm_package_name $HOME/bin/` ## Code Style Guidelines ### Imports & Organization - Use named imports from MCP SDK: `import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js"` - Group imports by external packages first, then internal modules ### TypeScript & Types - Use Zod for parameter validation in tools and resources - Prefer TypeScript strict mode with explicit type annotations - Use async/await for asynchronous operations ### Naming Conventions - CamelCase for variables and functions - PascalCase for classes and types - Use descriptive names for resources, tools and prompts ### MCP Best Practices - Resources should be pure and not have side effects (like GET endpoints) - Tools should handle specific actions with well-defined parameters (like POST endpoints) - Write a enough description for tool and each parameters. - Use ResourceTemplate for parameterized resources - Properly handle errors in tool implementations and return isError: true ### Error Handling - Use try/catch blocks with specific error types - Return proper error responses with descriptive messages - Always close connections and free resources in finally blocks ## References - [Basic Examples](.cursor/rules/basic.mdc) ## Another Examples - [@modelcontextprotocol/server-memory](https://github.com/modelcontextprotocol/servers/blob/main/src/memory/index.ts) - [@modelcontextprotocol/server-filesystem](https://github.com/modelcontextprotocol/servers/blob/main/src/filesystem/index.ts) - [redis](https://github.com/modelcontextprotocol/servers/blob/main/src/redis/src/index.ts) ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json { "name": "slack_search_function_mcp", "module": "index.ts", "type": "module", "private": true, "scripts": { "build": "mkdir -p dist && bun build --compile --outfile=dist/$npm_package_name index.ts", "show-package-name": "echo $npm_package_name" }, "devDependencies": { "@types/bun": "latest" }, "peerDependencies": { "typescript": "^5" }, "dependencies": { "@modelcontextprotocol/sdk": "^1.6.0", "@slack/web-api": "^7.8.0", "zod": "^3.24.2" } } ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json { "compilerOptions": { // Enable latest features "lib": ["ESNext", "DOM"], "target": "ESNext", "module": "ESNext", "moduleDetection": "force", "jsx": "react-jsx", "allowJs": true, // Bundler mode "moduleResolution": "bundler", "allowImportingTsExtensions": true, "verbatimModuleSyntax": true, "noEmit": true, // Best practices "strict": true, "skipLibCheck": true, "noFallthroughCasesInSwitch": true, // Some stricter flags (disabled by default) "noUnusedLocals": false, "noUnusedParameters": false, "noPropertyAccessFromIndexSignature": false } } ``` -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- ```typescript #!/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(); } ```