#
tokens: 4584/50000 7/7 files
lines: off (toggle) GitHub
raw markdown copy
# 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();
}

```