# 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 |
```