Skip to main content
Glama
README.md
# slack-bot-mcp

An MCP server that lets an AI agent work in Slack as a bot. The agent can send DMs and channel messages, find members by email or name, read history and threads, react to messages, and wait for replies.

It is built on [Chat SDK](https://chat-sdk.dev) (`chat` + `@chat-adapter/slack`). By default it receives events over Socket Mode, so it needs no public URL. Set `SLACK_MODE=webhook` to receive events through a Request URL instead.

## Quick start

```bash
SLACK_BOT_TOKEN=xoxb-... SLACK_APP_TOKEN=xapp-... npx -y @liusdev/slack-bot-mcp@latest
# or
SLACK_BOT_TOKEN=xoxb-... SLACK_APP_TOKEN=xapp-... pnpm dlx @liusdev/slack-bot-mcp@latest
```

Check the installed version with `npx -y @liusdev/slack-bot-mcp@latest -v` (or `--version`); `--help` lists usage. The `whoami` tool also reports `server_version`.

### Claude Code

```bash
claude mcp add slack-bot \
  -e SLACK_BOT_TOKEN=xoxb-... \
  -e SLACK_APP_TOKEN=xapp-... \
  -- npx -y @liusdev/slack-bot-mcp@latest
```

### Claude Desktop, Cursor and other MCP clients

```json
{
  "mcpServers": {
    "slack-bot": {
      "command": "npx",
      "args": ["-y", "@liusdev/slack-bot-mcp@latest"],
      "env": {
        "SLACK_BOT_TOKEN": "xoxb-...",
        "SLACK_APP_TOKEN": "xapp-..."
      }
    }
  }
}
```

## Create the Slack app

A Slack [app manifest](https://docs.slack.dev/app-manifests/) is a YAML file that describes an app's configuration. The manifest below sets up everything this server needs in one step:

- a bot user that people can DM (the App Home **Messages** tab)
- the bot token scopes the tools use
- the `app_mention` and `message.im` events, which feed `get_inbox`
- Socket Mode, so no public URL is needed

1. Go to [api.slack.com/apps](https://api.slack.com/apps), click **Create New App**, choose **From an app manifest**, pick your workspace, and paste the manifest below.
2. Under **Basic Information → App-Level Tokens**, generate a token with the `connections:write` scope. This is `SLACK_APP_TOKEN` (`xapp-…`).
3. Under **OAuth & Permissions**, install the app to the workspace and copy the **Bot User OAuth Token**. This is `SLACK_BOT_TOKEN` (`xoxb-…`).
4. Invite the bot to any private channel it should read or post in: `/invite @AI Assistant`.

```yaml
display_information:
  name: AI Assistant
features:
  app_home:
    messages_tab_enabled: true
    messages_tab_read_only_enabled: false
  bot_user:
    display_name: AI Assistant
    always_online: true
oauth_config:
  scopes:
    bot:
      - app_mentions:read
      - channels:history
      - channels:read
      - chat:write
      - chat:write.public
      - groups:history
      - groups:read
      - im:history
      - im:read
      - im:write
      - mpim:history
      - mpim:read
      - reactions:read
      - reactions:write
      - users:read
      - users:read.email
settings:
  event_subscriptions:
    bot_events:
      - app_mention
      - message.im
  interactivity:
    is_enabled: true
  org_deploy_enabled: false
  socket_mode_enabled: true
  token_rotation_enabled: false
```

### Use an existing app

To reuse an app you already created, open it at [api.slack.com/apps](https://api.slack.com/apps) and select **App Manifest** in the sidebar. Merge in the scopes, events, `socket_mode_enabled` and `app_home` settings above. Do not paste over the whole manifest: that replaces the app's current configuration and can drop scopes, events or Request URLs it already uses. Save, then click **Reinstall to Workspace** under **OAuth & Permissions** so new scopes take effect. If the app has no app-level token yet, create one as in step 2.

You can drop scopes you do not need. Tools that lack a scope return an error that names the missing scope. `chat:write.public` lets the bot post in public channels without being invited first.

## Tools

| Tool | What it does | Bot scopes |
|---|---|---|
| `whoami` | Bot identity, connection status, granted scopes, and the scopes each tool is missing | none |
| `users_search` | Find members by email, name, display name, @handle or job title | `users:read`, `users:read.email` |
| `user_info` | Profile of one user (ID, email or @handle) | `users:read` |
| `channels_list` | List channels, filtered by type, name or membership | `channels:read`, `groups:read`, `im:read`, `mpim:read` |
| `send_message` | Post to a channel, a thread, or a user's DM: Markdown or plain text, an optional context line, or custom Block Kit. `expect_reply: true` saves the message as a pending question | `chat:write`, `im:write` |
| `edit_message` | Replace the content of a message the bot posted (same formatting options as `send_message`) | `chat:write` |
| `delete_message` | Delete a message the bot posted (bots cannot delete other people's messages) | `chat:write` |
| `conversations_history` | Read recent messages in a channel or DM | `channels:history`, `groups:history`, `im:history`, `mpim:history` |
| `conversations_replies` | Read a thread | same as above |
| `reactions_add`, `reactions_remove` | Add or remove an emoji reaction | `reactions:write` |
| `wait_for_reply` | Wait briefly (default 45 s) for a reply; saves the question as pending on timeout | history scopes |
| `check_replies` | Check all pending questions, or one conversation, once and return any answers | history scopes |
| `get_inbox` | DMs and @mentions the bot received while the server was running | `app_mention` and `message.im` events |

Wherever a tool takes a channel or user, you can pass:

- a channel ID (`C…`), `#channel-name`, or Slack markup (`<#C…>`)
- a user ID (`U…`), `@handle`, email, or display name

For a user, the tool opens a DM with them. If a name matches several users, the tool returns the candidates so the agent can ask which one you mean.

Bot tokens cannot call Slack's `search.messages` API, so this server has no message search.

### Message formatting

`send_message` has three levels of formatting:

- **`text`** alone: standard Markdown (bold, lists, code, links, quotes), or plain text with `format: "plain"`.
- **`context`**: small grey details under a divider at the bottom of the message, such as the Jira ticket or comment a question refers to. Each item is `{ label, value, url? }` or a pre-formatted mrkdwn string:

  ```json
  {
    "target": "quy@example.com",
    "text": "Does `VECURA_CONTENT_BACKEND_URL` have a public staging address?",
    "context": [
      { "label": "Jira", "value": "VEC-123", "url": "https://example.atlassian.net/browse/VEC-123" },
      { "label": "Comment", "value": "#4521", "url": "https://example.atlassian.net/browse/VEC-123?focusedCommentId=4521" }
    ]
  }
  ```

  This renders as: the question, a divider, then **Jira:** VEC-123 · **Comment:** #4521 in small text.
- **`blocks`**: any [Block Kit](https://docs.slack.dev/reference/block-kit/blocks) layout (header, section fields, images and so on). `text` then becomes the notification fallback, and `context` is still appended. Try layouts in the [Block Kit Builder](https://app.slack.com/block-kit-builder). Interactive elements such as buttons are not supported, because the server does not handle clicks. If Slack rejects the blocks, the error lists the invalid fields.

### Errors

Every tool returns failures as an MCP tool error (`isError: true`). The text says what failed, why, and what to do. For example:

```
Permission denied: the Slack bot is missing the OAuth scope `reactions:write` needed to add the :eyes: reaction.
Scopes the bot currently has: chat:write,users:read.
A Slack workspace admin must add it under api.slack.com/apps → your app → OAuth & Permissions → Bot Token Scopes, then reinstall the app to the workspace.
```

The server also explains common Slack errors: `not_in_channel`, `channel_not_found`, `users_not_found`, `is_archived`, `invalid_auth`, and rate limits with the retry delay. Without `users:read.email`, Slack silently leaves emails out of user lists. In that case, email lookups report the missing scope instead of "user not found".

## Configuration

| Variable | Required | Default | Description |
|---|---|---|---|
| `SLACK_BOT_TOKEN` | yes | | Bot token, `xoxb-…` |
| `SLACK_APP_TOKEN` | socket mode | | App-level token with `connections:write`, `xapp-…` |
| `SLACK_MODE` | no | `socket` | `socket` or `webhook` |
| `SLACK_SIGNING_SECRET` | webhook mode | | Signing secret from **Basic Information** |
| `SLACK_MCP_HTTP_PORT` | no | `3000` | Port for incoming Slack events in webhook mode |
| `SLACK_MCP_HTTP_PATH` | no | `/slack/events` | Path for incoming Slack events in webhook mode |
| `SLACK_MCP_ALLOWED_CHANNELS` | no | any | Comma-separated targets `send_message` may post to: channel IDs, `#names`, user IDs or emails |
| `SLACK_MCP_LOG_LEVEL` | no | `info` | `debug`, `info`, `warn`, `error` or `silent`. Logs go to stderr |
| `SLACK_MCP_STATE_DIR` | no | `~/.slack-bot-mcp` | Where pending questions are stored, one JSON file per workspace |
| `SLACK_MCP_PENDING_TTL_DAYS` | no | `7` | Pending questions older than this are dropped |

### Webhook mode

Set `SLACK_MODE=webhook` and `SLACK_SIGNING_SECRET`. The server then listens on `SLACK_MCP_HTTP_PORT`. In the app settings, turn off Socket Mode and set the Request URL under **Event Subscriptions** and **Interactivity** to `https://<your-host>/slack/events`.

## Waiting for replies

People may answer in a minute or the next morning. Polling inside one agent turn is cheap for the server but not for the agent: every `wait_for_reply` call is a model turn that re-reads the whole conversation. So the server keeps waits short and remembers what it is waiting for:

1. The agent sends the question with `send_message` and `expect_reply: true`. The server saves it as a pending question in `SLACK_MCP_STATE_DIR`, which outlives the MCP process and the agent session.
2. If a quick answer is likely, the agent calls `wait_for_reply` once or twice. After two timeouts the tool tells the agent to stop and report back to the user.
3. Later, `check_replies` with no arguments checks every pending question in one call, returns the answers, and removes answered questions from the list.

Something has to run the agent again for step 3; the server cannot wake an agent. Slack's official MCP server has no wait tool either, so agents using it re-read threads the same way. Options:

- The user asks: "check Slack replies".
- A loop in an open session, for example `/loop 30m check Slack replies and continue` in Claude Code.
- A scheduled run, for example a cron job at 8:00 running `claude -p "check Slack replies and continue"`.

Each check is one short turn, instead of one turn every minute.

To let a single `wait_for_reply` call block longer, raise your client's tool timeout and pass a larger `timeout_seconds` (up to 300). In Claude Code, set `MCP_TOOL_TIMEOUT` in milliseconds, for example `MCP_TOOL_TIMEOUT=320000`.

## Running several instances

Every tool except `get_inbox` calls the Slack Web API directly with the bot token. Those tools give the same results no matter how many agents run this server at once. Keep in mind:

- **Events are split.** Slack delivers each event to only one of an app's open Socket Mode connections. Each instance's `get_inbox` therefore sees only part of the incoming messages. `wait_for_reply` and `check_replies` read the Web API, so they are not affected.
- **Connection limit.** Slack allows at most 10 Socket Mode connections per app. Instances beyond that cannot open a socket, so their `get_inbox` reports the connection error. Their other tools still work.
- **Rate limits are shared.** All instances spend the same token's Slack rate limits. A tool that hits a limit fails at once and says how long to wait.

## Development

```bash
pnpm install
pnpm dev          # rebuild on change
pnpm test
pnpm typecheck
pnpm build
npx @modelcontextprotocol/inspector node dist/index.js   # try the tools interactively
```

## Releasing

Pushing a `v*` tag publishes to npm through GitHub Actions (`.github/workflows/publish.yml`). The workflow authenticates with [npm Trusted Publishing](https://docs.npmjs.com/trusted-publishers), so the repository stores no npm token.

```bash
npm version patch        # or minor / major: bumps package.json, commits, and tags
git push --follow-tags   # the tag triggers the publish workflow
```

The workflow fails if the tag does not match the `package.json` version.

TDQS

A3.8/5.0

Scored across 14 tools

Disambiguation4/5

Most tools map to distinct resource+action pairs (send/edit/delete_message, reactions_add/remove, conversations_history/replies), and the descriptions explicitly explain boundaries. The one fuzzy cluster is read/wait behavior: check_replies, wait_for_reply, get_inbox and conversations_history all retrieve incoming messages and differ only in polling semantics and scope, which an agent must read carefully to pick correctly. user_info vs users_search is also mildly overlap-prone but clarified by the descriptions.

Naming Consistency3/5

Conventions are mixed: verb_noun (send_message, edit_message, check_replies, get_inbox, wait_for_reply), noun_verb mirroring Slack's API (channels_list, reactions_add, reactions_remove, conversations_history, conversations_replies), a bare noun (user_info) and a bare verb-ish name (whoami). All names remain readable and most are guessable to anyone familiar with Slack's API, but there is no single predictable pattern to extrapolate from.

Tool Count5/5

14 tools sit comfortably in the well-scoped 3-15 range, and each tool earns its place: identity/context, user & channel discovery, message lifecycle, reading, reactions, and reply-waiting. No obvious filler or redundant duplicate tools.

Completeness4/5

The core bot messaging lifecycle is fully covered: send, edit, delete, read history and thread replies, react/unreact, receive mentions/DMs, and wait/check for replies, plus discovery tools for users and channels. Gaps are minor-to-moderate: no file upload/download, no message search, no channel creation/invite or member listing, and edit/delete are limited to the bot's own messages, but agents can work around these for typical bot workflows.

Maintenance

ActivityMaintained
ResponsivenessNo issues