#
tokens: 14789/50000 20/20 files
lines: off (toggle) GitHub
raw markdown copy
# Directory Structure

```
├── .env.sample
├── .github
│   └── workflows
│       ├── ci.yml
│       └── publish.yml
├── .gitignore
├── .node-version
├── .prettierrc
├── CLAUDE.md
├── Dockerfile
├── eslint.config.js
├── examples
│   ├── get_users_http.ts
│   ├── get_users.ts
│   └── README.md
├── LICENSE
├── package-lock.json
├── package.json
├── README.md
├── src
│   ├── index.ts
│   └── schemas.ts
├── ts-node-loader.js
├── tsconfig.build.json
├── tsconfig.dev.json
└── tsconfig.json
```

# Files

--------------------------------------------------------------------------------
/.node-version:
--------------------------------------------------------------------------------

```
20.17.0

```

--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------

```
{
  "semi": true,
  "singleQuote": true,
  "tabWidth": 2,
  "trailingComma": "es5"
} 
```

--------------------------------------------------------------------------------
/.env.sample:
--------------------------------------------------------------------------------

```
EXMAPLES_SLACK_BOT_TOKEN=xoxb-your-slack-token-here
EXMAPLES_SLACK_USER_TOKEN=xoxp-your-slack-token-here
```

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*

# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json

# Runtime data
pids
*.pid
*.seed
*.pid.lock

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage
*.lcov

# nyc test coverage
.nyc_output

# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# Bower dependency directory (https://bower.io/)
bower_components

# node-waf configuration
.lock-wscript

# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release

# Dependency directories
node_modules/
jspm_packages/

# Snowpack dependency directory (https://snowpack.dev/)
web_modules/

# TypeScript cache
*.tsbuildinfo

# Optional npm cache directory
.npm

# Optional eslint cache
.eslintcache

# Optional stylelint cache
.stylelintcache

# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/

# Optional REPL history
.node_repl_history

# Output of 'npm pack'
*.tgz

# Yarn Integrity file
.yarn-integrity

# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local

# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache

# Next.js build output
.next
out

# Nuxt.js build / generate output
.nuxt
dist

# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public

# vuepress build output
.vuepress/dist

# vuepress v2.x temp and cache directory
.temp
.cache

# vitepress build output
**/.vitepress/dist

# vitepress cache directory
**/.vitepress/cache

# Docusaurus cache and generated files
.docusaurus

# Serverless directories
.serverless/

# FuseBox cache
.fusebox/

# DynamoDB Local files
.dynamodb/

# TernJS port file
.tern-port

# Stores VSCode versions used for testing VSCode extensions
.vscode-test

# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*

```

--------------------------------------------------------------------------------
/examples/README.md:
--------------------------------------------------------------------------------

```markdown
# Examples

This directory contains example clients for the Slack MCP Server, demonstrating both stdio and Streamable HTTP transport methods.

## Available Examples

### 1. Stdio Client (`get_users.ts`)
Uses the traditional stdio transport to communicate with the MCP server.

### 2. Streamable HTTP Client (`get_users_http.ts`)
Uses the newer Streamable HTTP transport to communicate with the MCP server over HTTP.

## Setup

1. Set up your environment variables in `.env`:
```env
# For the server
SLACK_BOT_TOKEN=xoxb-your-bot-token
SLACK_USER_TOKEN=xoxp-your-user-token

# For the examples (same values, different variable names)
EXMAPLES_SLACK_BOT_TOKEN=xoxb-your-bot-token
EXMAPLES_SLACK_USER_TOKEN=xoxp-your-user-token
```

## Usage

### Running the Stdio Example

```bash
# Run the stdio client example
npm run examples
```

### Running the HTTP Example

```bash
# Terminal 1: Start the HTTP server
npm run start -- -port 3000

# Terminal 2: Run the HTTP client example
npm run examples:http

# Or specify a custom server URL
npm run examples:http http://localhost:3001/mcp
```

## What the Examples Do

Both examples:
1. Connect to the Slack MCP Server
2. List available tools
3. Call the `slack_get_users` tool with a limit of 100 users
4. Display the retrieved user information including:
   - User name
   - Real name
   - User ID
   - Pagination information if more users are available

## Transport Comparison

### Stdio Transport
- **Pros**: Simple, no network setup required
- **Cons**: Process-based communication, harder to debug network issues
- **Use case**: Local development, direct integration

### Streamable HTTP Transport  
- **Pros**: Standard HTTP, easier debugging, supports web-based clients
- **Cons**: Requires server setup, network configuration
- **Use case**: Web applications, remote clients, production deployments

## Troubleshooting

### Common Issues

1. **Missing environment variables**: Ensure all required `SLACK_BOT_TOKEN`, `SLACK_USER_TOKEN`, `EXMAPLES_SLACK_BOT_TOKEN`, and `EXMAPLES_SLACK_USER_TOKEN` are set.

2. **Connection refused (HTTP)**: Make sure the HTTP server is running on the specified port before running the HTTP client.

3. **Permission errors**: Ensure your Slack tokens have the necessary permissions to list users in your workspace. 
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
# slack-mcp-server

