# 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: -------------------------------------------------------------------------------- ``` 1 | ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` 1 | # dependencies (bun install) 2 | node_modules 3 | 4 | # output 5 | out 6 | dist 7 | *.tgz 8 | 9 | # code coverage 10 | coverage 11 | *.lcov 12 | 13 | # logs 14 | logs 15 | _.log 16 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 17 | 18 | # dotenv environment variable files 19 | .env 20 | .env.development.local 21 | .env.test.local 22 | .env.production.local 23 | .env.local 24 | 25 | # caches 26 | .eslintcache 27 | .cache 28 | *.tsbuildinfo 29 | 30 | # IntelliJ based IDEs 31 | .idea 32 | 33 | # Finder (MacOS) folder config 34 | .DS_Store 35 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | # Slack Search MCP Server 2 | 3 | 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. 4 | 5 | ## Features 6 | 7 | ### Tools 8 | 9 | 1. `get_users` - Get a list of users in the Slack workspace 10 | 2. `get_channels` - Get a list of channels in the Slack workspace 11 | 3. `get_channel_messages` - Get messages from a specific channel 12 | 4. `get_thread_replies` - Get replies in a thread 13 | 5. `search_messages` - Search for messages in Slack 14 | 15 | ### Resources 16 | 17 | 1. `allusers://` - Get all users in the Slack workspace 18 | 2. `allchannels://` - Get all channels in the Slack workspace 19 | 20 | ## Requirements 21 | 22 | - [Bun](https://bun.sh/) runtime 23 | - Slack API token with appropriate permissions 24 | 25 | ## Installation 26 | 27 | 1. Clone the repository 28 | 2. Install dependencies: 29 | ```bash 30 | bun install 31 | ``` 32 | 33 | ## Usage 34 | 35 | 1. Set the Slack API token as an environment variable: 36 | ```bash 37 | export SLACK_TOKEN=xoxb-your-token-here 38 | ``` 39 | 40 | 2. Run the server: 41 | ```bash 42 | bun run index.ts 43 | ``` 44 | 45 | Or use the compiled version: 46 | ```bash 47 | ./dist/slack_search_function_mcp 48 | ``` 49 | 50 | ## Building 51 | 52 | To build the executable: 53 | 54 | ```bash 55 | bun run build 56 | ``` 57 | 58 | This will create a compiled executable in the `dist` directory. 59 | 60 | ## MCP Configuration 61 | 62 | To use this server with an MCP-enabled LLM, add it to your MCP configuration: 63 | 64 | ```json 65 | { 66 | "mcpServers": { 67 | "slack": { 68 | "command": "/path/to/dist/slack_search_function_mcp", 69 | "env": { 70 | "SLACK_TOKEN": "xoxb-your-token-here" 71 | } 72 | } 73 | } 74 | } 75 | ``` 76 | 77 | ## Tool Examples 78 | 79 | ### Get Users 80 | 81 | ```json 82 | { 83 | "name": "get_users", 84 | "arguments": { 85 | "limit": 10 86 | } 87 | } 88 | ``` 89 | 90 | ### Get Channels 91 | 92 | ```json 93 | { 94 | "name": "get_channels", 95 | "arguments": { 96 | "limit": 10, 97 | "exclude_archived": true 98 | } 99 | } 100 | ``` 101 | 102 | ### Get Channel Messages 103 | 104 | ```json 105 | { 106 | "name": "get_channel_messages", 107 | "arguments": { 108 | "channel": "C01234ABCDE", 109 | "limit": 10 110 | } 111 | } 112 | ``` 113 | 114 | ### Get Thread Replies 115 | 116 | ```json 117 | { 118 | "name": "get_thread_replies", 119 | "arguments": { 120 | "channel": "C01234ABCDE", 121 | "thread_ts": "1234567890.123456", 122 | "limit": 10 123 | } 124 | } 125 | ``` 126 | 127 | ### Search Messages 128 | 129 | ```json 130 | { 131 | "name": "search_messages", 132 | "arguments": { 133 | "query": "important announcement", 134 | "sort": "timestamp", 135 | "sort_dir": "desc", 136 | "count": 10 137 | } 138 | } 139 | ``` 140 | 141 | ## Resource Examples 142 | 143 | ### Get All Users 144 | 145 | ``` 146 | allusers:// 147 | ``` 148 | 149 | ### Get All Channels 150 | 151 | ``` 152 | allchannels:// 153 | ``` 154 | 155 | ## Error Handling 156 | 157 | The server includes comprehensive error handling for: 158 | - Invalid or missing Slack API token 159 | - API rate limits 160 | - Network errors 161 | - Invalid parameters 162 | - Authentication failures 163 | 164 | ## Security 165 | 166 | - The Slack API token is never logged or exposed in responses 167 | - The token is securely passed via environment variables 168 | ``` -------------------------------------------------------------------------------- /CLAUDE.md: -------------------------------------------------------------------------------- ```markdown 1 | # CLAUDE.md - Guidelines for Bun MCP Server 2 | 3 | ## Implementation Rules 4 | 5 | - See [mcp.mdc](.cursor/rules/mcp.mdc) at first and learn how to implement a MCP Server. 6 | - Generate a appropriate package name and overwrite package.json 7 | - Some examples using `setRequestHandler()` to implement a tool or resource, but it's deprecated. Use `server.tool()` or `server.resource()` instead. 8 | - You can add a thrid party package if needed to meet the requirements. 9 | - 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) 10 | 11 | ## Build Commands 12 | 13 | - `bun run build` - Build the MCP server executable 14 | - `bun run show-package-name` - Display the package name for installation 15 | - Install with: `cp dist/$npm_package_name $HOME/bin/` 16 | 17 | ## Code Style Guidelines 18 | 19 | ### Imports & Organization 20 | 21 | - Use named imports from MCP SDK: `import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js"` 22 | - Group imports by external packages first, then internal modules 23 | 24 | ### TypeScript & Types 25 | 26 | - Use Zod for parameter validation in tools and resources 27 | - Prefer TypeScript strict mode with explicit type annotations 28 | - Use async/await for asynchronous operations 29 | 30 | ### Naming Conventions 31 | 32 | - CamelCase for variables and functions 33 | - PascalCase for classes and types 34 | - Use descriptive names for resources, tools and prompts 35 | 36 | ### MCP Best Practices 37 | 38 | - Resources should be pure and not have side effects (like GET endpoints) 39 | - Tools should handle specific actions with well-defined parameters (like POST endpoints) 40 | - Write a enough description for tool and each parameters. 41 | - Use ResourceTemplate for parameterized resources 42 | - Properly handle errors in tool implementations and return isError: true 43 | 44 | ### Error Handling 45 | 46 | - Use try/catch blocks with specific error types 47 | - Return proper error responses with descriptive messages 48 | - Always close connections and free resources in finally blocks 49 | 50 | ## References 51 | 52 | - [Basic Examples](.cursor/rules/basic.mdc) 53 | 54 | ## Another Examples 55 | 56 | - [@modelcontextprotocol/server-memory](https://github.com/modelcontextprotocol/servers/blob/main/src/memory/index.ts) 57 | - [@modelcontextprotocol/server-filesystem](https://github.com/modelcontextprotocol/servers/blob/main/src/filesystem/index.ts) 58 | - [redis](https://github.com/modelcontextprotocol/servers/blob/main/src/redis/src/index.ts) 59 | ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "slack_search_function_mcp", 3 | "module": "index.ts", 4 | "type": "module", 5 | "private": true, 6 | "scripts": { 7 | "build": "mkdir -p dist && bun build --compile --outfile=dist/$npm_package_name index.ts", 8 | "show-package-name": "echo $npm_package_name" 9 | }, 10 | "devDependencies": { 11 | "@types/bun": "latest" 12 | }, 13 | "peerDependencies": { 14 | "typescript": "^5" 15 | }, 16 | "dependencies": { 17 | "@modelcontextprotocol/sdk": "^1.6.0", 18 | "@slack/web-api": "^7.8.0", 19 | "zod": "^3.24.2" 20 | } 21 | } ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "compilerOptions": { 3 | // Enable latest features 4 | "lib": ["ESNext", "DOM"], 5 | "target": "ESNext", 6 | "module": "ESNext", 7 | "moduleDetection": "force", 8 | "jsx": "react-jsx", 9 | "allowJs": true, 10 | 11 | // Bundler mode 12 | "moduleResolution": "bundler", 13 | "allowImportingTsExtensions": true, 14 | "verbatimModuleSyntax": true, 15 | "noEmit": true, 16 | 17 | // Best practices 18 | "strict": true, 19 | "skipLibCheck": true, 20 | "noFallthroughCasesInSwitch": true, 21 | 22 | // Some stricter flags (disabled by default) 23 | "noUnusedLocals": false, 24 | "noUnusedParameters": false, 25 | "noPropertyAccessFromIndexSignature": false 26 | } 27 | } 28 | ``` -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- ```typescript 1 | #!/usr/bin/env bun 2 | import { 3 | McpServer, 4 | ResourceTemplate, 5 | } from "@modelcontextprotocol/sdk/server/mcp.js"; 6 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 7 | import { z } from "zod"; 8 | import { WebClient, ErrorCode as SlackErrorCode } from "@slack/web-api"; 9 | import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js"; 10 | 11 | // Get Slack API token from environment variables 12 | const SLACK_TOKEN = process.env.SLACK_TOKEN; 13 | 14 | if (!SLACK_TOKEN) { 15 | console.error("Error: SLACK_TOKEN environment variable is required"); 16 | process.exit(1); 17 | } 18 | 19 | // Create Slack Web Client 20 | const slack = new WebClient(SLACK_TOKEN); 21 | 22 | // Create an MCP server 23 | const server = new McpServer({ 24 | name: "slack-search-mcp", 25 | version: "1.0.0", 26 | }); 27 | 28 | // Validate Slack token on startup 29 | async function validateSlackToken() { 30 | try { 31 | await slack.auth.test(); 32 | console.error("Successfully connected to Slack API"); 33 | } catch (error: any) { 34 | console.error("Failed to connect to Slack API:", error); 35 | process.exit(1); 36 | } 37 | } 38 | 39 | // Common schemas 40 | const tokenSchema = z.string().describe("Slack API token"); 41 | 42 | // Common error handling function 43 | function handleSlackError(error: any): never { 44 | console.error("Slack API error:", error); 45 | 46 | if (error.code === SlackErrorCode.PlatformError) { 47 | throw new McpError( 48 | ErrorCode.InternalError, 49 | `Slack API error: ${error.data?.error || "Unknown error"}` 50 | ); 51 | } else if (error.code === SlackErrorCode.RequestError) { 52 | throw new McpError( 53 | ErrorCode.InternalError, 54 | "Network error when connecting to Slack API" 55 | ); 56 | } else if (error.code === SlackErrorCode.RateLimitedError) { 57 | throw new McpError( 58 | ErrorCode.InternalError, 59 | "Rate limited by Slack API" 60 | ); 61 | } else if (error.code === SlackErrorCode.HTTPError) { 62 | throw new McpError( 63 | ErrorCode.InternalError, 64 | `HTTP error: ${error.statusCode}` 65 | ); 66 | } else { 67 | throw new McpError( 68 | ErrorCode.InternalError, 69 | `Unexpected error: ${error.message || "Unknown error"}` 70 | ); 71 | } 72 | } 73 | 74 | // Tool: get_users 75 | server.tool( 76 | "get_users", 77 | "Get a list of users in the Slack workspace", 78 | { 79 | token: tokenSchema.optional(), 80 | limit: z.number().min(1).max(1000).optional().describe("Maximum number of users to return"), 81 | cursor: z.string().optional().describe("Pagination cursor for fetching next page"), 82 | }, 83 | async ({ token = SLACK_TOKEN, limit = 100, cursor }) => { 84 | try { 85 | const response = await slack.users.list({ 86 | token, 87 | limit, 88 | cursor, 89 | }); 90 | 91 | return { 92 | content: [ 93 | { 94 | type: "text", 95 | text: JSON.stringify({ 96 | users: response.members, 97 | next_cursor: response.response_metadata?.next_cursor, 98 | has_more: !!response.response_metadata?.next_cursor, 99 | }, null, 2), 100 | }, 101 | ], 102 | }; 103 | } catch (error: any) { 104 | handleSlackError(error); 105 | } 106 | } 107 | ); 108 | 109 | // Tool: get_channels 110 | server.tool( 111 | "get_channels", 112 | "Get a list of channels in the Slack workspace", 113 | { 114 | token: tokenSchema.optional(), 115 | limit: z.number().min(1).max(1000).optional().describe("Maximum number of channels to return"), 116 | cursor: z.string().optional().describe("Pagination cursor for fetching next page"), 117 | exclude_archived: z.boolean().optional().describe("Exclude archived channels"), 118 | types: z.string().optional().describe("Types of channels to include (public_channel, private_channel, mpim, im)"), 119 | }, 120 | async ({ token = SLACK_TOKEN, limit = 100, cursor, exclude_archived = true, types = "public_channel,private_channel" }) => { 121 | try { 122 | const response = await slack.conversations.list({ 123 | token, 124 | limit, 125 | cursor, 126 | exclude_archived, 127 | types, 128 | }); 129 | 130 | return { 131 | content: [ 132 | { 133 | type: "text", 134 | text: JSON.stringify({ 135 | channels: response.channels, 136 | next_cursor: response.response_metadata?.next_cursor, 137 | has_more: !!response.response_metadata?.next_cursor, 138 | }, null, 2), 139 | }, 140 | ], 141 | }; 142 | } catch (error: any) { 143 | handleSlackError(error); 144 | } 145 | } 146 | ); 147 | 148 | // Tool: get_channel_messages 149 | server.tool( 150 | "get_channel_messages", 151 | "Get messages from a specific channel", 152 | { 153 | token: tokenSchema.optional(), 154 | channel: z.string().describe("Channel ID"), 155 | limit: z.number().min(1).max(1000).optional().describe("Maximum number of messages to return"), 156 | oldest: z.string().optional().describe("Start of time range (Unix timestamp)"), 157 | latest: z.string().optional().describe("End of time range (Unix timestamp)"), 158 | inclusive: z.boolean().optional().describe("Include messages with timestamps matching oldest or latest"), 159 | cursor: z.string().optional().describe("Pagination cursor for fetching next page"), 160 | }, 161 | async ({ token = SLACK_TOKEN, channel, limit = 100, oldest, latest, inclusive, cursor }) => { 162 | try { 163 | // Validate channel ID format 164 | if (!channel.match(/^[A-Z0-9]+$/i)) { 165 | throw new McpError( 166 | ErrorCode.InvalidParams, 167 | "Invalid channel ID format" 168 | ); 169 | } 170 | 171 | const response = await slack.conversations.history({ 172 | token, 173 | channel, 174 | limit, 175 | oldest, 176 | latest, 177 | inclusive, 178 | cursor, 179 | }); 180 | 181 | return { 182 | content: [ 183 | { 184 | type: "text", 185 | text: JSON.stringify({ 186 | messages: response.messages, 187 | has_more: response.has_more, 188 | next_cursor: response.response_metadata?.next_cursor, 189 | }, null, 2), 190 | }, 191 | ], 192 | }; 193 | } catch (error: any) { 194 | handleSlackError(error); 195 | } 196 | } 197 | ); 198 | 199 | // Tool: get_thread_replies 200 | server.tool( 201 | "get_thread_replies", 202 | "Get replies in a thread", 203 | { 204 | token: tokenSchema.optional(), 205 | channel: z.string().describe("Channel ID"), 206 | thread_ts: z.string().describe("Timestamp of the parent message"), 207 | limit: z.number().min(1).max(1000).optional().describe("Maximum number of replies to return"), 208 | oldest: z.string().optional().describe("Start of time range (Unix timestamp)"), 209 | latest: z.string().optional().describe("End of time range (Unix timestamp)"), 210 | inclusive: z.boolean().optional().describe("Include messages with timestamps matching oldest or latest"), 211 | cursor: z.string().optional().describe("Pagination cursor for fetching next page"), 212 | }, 213 | async ({ token = SLACK_TOKEN, channel, thread_ts, limit = 100, oldest, latest, inclusive, cursor }) => { 214 | try { 215 | // Validate channel ID format 216 | if (!channel.match(/^[A-Z0-9]+$/i)) { 217 | throw new McpError( 218 | ErrorCode.InvalidParams, 219 | "Invalid channel ID format" 220 | ); 221 | } 222 | 223 | // Validate thread_ts format (Unix timestamp) 224 | if (!thread_ts.match(/^\d+\.\d+$/)) { 225 | throw new McpError( 226 | ErrorCode.InvalidParams, 227 | "Invalid thread_ts format. Expected Unix timestamp (e.g., 1234567890.123456)" 228 | ); 229 | } 230 | 231 | const response = await slack.conversations.replies({ 232 | token, 233 | channel, 234 | ts: thread_ts, 235 | limit, 236 | oldest, 237 | latest, 238 | inclusive, 239 | cursor, 240 | }); 241 | 242 | return { 243 | content: [ 244 | { 245 | type: "text", 246 | text: JSON.stringify({ 247 | messages: response.messages, 248 | has_more: response.has_more, 249 | next_cursor: response.response_metadata?.next_cursor, 250 | }, null, 2), 251 | }, 252 | ], 253 | }; 254 | } catch (error: any) { 255 | handleSlackError(error); 256 | } 257 | } 258 | ); 259 | 260 | // Tool: search_messages 261 | server.tool( 262 | "search_messages", 263 | "Search for messages in Slack", 264 | { 265 | token: tokenSchema.optional(), 266 | query: z.string().describe("Search query"), 267 | sort: z.enum(["score", "timestamp"]).optional().describe("Sort by relevance or timestamp"), 268 | sort_dir: z.enum(["asc", "desc"]).optional().describe("Sort direction"), 269 | highlight: z.boolean().optional().describe("Whether to highlight the matches"), 270 | count: z.number().min(1).max(100).optional().describe("Number of results to return per page"), 271 | page: z.number().min(1).optional().describe("Page number of results to return"), 272 | }, 273 | async ({ token = SLACK_TOKEN, query, sort = "score", sort_dir = "desc", highlight = true, count = 20, page = 1 }) => { 274 | try { 275 | const response = await slack.search.messages({ 276 | token, 277 | query, 278 | sort, 279 | sort_dir, 280 | highlight, 281 | count, 282 | page, 283 | }); 284 | 285 | return { 286 | content: [ 287 | { 288 | type: "text", 289 | text: JSON.stringify({ 290 | messages: response.messages, 291 | pagination: response.messages?.pagination, 292 | total: response.messages?.total, 293 | }, null, 2), 294 | }, 295 | ], 296 | }; 297 | } catch (error: any) { 298 | handleSlackError(error); 299 | } 300 | } 301 | ); 302 | 303 | // Resource: all_users 304 | server.resource( 305 | "all_users", 306 | new ResourceTemplate("allusers://", { list: undefined }), 307 | async (uri) => { 308 | try { 309 | // Get all users (handle pagination internally) 310 | const allUsers: any[] = []; 311 | let cursor; 312 | let hasMore = true; 313 | 314 | while (hasMore) { 315 | const response = await slack.users.list({ 316 | token: SLACK_TOKEN, 317 | limit: 1000, 318 | cursor, 319 | }); 320 | 321 | if (response.members) { 322 | allUsers.push(...response.members); 323 | } 324 | 325 | cursor = response.response_metadata?.next_cursor; 326 | hasMore = !!cursor; 327 | } 328 | 329 | return { 330 | contents: [ 331 | { 332 | uri: uri.href, 333 | text: JSON.stringify(allUsers, null, 2), 334 | mimeType: "application/json", 335 | }, 336 | ], 337 | }; 338 | } catch (error: any) { 339 | console.error("Error fetching all users:", error); 340 | throw new McpError( 341 | ErrorCode.InternalError, 342 | `Failed to fetch all users: ${error.message || "Unknown error"}` 343 | ); 344 | } 345 | } 346 | ); 347 | 348 | // Resource: all_channels 349 | server.resource( 350 | "all_channels", 351 | new ResourceTemplate("allchannels://", { list: undefined }), 352 | async (uri) => { 353 | try { 354 | // Get all channels (handle pagination internally) 355 | const allChannels: any[] = []; 356 | let cursor; 357 | let hasMore = true; 358 | 359 | while (hasMore) { 360 | const response = await slack.conversations.list({ 361 | token: SLACK_TOKEN, 362 | limit: 1000, 363 | cursor, 364 | types: "public_channel,private_channel", 365 | }); 366 | 367 | if (response.channels) { 368 | allChannels.push(...response.channels); 369 | } 370 | 371 | cursor = response.response_metadata?.next_cursor; 372 | hasMore = !!cursor; 373 | } 374 | 375 | return { 376 | contents: [ 377 | { 378 | uri: uri.href, 379 | text: JSON.stringify(allChannels, null, 2), 380 | mimeType: "application/json", 381 | }, 382 | ], 383 | }; 384 | } catch (error: any) { 385 | console.error("Error fetching all channels:", error); 386 | throw new McpError( 387 | ErrorCode.InternalError, 388 | `Failed to fetch all channels: ${error.message || "Unknown error"}` 389 | ); 390 | } 391 | } 392 | ); 393 | 394 | async function main() { 395 | try { 396 | // Validate Slack token before starting the server 397 | await validateSlackToken(); 398 | 399 | // Start receiving messages on stdin and sending messages on stdout 400 | const transport = new StdioServerTransport(); 401 | await server.connect(transport); 402 | console.error("Slack Search MCP server running on stdio"); 403 | } catch (error: any) { 404 | console.error("Failed to start MCP server:", error); 405 | process.exit(1); 406 | } 407 | } 408 | 409 | if (import.meta.main) { 410 | main(); 411 | } 412 | ```