# 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();
}
```