A [MCP(Model Context Protocol)](https://www.anthropic.com/news/model-context-protocol) server for accessing Slack API. This server allows AI assistants to interact with the Slack API through a standardized interface.

## Transport Support

This server supports both traditional and modern MCP transport methods:

- **Stdio Transport** (default): Process-based communication for local integration
- **Streamable HTTP Transport**: HTTP-based communication for web applications and remote clients

## Features

Available tools:

- `slack_list_channels` - List public channels in the workspace with pagination
- `slack_post_message` - Post a new message to a Slack channel
- `slack_reply_to_thread` - Reply to a specific message thread in Slack
- `slack_add_reaction` - Add a reaction emoji to a message
- `slack_get_channel_history` - Get recent messages from a channel
- `slack_get_thread_replies` - Get all replies in a message thread
- `slack_get_users` - Retrieve basic profile information of all users in the workspace
- `slack_get_user_profiles` - Get multiple users' profile information in bulk (efficient for batch operations)
- `slack_search_messages` - Search for messages in the workspace with powerful filters:
  - Basic query search
  - Location filters: `in_channel`
  - User filters: `from_user`, `with`
  - Date filters: `before` (YYYY-MM-DD), `after` (YYYY-MM-DD), `on` (YYYY-MM-DD), `during` (e.g., "July", "2023")
  - Content filters: `has` (emoji reactions), `is` (saved/thread)
  - Sorting options by relevance score or timestamp

## Quick Start

### Installation

```bash
npm install @ubie-oss/slack-mcp-server
```

NOTE: Its now hosted in GitHub Registry so you need your PAT.

### Configuration

You need to set the following environment variables:

- `SLACK_BOT_TOKEN`: Slack Bot User OAuth Token
- `SLACK_USER_TOKEN`: Slack User OAuth Token (required for some features like message search)
- `SLACK_SAFE_SEARCH` (optional): When set to `true`, automatically excludes private channels, DMs, and group DMs from search results. This is enforced server-side and cannot be overridden by clients.

You can also create a `.env` file to set these environment variables:

```
SLACK_BOT_TOKEN=xoxb-your-bot-token
SLACK_USER_TOKEN=xoxp-your-user-token
SLACK_SAFE_SEARCH=true  # Optional: Enable safe search mode
```

### Usage

#### Start the MCP server

**Stdio Transport (default)**:
```bash
npx @ubie-oss/slack-mcp-server
```

**Streamable HTTP Transport**:
```bash
npx @ubie-oss/slack-mcp-server -port 3000
```

You can also run the installed module with node:
```bash
# Stdio transport
node node_modules/.bin/slack-mcp-server

# HTTP transport  
node node_modules/.bin/slack-mcp-server -port 3000
```

**Command Line Options**:
- `-port <number>`: Start with Streamable HTTP transport on specified port
- `-h, --help`: Show help message

#### Client Configuration

**For Stdio Transport (Claude Desktop, etc.)**:

```json
{
  "slack": {
    "command": "npx",
    "args": [
      "-y",
      "@ubie-oss/slack-mcp-server"
    ],
    "env": {
      "NPM_CONFIG_//npm.pkg.github.com/:_authToken": "<your-github-pat>",
      "SLACK_BOT_TOKEN": "<your-bot-token>",
      "SLACK_USER_TOKEN": "<your-user-token>",
      "SLACK_SAFE_SEARCH": "true"
    }
  }
}
```

**For Streamable HTTP Transport (Web applications)**:

Start the server:
```bash
SLACK_BOT_TOKEN=<your-bot-token> SLACK_USER_TOKEN=<your-user-token> npx @ubie-oss/slack-mcp-server -port 3000
```

Connect to: `http://localhost:3000/mcp`

See [examples/README.md](examples/README.md) for detailed client examples.

## Implementation Pattern

This server adopts the following implementation pattern:

1. Define request/response using Zod schemas
   - Request schema: Define input parameters
   - Response schema: Define responses limited to necessary fields

2. Implementation flow:
   - Validate request with Zod schema
   - Call Slack WebAPI
   - Parse response with Zod schema to limit to necessary fields
   - Return as JSON

For example, the `slack_list_channels` implementation parses the request with `ListChannelsRequestSchema`, calls `slackClient.conversations.list`, and returns the response parsed with `ListChannelsResponseSchema`.

## Development

### Available Scripts

- `npm run dev` - Start the server in development mode with hot reloading
- `npm run build` - Build the project for production
- `npm run start` - Start the production server
- `npm run lint` - Run linting checks (ESLint and Prettier)
- `npm run fix` - Automatically fix linting issues

### Contributing

1. Fork the repository
2. Create your feature branch
3. Run tests and linting: `npm run lint`
4. Commit your changes
5. Push to the branch
6. Create a Pull Request

```

--------------------------------------------------------------------------------
/CLAUDE.md:
--------------------------------------------------------------------------------

```markdown
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

This is a Model Context Protocol (MCP) server that provides AI assistants with standardized access to Slack APIs. It's written in TypeScript and supports both stdio (process-based) and HTTP transport methods.

## Development Commands

### Building and Running
- `npm run build` - Compile TypeScript to JavaScript in `/dist`
- `npm run dev` - Start server in development mode with hot reloading
- `npm start` - Run the production build

### Code Quality
- `npm run lint` - Run both ESLint and Prettier checks
- `npm run fix` - Auto-fix all linting issues
- `npm run lint:eslint` - Run ESLint only
- `npm run lint:prettier` - Run Prettier only

### Examples
- `npm run examples` - Run stdio transport example
- `npm run examples:http` - Run HTTP transport example

## Architecture

### Core Structure
The server follows a schema-driven design pattern:

1. **Request/Response Schemas** (`src/schemas.ts`):
   - All Slack API interactions are validated with Zod schemas
   - Request schemas define input parameters
   - Response schemas filter API responses to only necessary fields

2. **Main Server** (`src/index.ts`):
   - Dual transport support via command-line flag
   - Tool registration and request handling
   - Environment variable validation

### Transport Modes
- **Stdio (default)**: For CLI integration (Claude Desktop, etc.)
- **HTTP**: For web applications via `-port` flag

### Available Tools
All tools follow the pattern: validate request → call Slack API → parse response → return JSON

- Channel operations: list, post message, get history
- Thread operations: reply, get replies  
- User operations: get users, profiles, bulk profiles
- Message operations: search, add reactions

### Tool Selection Guidelines

**When to use `slack_search_messages`:**
- You need to find messages with specific criteria (keywords, user, date range, channel)
- You want to filter/narrow down results based on conditions
- You're looking for targeted information rather than browsing

**When to use `slack_get_channel_history`:**
- You want to see the latest conversation flow without specific filters
- You need ALL messages including bot/automation messages (search excludes these)
- You want to browse messages chronologically with pagination
- You don't have specific search criteria and just want to understand recent activity

### Environment Requirements
Must set in environment or `.env` file:
- `SLACK_BOT_TOKEN`: Bot User OAuth Token
- `SLACK_USER_TOKEN`: User OAuth Token (for search)

## Key Implementation Notes

1. **No Test Suite**: Currently no tests implemented (`"test": "echo \"No tests yet\""`)

2. **Type Safety**: All Slack API responses are parsed through Zod schemas to ensure type safety and limit response size

3. **Error Handling**: The server validates tokens on startup and provides clear error messages

4. **Publishing**: Uses GitHub Package Registry - requires PAT for installation

5. **ES Modules**: Project uses `"type": "module"` - use ES import syntax

## Common Tasks

### Adding a New Slack Tool
1. Define request/response schemas in `src/schemas.ts`
2. Add tool registration in `src/index.ts` server setup
3. Implement handler following existing pattern: validate → API call → parse → return
4. Update README.md with new tool documentation

### Search Messages Considerations
1. **Query Field**: The `query` field accepts plain text search terms only. Modifiers like `from:`, `in:`, `before:` etc. are NOT allowed in the query field - use the dedicated fields instead
2. **Date Search**: The `on:` modifier may not find results due to timezone differences between the Slack workspace and the user's local time
3. **ID-Only Fields**: All search modifier fields require proper Slack IDs for consistency and reliability:
   - `in_channel`: Channel ID (e.g., `C1234567`) - use `slack_list_channels` to find channel IDs. The server automatically converts channel IDs to channel names for search compatibility.
   - `from_user`: User ID (e.g., `U1234567`) - use `slack_get_users` to find user IDs
4. **Required Workflow**: Always use the appropriate listing tools first to convert names to IDs before searching
5. **Debug**: Search queries are logged to console for troubleshooting

### Known API Limitations
1. **Bot Message Exclusion**: The `search.messages` API excludes bot/automation messages by default, unlike the Slack UI
2. **Indexing Delays**: Messages are not indexed immediately; there can be delays between posting and searchability
3. **Proximity Filtering**: When multiple messages match in close proximity, only one result may be returned
4. **Rate Limiting**: Non-Marketplace apps have severe rate limits (1 request/minute, 15 messages max as of 2025)
5. **Comprehensive Alternative**: Use `conversations.history` for retrieving all messages including bot messages

### Modifying Schemas
When updating schemas, ensure backward compatibility and update both request validation and response filtering to maintain efficiency.
```

--------------------------------------------------------------------------------
/ts-node-loader.js:
--------------------------------------------------------------------------------

```javascript
import { register } from 'node:module';
import { pathToFileURL } from 'node:url';

register('ts-node/esm', pathToFileURL('./')); 
```

--------------------------------------------------------------------------------
/tsconfig.dev.json:
--------------------------------------------------------------------------------

```json
{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "outDir": "./dist"
  },
  "include": ["src/**/*", "examples/**/*"],
  "exclude": ["node_modules", "dist"]
} 
```

--------------------------------------------------------------------------------
/tsconfig.build.json:
--------------------------------------------------------------------------------

```json
{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "outDir": "./dist",
    "rootDir": "./src"
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist", "examples"]
} 
```

--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------

```yaml
name: CI

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  lint-and-build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      
      - name: Install dependencies
        run: npm ci
      
      - name: Lint
        run: npm run lint
      
      - name: Build
        run: npm run build

```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
{
  "compilerOptions": {
    "target": "es2022",
    "module": "NodeNext",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "moduleResolution": "NodeNext",
    "resolveJsonModule": true,
    "types": ["node"],
    "lib": ["es2022"],
    "declaration": true,
    "sourceMap": true,
    "allowSyntheticDefaultImports": true
  },
  "ts-node": {
    "esm": true,
    "experimentalSpecifiers": true,
    "project": "./tsconfig.dev.json"
  }
} 
```

--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------

```javascript
import js from "@eslint/js";
import globals from "globals";
import tseslint from "typescript-eslint";
import prettier from "eslint-config-prettier";

export default [
  js.configs.recommended,
  {
    files: ["**/*.{js,mjs,cjs,ts}"],
    languageOptions: {
      globals: { ...globals.browser, ...globals.node },
      ecmaVersion: 2022
    }
  },
  ...tseslint.configs.recommended,
  {
    files: ["**/*.ts"],
    plugins: { "@typescript-eslint": tseslint.plugin },
    languageOptions: {
      parser: tseslint.parser,
      parserOptions: {
        project: true
      }
    },
    rules: {}
  },
  prettier
];
```

--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------

```dockerfile
# Use Node.js base image
FROM node:20-slim AS builder

# Set working directory
WORKDIR /app

# Copy package.json and package-lock.json
COPY package*.json ./

# Install dependencies
RUN npm ci

# Copy source code
COPY . .

# Build TypeScript
RUN npm run build

# Runtime image with minimal footprint
FROM node:20-slim

WORKDIR /app

# Copy only necessary files from builder stage
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package*.json ./
COPY --from=builder /app/node_modules ./node_modules

# Set non-root user for security
USER node

# Set environment variables
ENV NODE_ENV=production

# Run the application
ENTRYPOINT ["node", "dist/index.js"]

```

--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------

```yaml
name: Publish Package

on:
  release:
    types: [created]

jobs:
  build-and-publish:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    steps:
      - uses: actions/checkout@v4
      
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          registry-url: 'https://npm.pkg.github.com'
          scope: '@ubie-oss'
          cache: 'npm'
      
      - name: Install dependencies
        run: npm ci
      
      - name: Lint
        run: npm run lint
      
      - name: Publish to GitHub Packages
        run: npm publish
        env:
          NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}}

```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
{
  "name": "@ubie-oss/slack-mcp-server",
  "version": "0.1.4",
  "description": "A Slack MCP server",
  "main": "dist/index.js",
  "type": "module",
  "bin": {
    "slack-mcp-server": "dist/index.js"
  },
  "scripts": {
    "dev": "node --import ./ts-node-loader.js src/index.ts",
    "build": "tsc -p tsconfig.build.json && shx chmod +x dist/*.js",
    "start": "node dist/index.js",
    "test": "echo \"No tests yet\"",
    "lint": "npm run lint:eslint && npm run lint:prettier",
    "lint:eslint": "eslint \"src/**/*.ts\" \"examples/**/*.ts\"",
    "lint:prettier": "prettier --check \"src/**/*.ts\" \"examples/**/*.ts\"",
    "fix": "npm run fix:eslint && npm run fix:prettier",
    "fix:eslint": "eslint \"src/**/*.ts\" \"examples/**/*.ts\" --fix",
    "fix:prettier": "prettier --write \"src/**/*.ts\" \"examples/**/*.ts\"",
    "examples": "node --import ./ts-node-loader.js examples/get_users.ts",
    "examples:http": "node --import ./ts-node-loader.js examples/get_users_http.ts",
    "prepublishOnly": "npm run build"
  },
  "keywords": [
    "mcp",
    "slack"
  ],
  "author": "Ubie, Inc.",
  "repository": {
    "type": "git",
    "url": "https://github.com/ubie-oss/slack-mcp-server.git"
  },
  "homepage": "https://github.com/ubie-oss/slack-mcp-server",
  "bugs": {
    "url": "https://github.com/ubie-oss/slack-mcp-server/issues"
  },
  "license": "Apache-2.0",
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.12.1",
    "@slack/web-api": "^7.9.1",
    "@types/node": "^20.10.3",
    "dotenv": "^16.4.7",
    "express": "^5.1.0",
    "typescript": "^5.3.2",
    "zod": "^3.22.4",
    "zod-to-json-schema": "^3.22.4"
  },
  "devDependencies": {
    "@eslint/js": "^9.24.0",
    "@types/express": "^5.0.3",
    "@typescript-eslint/eslint-plugin": "^6.19.0",
    "@typescript-eslint/parser": "^6.19.0",
    "eslint": "^8.57.1",
    "eslint-config-prettier": "^9.1.0",
    "globals": "^16.0.0",
    "prettier": "^3.2.2",
    "shx": "^0.3.4",
    "ts-node": "^10.9.2",
    "typescript-eslint": "^8.29.1"
  }
}

```

--------------------------------------------------------------------------------
/examples/get_users_http.ts:
--------------------------------------------------------------------------------

```typescript
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
import { config } from 'dotenv';
import { CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js';
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';

// Load environment variables from .env file
config();

// Get and validate necessary environment variables
const slackToken = process.env.EXMAPLES_SLACK_BOT_TOKEN;
const userToken = process.env.EXMAPLES_SLACK_USER_TOKEN;

if (!slackToken) {
  throw new Error('EXMAPLES_SLACK_BOT_TOKEN environment variable is required');
}

if (!userToken) {
  throw new Error('EXMAPLES_SLACK_USER_TOKEN environment variable is required');
}

async function main() {
  // Parse command line arguments for server URL
  const args = process.argv.slice(2);
  const serverUrl = args[0] || 'http://localhost:3000/mcp';

  console.log(`Connecting to MCP server at: ${serverUrl}`);

  // Initialize MCP client
  const client = new Client(
    {
      name: 'slack-get-users-http-example-client',
      version: '1.0.0',
    },
    {
      capabilities: {},
    }
  );

  // Create Streamable HTTP transport for connecting to the server
  const transport = new StreamableHTTPClientTransport(new URL(serverUrl));

  try {
    // Connect to the server
    await client.connect(transport);
    console.log('Connected to MCP server via HTTP');

    // List available tools
    const toolsResponse = await client.listTools();
    console.log(
      'Available tools:',
      toolsResponse.tools.map((t) => t.name).join(', ')
    );

    // Call slack_get_users
    console.log('\nCalling slack_get_users...');
    const response = (await client.callTool(
      {
        name: 'slack_get_users',
        arguments: {
          limit: 100,
        },
      },
      CallToolResultSchema
    )) as CallToolResult;

    if (
      Array.isArray(response.content) &&
      response.content.length > 0 &&
      response.content[0]?.type === 'text'
    ) {
      // Parse the response and display user information
      const slackResponse = JSON.parse(response.content[0].text);

      console.log('Slack users retrieved successfully!');
      console.log('Total users:', slackResponse.members?.length || 0);

      // Display basic information for each user
      if (slackResponse.members && slackResponse.members.length > 0) {
        console.log('\nUser information:');
        slackResponse.members.forEach(
          (user: { id: string; name: string; real_name?: string }) => {
            console.log(
              `- ${user.name} (${user.real_name || 'N/A'}) [ID: ${user.id}]`
            );
          }
        );

        // Display pagination information if available
        if (slackResponse.response_metadata?.next_cursor) {
          console.log(
            `\nMore users available. Next cursor: ${slackResponse.response_metadata.next_cursor}`
          );
        }
      }
    } else {
      console.error('Unexpected response format');
    }
  } catch (error) {
    console.error('Error:', error);
    process.exit(1);
  } finally {
    // Close the connection
    await transport.close();
    console.log('Connection closed');
  }
}

main();

```

--------------------------------------------------------------------------------
/examples/get_users.ts:
--------------------------------------------------------------------------------

```typescript
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
import { config } from 'dotenv';
import { CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js';
import { fileURLToPath } from 'node:url';
import { dirname, resolve } from 'node:path';
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';

// Get the directory of the current file
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

// Load environment variables from .env file
config();

// Get and validate necessary environment variables
const slackToken = process.env.EXMAPLES_SLACK_BOT_TOKEN;
const userToken = process.env.EXMAPLES_SLACK_USER_TOKEN;

if (!slackToken) {
  throw new Error('EXMAPLES_SLACK_BOT_TOKEN environment variable is required');
}

if (!userToken) {
  throw new Error('EXMAPLES_SLACK_USER_TOKEN environment variable is required');
}

// After validation, can be safely treated as a string
const env = {
  SLACK_BOT_TOKEN: slackToken,
  SLACK_USER_TOKEN: userToken,
} as const satisfies Record<string, string>;

async function main() {
  // Initialize MCP client
  const client = new Client(
    {
      name: 'slack-get-users-example-client',
      version: '1.0.0',
    },
    {
      capabilities: {},
    }
  );

  // Create transport for connecting to the server
  const transport = new StdioClientTransport({
    command: process.execPath,
    args: [
      '--import',
      resolve(__dirname, '../ts-node-loader.js'),
      resolve(__dirname, '../src/index.ts'),
    ],
    env,
  });

  try {
    // Connect to the server
    await client.connect(transport);
    console.log('Connected to MCP server');

    // List available tools
    const toolsResponse = await client.listTools();
    console.log('Available tools:', toolsResponse.tools);

    // Call slack_get_users
    const response = (await client.callTool(
      {
        name: 'slack_get_users',
        arguments: {
          limit: 100,
        },
      },
      CallToolResultSchema
    )) as CallToolResult;

    if (
      Array.isArray(response.content) &&
      response.content.length > 0 &&
      response.content[0]?.type === 'text'
    ) {
      // Parse the response and display user information
      const slackResponse = JSON.parse(response.content[0].text);

      console.log('Slack users retrieved successfully!');
      console.log('Total users:', slackResponse.members?.length || 0);

      // Display basic information for each user
      if (slackResponse.members && slackResponse.members.length > 0) {
        console.log('\nUser information:');
        slackResponse.members.forEach(
          (user: { id: string; name: string; real_name?: string }) => {
            console.log(
              `- ${user.name} (${user.real_name || 'N/A'}) [ID: ${user.id}]`
            );
          }
        );

        // Display pagination information if available
        if (slackResponse.response_metadata?.next_cursor) {
          console.log(
            `\nMore users available. Next cursor: ${slackResponse.response_metadata.next_cursor}`
          );
        }
      }
    } else {
      console.error('Unexpected response format');
    }
  } catch (error) {
    console.error('Error:', error);
    process.exit(1);
  } finally {
    // Close the connection
    await transport.close();
  }
}

main();

```

--------------------------------------------------------------------------------
/src/schemas.ts:
--------------------------------------------------------------------------------

```typescript
import { z } from 'zod';

//
// Basic schemas
//

export const ChannelSchema = z
  .object({
    conversation_host_id: z.string().optional(),
    created: z.number().optional(),
    id: z.string().optional(),
    is_archived: z.boolean().optional(),
    name: z.string().optional(),
    name_normalized: z.string().optional(),
    num_members: z.number().optional(),
    purpose: z
      .object({
        creator: z.string().optional(),
        last_set: z.number().optional(),
        value: z.string().optional(),
      })
      .optional(),
    shared_team_ids: z.array(z.string()).optional(),
    topic: z
      .object({
        creator: z.string().optional(),
        last_set: z.number().optional(),
        value: z.string().optional(),
      })
      .optional(),
    updated: z.number().optional(),
  })
  .strip();

const ReactionSchema = z
  .object({
    count: z.number().optional(),
    name: z.string().optional(),
    url: z.string().optional(),
    users: z.array(z.string()).optional(),
  })
  .strip();

const ConversationsHistoryMessageSchema = z
  .object({
    reactions: z.array(ReactionSchema).optional(),
    reply_count: z.number().optional(),
    reply_users: z.array(z.string()).optional(),
    reply_users_count: z.number().optional(),
    subtype: z.string().optional(),
    text: z.string().optional(),
    thread_ts: z.string().optional(),
    ts: z.string().optional(),
    type: z.string().optional(),
    user: z.string().nullable().optional(),
  })
  .strip();

const MemberSchema = z
  .object({
    id: z.string().optional(),
    name: z.string().optional(),
    real_name: z.string().optional(),
  })
  .strip();

const ProfileSchema = z
  .object({
    display_name: z.string().optional(),
    display_name_normalized: z.string().optional(),
    email: z.string().email().optional(),
    first_name: z.string().optional(),
    last_name: z.string().optional(),
    phone: z.string().optional(),
    real_name: z.string().optional(),
    real_name_normalized: z.string().optional(),
    title: z.string().optional(),
  })
  .strip();

const SearchMessageSchema = z
  .object({
    channel: z
      .object({
        id: z.string().optional(),
        name: z.string().optional(),
      })
      .optional(),
    permalink: z.string().url().optional(),
    text: z.string().optional(),
    ts: z.string().optional(),
    type: z.string().optional(),
    user: z.string().nullable().optional(),
  })
  .strip();

//
// Request schemas
//

export const AddReactionRequestSchema = z.object({
  channel_id: z
    .string()
    .describe('The ID of the channel containing the message'),
  reaction: z.string().describe('The name of the emoji reaction (without ::)'),
  timestamp: z
    .string()
    .regex(/^\d{10}\.\d{6}$/, {
      message: "Timestamp must be in the format '1234567890.123456'",
    })
    .describe(
      "The timestamp of the message to react to in the format '1234567890.123456'"
    ),
});

export const GetChannelHistoryRequestSchema = z.object({
  channel_id: z
    .string()
    .describe(
      'The ID of the channel. Use this tool for: browsing latest messages without filters, getting ALL messages including bot/automation messages, sequential pagination. If you need to search by user, keywords, or dates, use slack_search_messages instead.'
    ),
  cursor: z
    .string()
    .optional()
    .describe('Pagination cursor for next page of results'),
  limit: z
    .number()
    .int()
    .min(1)
    .max(1000) // Align with Slack API's default limit
    .optional()
    .default(100) // The reference repository uses 10, but aligning with list_channels etc., set to 100
    .describe('Number of messages to retrieve (default 100)'),
});

export const GetThreadRepliesRequestSchema = z.object({
  channel_id: z
    .string()
    .describe('The ID of the channel containing the thread'),
  thread_ts: z
    .string()
    .regex(/^\d{10}\.\d{6}$/, {
      message: "Timestamp must be in the format '1234567890.123456'",
    })
    .describe(
      "The timestamp of the parent message in the format '1234567890.123456'. Timestamps in the format without the period can be converted by adding the period such that 6 numbers come after it."
    ),
  cursor: z
    .string()
    .optional()
    .describe('Pagination cursor for next page of results'),
  limit: z
    .number()
    .int()
    .min(1)
    .max(1000)
    .optional()
    .default(100)
    .describe('Number of replies to retrieve (default 100)'),
});

export const GetUsersRequestSchema = z.object({
  cursor: z
    .string()
    .optional()
    .describe('Pagination cursor for next page of results'),
  limit: z
    .number()
    .int()
    .min(1)
    .optional()
    .default(100)
    .describe('Maximum number of users to return (default 100)'),
});

export const GetUserProfilesRequestSchema = z.object({
  user_ids: z
    .array(z.string())
    .min(1)
    .max(100)
    .describe('Array of user IDs to retrieve profiles for (max 100)'),
});

export const ListChannelsRequestSchema = z.object({
  cursor: z
    .string()
    .optional()
    .describe('Pagination cursor for next page of results'),
  limit: z
    .number()
    .int()
    .min(1)
    .max(1000) // Align with Slack API's default limit (conversations.list is actually cursor-based)
    .optional()
    .default(100)
    .describe('Maximum number of channels to return (default 100)'),
});

export const PostMessageRequestSchema = z.object({
  channel_id: z.string().describe('The ID of the channel to post to'),
  text: z.string().describe('The message text to post'),
});

export const ReplyToThreadRequestSchema = z.object({
  channel_id: z
    .string()
    .describe('The ID of the channel containing the thread'),
  text: z.string().describe('The reply text'),
  thread_ts: z
    .string()
    .regex(/^\d{10}\.\d{6}$/, {
      message: "Timestamp must be in the format '1234567890.123456'",
    })
    .describe(
      "The timestamp of the parent message in the format '1234567890.123456'. Timestamps in the format without the period can be converted by adding the period such that 6 numbers come after it."
    ),
});

export const SearchChannelsRequestSchema = z.object({
  query: z
    .string()
    .describe(
      'Search channels by partial name match (case-insensitive). Searches across channel names.'
    ),
  limit: z
    .number()
    .int()
    .min(1)
    .max(100)
    .optional()
    .default(20)
    .describe('Maximum number of channels to return (default 20)'),
  include_archived: z
    .boolean()
    .optional()
    .default(false)
    .describe('Include archived channels in results (default false)'),
});

export const SearchUsersRequestSchema = z.object({
  query: z
    .string()
    .describe(
      'Search users by name, display name, or real name (partial match, case-insensitive)'
    ),
  limit: z
    .number()
    .int()
    .min(1)
    .max(100)
    .optional()
    .default(20)
    .describe('Maximum number of users to return (default 20)'),
  include_bots: z
    .boolean()
    .optional()
    .default(false)
    .describe('Include bot users in results (default false)'),
});

export const SearchMessagesRequestSchema = z.object({
  query: z
    .string()
    .optional()
    .default('')
    .describe(
      'Basic search query text only. Use this tool when you need to: search by keywords, filter by user/date/channel, find specific messages with criteria. For general channel browsing without filters, use slack_get_channel_history instead. Do NOT include modifiers like "from:", "in:", etc. - use the dedicated fields instead.'
    )
    .refine(
      (val) => {
        if (!val) return true;
        const modifierPattern =
          /\b(from|in|before|after|on|during|has|is|with):/i;
        return !modifierPattern.test(val);
      },
      {
        message:
          'Query field cannot contain modifiers (from:, in:, before:, etc.). Please use the dedicated fields for these filters.',
      }
    ),

  in_channel: z
    .string()
    .regex(/^C[A-Z0-9]+$/, {
      message: 'Must be a valid Slack channel ID (e.g., "C1234567")',
    })
    .optional()
    .describe(
      'Search within a specific channel. Must be a Slack channel ID (e.g., "C1234567"). Use slack_list_channels to find channel IDs first.'
    ),

  from_user: z
    .string()
    .regex(/^U[A-Z0-9]+$/, {
      message: 'Must be a valid Slack user ID (e.g., "U1234567")',
    })
    .optional()
    .describe(
      'Search for messages from a specific user. IMPORTANT: You cannot use display names or usernames directly. First use slack_get_users to find the user by name and get their user ID (e.g., "U1234567"), then use that ID here.'
    ),

  // Date modifiers
  before: z
    .string()
    .regex(/^\d{4}-\d{2}-\d{2}$/, {
      message: 'Date must be in YYYY-MM-DD format',
    })
    .optional()
    .describe('Search for messages before this date (YYYY-MM-DD)'),
  after: z
    .string()
    .regex(/^\d{4}-\d{2}-\d{2}$/, {
      message: 'Date must be in YYYY-MM-DD format',
    })
    .optional()
    .describe('Search for messages after this date (YYYY-MM-DD)'),
  on: z
    .string()
    .regex(/^\d{4}-\d{2}-\d{2}$/, {
      message: 'Date must be in YYYY-MM-DD format',
    })
    .optional()
    .describe('Search for messages on this specific date (YYYY-MM-DD)'),
  during: z
    .string()
    .optional()
    .describe(
      'Search for messages during a specific time period (e.g., "July", "2023", "last week")'
    ),

  highlight: z
    .boolean()
    .optional()
    .default(false)
    .describe('Enable highlighting of search results'),
  sort: z
    .enum(['score', 'timestamp'])
    .optional()
    .default('score')
    .describe('Search result sort method (score or timestamp)'),
  sort_dir: z
    .enum(['asc', 'desc'])
    .optional()
    .default('desc')
    .describe('Sort direction (ascending or descending)'),

  count: z
    .number()
    .int()
    .min(1)
    .max(100)
    .optional()
    .default(20)
    .describe('Number of results per page (max 100)'),
  page: z
    .number()
    .int()
    .min(1)
    .max(100)
    .optional()
    .default(1)
    .describe('Page number of results (max 100)'),
});

const SearchPaginationSchema = z.object({
  first: z.number().optional(),
  last: z.number().optional(),
  page: z.number().optional(),
  page_count: z.number().optional(),
  per_page: z.number().optional(),
  total_count: z.number().optional(),
});

//
// Response schemas
//

const BaseResponseSchema = z
  .object({
    error: z.string().optional(),
    ok: z.boolean().optional(),
    response_metadata: z
      .object({
        next_cursor: z.string().optional(),
      })
      .optional(),
  })
  .strip();

export const ConversationsHistoryResponseSchema = BaseResponseSchema.extend({
  messages: z.array(ConversationsHistoryMessageSchema).optional(),
});

export const ConversationsRepliesResponseSchema = BaseResponseSchema.extend({
  messages: z.array(ConversationsHistoryMessageSchema).optional(),
});

export const GetUsersResponseSchema = BaseResponseSchema.extend({
  members: z.array(MemberSchema).optional(),
});

export const UserProfileResponseSchema = BaseResponseSchema.extend({
  profile: ProfileSchema.optional(),
});

export const GetUserProfilesResponseSchema = z.object({
  profiles: z.array(
    z.object({
      user_id: z.string(),
      profile: ProfileSchema.optional(),
      error: z.string().optional(),
    })
  ),
});

export const ListChannelsResponseSchema = BaseResponseSchema.extend({
  channels: z.array(ChannelSchema).optional(),
});

export const SearchMessagesResponseSchema = BaseResponseSchema.extend({
  messages: z
    .object({
      matches: z.array(SearchMessageSchema).optional(),
      pagination: SearchPaginationSchema.optional(),
    })
    .optional(),
});

```

--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------

```typescript
#!/usr/bin/env node

import {
  ListToolsRequestSchema,
  CallToolRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import { zodToJsonSchema } from 'zod-to-json-schema';
import { WebClient } from '@slack/web-api';
import dotenv from 'dotenv';
import express from 'express';
import { randomUUID } from 'node:crypto';
import {
  ListChannelsRequestSchema,
  PostMessageRequestSchema,
  ReplyToThreadRequestSchema,
  AddReactionRequestSchema,
  GetChannelHistoryRequestSchema,
  GetThreadRepliesRequestSchema,
  GetUsersRequestSchema,
  GetUserProfilesRequestSchema,
  ListChannelsResponseSchema,
  GetUsersResponseSchema,
  GetUserProfilesResponseSchema,
  UserProfileResponseSchema,
  SearchMessagesRequestSchema,
  SearchMessagesResponseSchema,
  SearchChannelsRequestSchema,
  SearchUsersRequestSchema,
  ConversationsHistoryResponseSchema,
  ConversationsRepliesResponseSchema,
} from './schemas.js';

dotenv.config();

if (!process.env.SLACK_BOT_TOKEN) {
  console.error(
    'SLACK_BOT_TOKEN is not set. Please set it in your environment or .env file.'
  );
  process.exit(1);
}

if (!process.env.SLACK_USER_TOKEN) {
  console.error(
    'SLACK_USER_TOKEN is not set. Please set it in your environment or .env file.'
  );
  process.exit(1);
}

const slackClient = new WebClient(process.env.SLACK_BOT_TOKEN);
const userClient = new WebClient(process.env.SLACK_USER_TOKEN);

// Safe search mode to exclude private channels and DMs
const safeSearchMode = process.env.SLACK_SAFE_SEARCH === 'true';
if (safeSearchMode) {
  console.error(
    'Safe search mode enabled: Private channels and DMs will be excluded from search results'
  );
}

// Parse command line arguments
function parseArguments() {
  const args = process.argv.slice(2);
  let port: number | undefined;

  for (let i = 0; i < args.length; i++) {
    if (args[i] === '-port' && i + 1 < args.length) {
      const portValue = parseInt(args[i + 1], 10);
      if (isNaN(portValue) || portValue <= 0 || portValue > 65535) {
        console.error(`Invalid port number: ${args[i + 1]}`);
        process.exit(1);
      }
      port = portValue;
      i++; // Skip the next argument since it's the port value
    } else if (args[i] === '--help' || args[i] === '-h') {
      console.log(`
Usage: slack-mcp-server [options]

Options:
  -port <number>    Start the server with Streamable HTTP transport on the specified port
  -h, --help        Show this help message

Examples:
  slack-mcp-server                  # Start with stdio transport (default)
  slack-mcp-server -port 3000       # Start with Streamable HTTP transport on port 3000
`);
      process.exit(0);
    } else if (args[i].startsWith('-')) {
      console.error(`Unknown option: ${args[i]}`);
      console.error('Use --help for usage information');
      process.exit(1);
    }
  }

  return { port };
}

function createServer(): Server {
  const server = new Server(
    {
      name: 'slack-mcp-server',
      version: '0.0.1',
    },
    {
      capabilities: {
        tools: {},
      },
    }
  );

  server.setRequestHandler(ListToolsRequestSchema, async () => {
    return {
      tools: [
        {
          name: 'slack_list_channels',
          description: 'List public channels in the workspace with pagination',
          inputSchema: zodToJsonSchema(ListChannelsRequestSchema),
        },
        {
          name: 'slack_post_message',
          description: 'Post a new message to a Slack channel',
          inputSchema: zodToJsonSchema(PostMessageRequestSchema),
        },
        {
          name: 'slack_reply_to_thread',
          description: 'Reply to a specific message thread in Slack',
          inputSchema: zodToJsonSchema(ReplyToThreadRequestSchema),
        },
        {
          name: 'slack_add_reaction',
          description: 'Add a reaction emoji to a message',
          inputSchema: zodToJsonSchema(AddReactionRequestSchema),
        },
        {
          name: 'slack_get_channel_history',
          description:
            'Get messages from a channel in chronological order. Use this when: 1) You need the latest conversation flow without specific filters, 2) You want ALL messages including bot/automation messages, 3) You need to browse messages sequentially with pagination. Do NOT use if you have specific search criteria (user, keywords, dates) - use slack_search_messages instead.',
          inputSchema: zodToJsonSchema(GetChannelHistoryRequestSchema),
        },
        {
          name: 'slack_get_thread_replies',
          description: 'Get all replies in a message thread',
          inputSchema: zodToJsonSchema(GetThreadRepliesRequestSchema),
        },
        {
          name: 'slack_get_users',
          description:
            'Retrieve basic profile information of all users in the workspace',
          inputSchema: zodToJsonSchema(GetUsersRequestSchema),
        },
        {
          name: 'slack_get_user_profiles',
          description: 'Get multiple users profile information in bulk',
          inputSchema: zodToJsonSchema(GetUserProfilesRequestSchema),
        },
        {
          name: 'slack_search_messages',
          description:
            'Search for messages with specific criteria/filters. Use this when: 1) You need to find messages from a specific user, 2) You need messages from a specific date range, 3) You need to search by keywords, 4) You want to filter by channel. This tool is optimized for targeted searches. For general channel browsing without filters, use slack_get_channel_history instead.',
          inputSchema: zodToJsonSchema(SearchMessagesRequestSchema),
        },
        {
          name: 'slack_search_channels',
          description:
            'Search for channels by partial name match. Use this when you need to find channels containing specific keywords in their names. Returns up to the specified limit of matching channels.',
          inputSchema: zodToJsonSchema(SearchChannelsRequestSchema),
        },
        {
          name: 'slack_search_users',
          description:
            'Search for users by partial name match across username, display name, and real name. Use this when you need to find users containing specific keywords in their names. Returns up to the specified limit of matching users.',
          inputSchema: zodToJsonSchema(SearchUsersRequestSchema),
        },
      ],
    };
  });

  server.setRequestHandler(CallToolRequestSchema, async (request) => {
    try {
      if (!request.params) {
        throw new Error('Params are required');
      }
      switch (request.params.name) {
        case 'slack_list_channels': {
          const args = ListChannelsRequestSchema.parse(
            request.params.arguments
          );
          const response = await slackClient.conversations.list({
            limit: args.limit,
            cursor: args.cursor,
            types: 'public_channel', // Only public channels
          });
          if (!response.ok) {
            throw new Error(`Failed to list channels: ${response.error}`);
          }
          const parsed = ListChannelsResponseSchema.parse(response);

          return {
            content: [{ type: 'text', text: JSON.stringify(parsed) }],
          };
        }

        case 'slack_post_message': {
          const args = PostMessageRequestSchema.parse(request.params.arguments);
          const response = await slackClient.chat.postMessage({
            channel: args.channel_id,
            text: args.text,
          });
          if (!response.ok) {
            throw new Error(`Failed to post message: ${response.error}`);
          }
          return {
            content: [{ type: 'text', text: 'Message posted successfully' }],
          };
        }

        case 'slack_reply_to_thread': {
          const args = ReplyToThreadRequestSchema.parse(
            request.params.arguments
          );
          const response = await slackClient.chat.postMessage({
            channel: args.channel_id,
            thread_ts: args.thread_ts,
            text: args.text,
          });
          if (!response.ok) {
            throw new Error(`Failed to reply to thread: ${response.error}`);
          }
          return {
            content: [
              { type: 'text', text: 'Reply sent to thread successfully' },
            ],
          };
        }
        case 'slack_add_reaction': {
          const args = AddReactionRequestSchema.parse(request.params.arguments);
          const response = await slackClient.reactions.add({
            channel: args.channel_id,
            timestamp: args.timestamp,
            name: args.reaction,
          });
          if (!response.ok) {
            throw new Error(`Failed to add reaction: ${response.error}`);
          }
          return {
            content: [{ type: 'text', text: 'Reaction added successfully' }],
          };
        }

        case 'slack_get_channel_history': {
          const args = GetChannelHistoryRequestSchema.parse(
            request.params.arguments
          );
          const response = await slackClient.conversations.history({
            channel: args.channel_id,
            limit: args.limit,
            cursor: args.cursor,
          });
          if (!response.ok) {
            throw new Error(`Failed to get channel history: ${response.error}`);
          }
          const parsedResponse =
            ConversationsHistoryResponseSchema.parse(response);
          return {
            content: [{ type: 'text', text: JSON.stringify(parsedResponse) }],
          };
        }

        case 'slack_get_thread_replies': {
          const args = GetThreadRepliesRequestSchema.parse(
            request.params.arguments
          );
          const response = await slackClient.conversations.replies({
            channel: args.channel_id,
            ts: args.thread_ts,
            limit: args.limit,
            cursor: args.cursor,
          });
          if (!response.ok) {
            throw new Error(`Failed to get thread replies: ${response.error}`);
          }
          const parsedResponse =
            ConversationsRepliesResponseSchema.parse(response);
          return {
            content: [{ type: 'text', text: JSON.stringify(parsedResponse) }],
          };
        }

        case 'slack_get_users': {
          const args = GetUsersRequestSchema.parse(request.params.arguments);
          const response = await slackClient.users.list({
            limit: args.limit,
            cursor: args.cursor,
          });
          if (!response.ok) {
            throw new Error(`Failed to get users: ${response.error}`);
          }
          const parsed = GetUsersResponseSchema.parse(response);

          return {
            content: [{ type: 'text', text: JSON.stringify(parsed) }],
          };
        }

        case 'slack_get_user_profiles': {
          const args = GetUserProfilesRequestSchema.parse(
            request.params.arguments
          );

          // Use Promise.all for concurrent API calls
          const profilePromises = args.user_ids.map(async (userId) => {
            try {
              const response = await slackClient.users.profile.get({
                user: userId,
              });
              if (!response.ok) {
                return {
                  user_id: userId,
                  error: response.error || 'Unknown error',
                };
              }
              const parsed = UserProfileResponseSchema.parse(response);
              return {
                user_id: userId,
                profile: parsed.profile,
              };
            } catch (error) {
              return {
                user_id: userId,
                error: error instanceof Error ? error.message : 'Unknown error',
              };
            }
          });

          const results = await Promise.all(profilePromises);
          const responseData = GetUserProfilesResponseSchema.parse({
            profiles: results,
          });

          return {
            content: [{ type: 'text', text: JSON.stringify(responseData) }],
          };
        }

        case 'slack_search_messages': {
          const parsedParams = SearchMessagesRequestSchema.parse(
            request.params.arguments
          );

          let query = parsedParams.query || '';

          if (parsedParams.in_channel) {
            // Resolve channel name from ID
            const channelInfo = await slackClient.conversations.info({
              channel: parsedParams.in_channel,
            });
            if (!channelInfo.ok || !channelInfo.channel?.name) {
              throw new Error(
                `Failed to get channel info: ${channelInfo.error}`
              );
            }
            query += ` in:${channelInfo.channel.name}`;
          }

          // Handle from_user - always use user ID format
          if (parsedParams.from_user) {
            query += ` from:<@${parsedParams.from_user}>`;
          }

          // Date modifiers
          if (parsedParams.before) {
            query += ` before:${parsedParams.before}`;
          }
          if (parsedParams.after) {
            query += ` after:${parsedParams.after}`;
          }
          if (parsedParams.on) {
            query += ` on:${parsedParams.on}`;
          }
          if (parsedParams.during) {
            query += ` during:${parsedParams.during}`;
          }

          // Trim and log the final query for debugging
          query = query.trim();
          console.log('Search query:', query);

          const response = await userClient.search.messages({
            query: query,
            highlight: parsedParams.highlight,
            sort: parsedParams.sort,
            sort_dir: parsedParams.sort_dir,
            count: parsedParams.count,
            page: parsedParams.page,
          });

          if (!response.ok) {
            throw new Error(`Failed to search messages: ${response.error}`);
          }

          // Apply safe search filtering if enabled (before parsing)
          if (safeSearchMode && response.messages?.matches) {
            const originalCount = response.messages.matches.length;
            response.messages.matches = response.messages.matches.filter(
              (msg: {
                channel?: {
                  is_private?: boolean;
                  is_im?: boolean;
                  is_mpim?: boolean;
                };
              }) => {
                // Exclude private channels, DMs, and multi-party DMs
                const channel = msg.channel;
                if (!channel) return true; // Keep if no channel info

                return !(
                  channel.is_private ||
                  channel.is_im ||
                  channel.is_mpim
                );
              }
            );

            const filteredCount =
              originalCount - response.messages.matches.length;
            if (filteredCount > 0) {
              console.error(
                `Safe search: Filtered out ${filteredCount} messages from private channels/DMs`
              );
            }
          }

          const parsed = SearchMessagesResponseSchema.parse(response);
          return {
            content: [{ type: 'text', text: JSON.stringify(parsed) }],
          };
        }

        case 'slack_search_channels': {
          const args = SearchChannelsRequestSchema.parse(
            request.params.arguments
          );

          // Fetch all channels with a reasonable limit
          const allChannels: Array<{
            id?: string;
            name?: string;
            is_archived?: boolean;
            [key: string]: unknown;
          }> = [];
          let cursor: string | undefined;
          const maxPages = 5; // Limit to prevent infinite loops
          let pageCount = 0;

          // Fetch multiple pages if needed
          while (pageCount < maxPages) {
            const response = await slackClient.conversations.list({
              types: 'public_channel',
              exclude_archived: !args.include_archived,
              limit: 1000, // Max allowed by Slack API
              cursor,
            });

            if (!response.ok) {
              throw new Error(`Failed to search channels: ${response.error}`);
            }

            if (response.channels) {
              allChannels.push(...(response.channels as typeof allChannels));
            }

            cursor = response.response_metadata?.next_cursor;
            pageCount++;

            // Stop if no more pages
            if (!cursor) break;
          }

          // Filter channels by name (case-insensitive partial match)
          const searchTerm = args.query.toLowerCase();
          const filteredChannels = allChannels.filter((channel) =>
            channel.name?.toLowerCase().includes(searchTerm)
          );

          // Limit results
          const limitedChannels = filteredChannels.slice(0, args.limit);

          const response = {
            ok: true,
            channels: limitedChannels,
          };

          const parsed = ListChannelsResponseSchema.parse(response);
          return {
            content: [{ type: 'text', text: JSON.stringify(parsed) }],
          };
        }

        case 'slack_search_users': {
          const args = SearchUsersRequestSchema.parse(request.params.arguments);

          // Fetch all users with a reasonable limit
          const allUsers: Array<{
            id?: string;
            name?: string;
            real_name?: string;
            is_bot?: boolean;
            profile?: {
              display_name?: string;
              display_name_normalized?: string;
              [key: string]: unknown;
            };
            [key: string]: unknown;
          }> = [];
          let cursor: string | undefined;
          const maxPages = 5; // Limit to prevent infinite loops
          let pageCount = 0;

          // Fetch multiple pages if needed
          while (pageCount < maxPages) {
            const response = await slackClient.users.list({
              limit: 1000, // Max allowed by Slack API
              cursor,
            });

            if (!response.ok) {
              throw new Error(`Failed to search users: ${response.error}`);
            }

            if (response.members) {
              allUsers.push(...(response.members as typeof allUsers));
            }

            cursor = response.response_metadata?.next_cursor;
            pageCount++;

            // Stop if no more pages
            if (!cursor) break;
          }

          // Filter users (case-insensitive partial match across multiple fields)
          const searchTerm = args.query.toLowerCase();
          const filteredUsers = allUsers.filter((user) => {
            // Skip bots if requested
            if (!args.include_bots && user.is_bot) {
              return false;
            }

            // Search across multiple name fields
            const name = user.name?.toLowerCase() || '';
            const realName = user.real_name?.toLowerCase() || '';
            const displayName = user.profile?.display_name?.toLowerCase() || '';
            const displayNameNormalized =
              user.profile?.display_name_normalized?.toLowerCase() || '';

            return (
              name.includes(searchTerm) ||
              realName.includes(searchTerm) ||
              displayName.includes(searchTerm) ||
              displayNameNormalized.includes(searchTerm)
            );
          });

          // Limit results
          const limitedUsers = filteredUsers.slice(0, args.limit);

          const response = {
            ok: true,
            members: limitedUsers,
          };

          const parsed = GetUsersResponseSchema.parse(response);
          return {
            content: [{ type: 'text', text: JSON.stringify(parsed) }],
          };
        }

        default:
          throw new Error(`Unknown tool: ${request.params.name}`);
      }
    } catch (error) {
      console.error('Error handling request:', error);
      const errorMessage =
        error instanceof Error ? error.message : 'Unknown error occurred';
      throw new Error(errorMessage);
    }
  });

  return server;
}

async function runStdioServer() {
  const server = createServer();
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error('Slack MCP Server running on stdio');
}

async function runHttpServer(port: number) {
  const app = express();
  app.use(express.json());

  // Map to store transports by session ID
  const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {};

  // Handle POST requests for client-to-server communication
  app.post('/mcp', async (req, res) => {
    try {
      // Check for existing session ID
      const sessionId = req.headers['mcp-session-id'] as string | undefined;
      let transport: StreamableHTTPServerTransport;

      if (sessionId && transports[sessionId]) {
        // Reuse existing transport
        transport = transports[sessionId];
      } else {
        // Create new transport
        transport = new StreamableHTTPServerTransport({
          sessionIdGenerator: () => randomUUID(),
          onsessioninitialized: (newSessionId) => {
            // Store the transport by session ID
            transports[newSessionId] = transport;
            console.error(`New MCP session initialized: ${newSessionId}`);
          },
        });

        // Clean up transport when closed
        transport.onclose = () => {
          if (transport.sessionId) {
            delete transports[transport.sessionId];
            console.error(`MCP session closed: ${transport.sessionId}`);
          }
        };

        const server = createServer();
        await server.connect(transport);
      }

      // Handle the request
      await transport.handleRequest(req, res, req.body);
    } catch (error) {
      console.error('Error handling MCP request:', error);
      if (!res.headersSent) {
        res.status(500).json({
          jsonrpc: '2.0',
          error: {
            code: -32603,
            message: 'Internal server error',
          },
          id: null,
        });
      }
    }
  });

  // Handle GET requests for server-to-client notifications via SSE
  app.get('/mcp', async (req, res) => {
    const sessionId = req.headers['mcp-session-id'] as string | undefined;
    if (!sessionId || !transports[sessionId]) {
      res.status(400).send('Invalid or missing session ID');
      return;
    }

    const transport = transports[sessionId];
    await transport.handleRequest(req, res);
  });

  // Handle DELETE requests for session termination
  app.delete('/mcp', async (req, res) => {
    const sessionId = req.headers['mcp-session-id'] as string | undefined;
    if (!sessionId || !transports[sessionId]) {
      res.status(400).send('Invalid or missing session ID');
      return;
    }

    const transport = transports[sessionId];
    await transport.handleRequest(req, res);
  });

  // Health check endpoint
  app.get('/health', (req, res) => {
    res.json({ status: 'ok', timestamp: new Date().toISOString() });
  });

  app.listen(port, () => {
    console.error(
      `Slack MCP Server running on Streamable HTTP at http://localhost:${port}/mcp`
    );
    console.error(`Health check available at http://localhost:${port}/health`);
  });
}

async function main() {
  const { port } = parseArguments();

  if (port !== undefined) {
    // Run with Streamable HTTP transport
    await runHttpServer(port);
  } else {
    // Run with stdio transport (default)
    await runStdioServer();
  }
}

main().catch((error) => {
  console.error('Fatal error in main():', error);
  process.exit(1);
});

```