slack-relay-mcp
v2.0.7
Published
Code-execution-based Slack MCP tool — CLI + TypeScript API + Claude Code skill
Downloads
2,201
Maintainers
Readme
slack-relay-mcp
Early Development — This project is under active development and not yet stable. APIs, commands, and configuration may change without notice. Use at your own risk.
A token-efficient Slack CLI and TypeScript API built for AI coding agents. Read channels, search messages, download files, send replies, manage pins and reactions, create canvases, and stream responses -- all from your terminal or agent code.
Zero Slack SDK. Direct fetch calls for full control. Three output modes (summary/csv/full) that cut token usage by 97% compared to raw Slack API responses. Ships as a CLI, a code-execution skill, and a traditional MCP server (stdio + Streamable HTTP).
slackrelay history '#feedback' --limit 50 --has-files
slackrelay download F0ABC123 --thumb thumb_720
slackrelay send '#engineering' 'Deploy complete'Why This Exists
Every Slack MCP server in the ecosystem is missing something: file downloads, token-efficient output, proper rate limiting, or Block Kit parsing. Most return raw Slack API JSON -- 2,000 tokens per message -- which fills an agent's context window after 12 messages.
slack-relay-mcp solves all of these:
- ~50 tokens per message in summary mode (vs ~2,000 raw) -- agents can scan 100+ messages in one call
- Streaming file downloads with thumbnail selection (5-30KB previews instead of 1-10MB originals)
- Full Block Kit parser -- bot messages from GitHub, Jira, Grafana, Datadog render as clean text, not empty strings
- Proactive rate limiting with per-method token buckets -- no 429 cascades
- Mentions resolved inline --
<@U0ABC>becomes@alice, not a meaningless ID
Installation
npm install -g slack-relay-mcpOr run directly:
npx slack-relay-mcp channelsOr with Bun (fastest):
bun add -g slack-relay-mcpRequirements: Node.js 18+ or Bun 1.0+
Quick Start
1. Create a Slack App and Get Tokens
Go to api.slack.com/apps and click Create New App → From scratch
Name it (e.g. "SlackRelay") and select your workspace
In the left sidebar, go to OAuth & Permissions
Scroll to Scopes → Bot Token Scopes and add:
| Scope | Purpose | |-------|---------| |
channels:history| Read messages from public channels | |channels:read| List channels | |groups:history| Read messages from private channels | |groups:read| List private channels | |chat:write| Send messages, replies, and ephemeral messages | |reactions:read| Read reactions | |reactions:write| Add/remove reactions | |files:read| Download files and get file info | |files:write| Upload files | |users:read| Look up user profiles | |pins:read| List pinned messages | |pins:write| Pin/unpin messages | |bookmarks:read| List bookmarks | |bookmarks:write| Add/remove bookmarks | |canvases:read| Read canvas content | |canvases:write| Create and update canvases |Scroll up and click Install to Workspace → Allow
You'll now see an OAuth Tokens block with two tokens. Copy them according to this mapping:
| Slack UI label | Starts with | Copy into env var | Used for | |---|---|---|---| | Bot User OAuth Token |
xoxb-|SLACK_BOT_TOKEN| Everything except search | | User OAuth Token |xoxp-|SLACK_USER_TOKEN|slackrelay searchonly |Both tokens can be copied from the same page; you only see the User OAuth Token after you add at least one User Token Scope (see below) and reinstall.
Optional scopes for additional features:
chat:write.customize— scheduled messages with custom sender nameusers.profile:write— set your Slack status via theuser-profilecommand
For search: Bot tokens cannot use Slack search. Under User Token Scopes, add search:read, then reinstall the app. Copy the User OAuth Token (xoxp-...) into SLACK_USER_TOKEN.
Gotcha: Every time you add a scope or reinstall the app, Slack regenerates both tokens — the old values stop working. Paste the fresh values into wherever you configured them. See "Scopes only take effect after reinstall" at the end of this section.
User-token-only deployments
If you want slackrelay to act as you (post as your user, not a separate bot) — common when the app will be used by a single operator or an AI agent like Claude Code — put the whole scope list under User Token Scopes instead of Bot Token Scopes, and use the xoxp-... User OAuth Token for SLACK_BOT_TOKEN. At minimum add:
| User scope | Unlocks |
|-----------|---------|
| chat:write | Sending / replying / editing messages |
| users:read | Looking up user IDs for @mentions and DM routing |
| search:read | slackrelay search |
| channels:history, groups:history, im:history, mpim:history | Reading channel/DM history |
| channels:read, groups:read, im:read, mpim:read | Listing channels / channel members |
| files:read | Downloading files |
| reactions:read, reactions:write | Reading / adding reactions |
Add only what you actually use — unused scopes are noise.
Scopes only take effect after reinstall. Adding a scope in the Slack app UI does not update tokens that are already issued. After changing scopes, click Reinstall to Workspace on the OAuth & Permissions page and copy the fresh token into your
.env.slackrelay auth-checkis a static capability map, not a live scope probe — ifchat.postMessagereturnsmissing_scopeeven thoughauth-checksayschat:writeis granted, you skipped the reinstall.
2. Channel Access — Public vs Private
Slack has two types of channels, and the bot's access differs for each:
| | Public channels (#) | Private channels (🔒) |
|---|---|---|
| Icon in Slack | # hash symbol | 🔒 lock symbol |
| Read messages | Yes, without invite | Only if bot is invited |
| Send / react / pin | Only if bot is invited | Only if bot is invited |
| List in channels | Always visible | Only if bot is invited |
To invite the bot to a channel, type /invite @YourBotName in that channel.
If you get a not_in_channel error, the bot needs to be invited first. For public channels you can read without an invite, but any write operation (sending messages, adding reactions, pinning) requires the bot to be a member.
3. Configure Your Token
slack-relay-mcp reads two env vars: SLACK_BOT_TOKEN (required, xoxb-) and SLACK_USER_TOKEN (optional, xoxp-, only for search). Where you set them depends on how you'll run the tool:
| Use case | Where tokens go | File path |
|---|---|---|
| slackrelay CLI in your terminal | .env file | ~/.config/slackrelay/.env |
| MCP server via an AI client (Claude Desktop / Code, Cursor, Codex) | env block inside the client's MCP config | client-specific (see below) |
| MCP server self-hosted over HTTP | real environment variables on the server | via Docker / systemd / your process manager |
Pick the section that matches your setup — each is self-contained.
A. CLI (global install) — ~/.config/slackrelay/.env
Install globally, then create exactly one file: ~/.config/slackrelay/.env.
npm install -g slack-relay-mcp
mkdir -p ~/.config/slackrelay
cat > ~/.config/slackrelay/.env << 'EOF'
SLACK_BOT_TOKEN=xoxb-your-bot-token
SLACK_USER_TOKEN=xoxp-your-user-token
EOFVerify it works:
slackrelay auth-check # prints configured token summary
slackrelay channels # hits the API — proves the token is validThings to know about this file:
- One
KEY=VALUEper line. No quotes needed. Theexportprefix is allowed but not required. SLACK_USER_TOKENis optional — skip it if you don't need search.- The CLI reads this file on every invocation — no shell restart needed.
- If you also have a
.envin your current working directory, it overrides the global file for that directory. - Explicit shell env vars (
SLACK_BOT_TOKEN=xoxb-... slackrelay …) always win. - Load order: shell env >
./.env(cwd) >~/.config/slackrelay/.env(global).
Don't want the raw token sitting in plaintext? See Secure credential storage below — you can back it with macOS Keychain, 1Password, or pass instead.
B. MCP server (stdio) — token in client config
For AI agents and MCP clients (Claude Desktop, Claude Code, Cursor, etc). The token goes in the client's MCP configuration — not in a .env file:
{
"mcpServers": {
"slack-relay": {
"command": "npx",
"args": ["-y", "slack-relay-mcp"],
"env": {
"SLACK_BOT_TOKEN": "xoxb-your-bot-token",
"SLACK_USER_TOKEN": "xoxp-your-user-token"
}
}
}
}The env block passes tokens directly to the process at startup. See Client Configuration for exact config file paths and formats for each client (Claude Desktop, Claude Code, Cursor, Codex).
C. MCP server (HTTP / self-hosted) — environment variables
For running the MCP server on a remote host or in Docker. Set tokens as environment variables on the server:
export SLACK_BOT_TOKEN=xoxb-your-bot-token
export SLACKRELAY_MCP_AUTH_BEARER=your-secret-bearer-token
slackrelay mcp --transport http --port 8080Clients connect via URL and authenticate with the bearer token — they don't need the Slack token:
{
"mcpServers": {
"slack-relay": {
"url": "https://slack-mcp.you.dev/mcp",
"headers": {
"Authorization": "Bearer your-secret-bearer-token"
}
}
}
}See Docker for running with Docker Compose.
Secure credential storage (optional)
For CLI use, you can point any token env var at an external credential store instead of storing the literal secret in .env. The CLI resolves URI-scheme values at startup:
| Scheme | Example | Backed by |
|--------|---------|-----------|
| op://Vault/Item/field | op://Personal/Slackrelay/token | 1Password CLI (op read). Works with biometric unlock locally and OP_SERVICE_ACCOUNT_TOKEN in CI. |
| keyring://service/account | keyring://slackrelay/slack-token | OS keyring — macOS Keychain, libsecret (GNOME/KDE), Windows Credential Manager. |
| pass:path | pass:slack/token | pass password-store. |
| xoxb-… / xoxp-… | (literal) | Plaintext value in .env (default, current behaviour). |
Set it up once with the auth set command — it stores the secret in the chosen backend and writes the reference (not the secret) to ~/.config/slackrelay/.env:
# Store in OS keyring (Keychain on macOS). Token read from stdin or prompted.
echo "xoxb-your-bot-token" | slackrelay auth set --keyring
# Point at an existing 1Password item (validated by a test read)
slackrelay auth set --op op://Personal/Slackrelay/token
# Point at an existing pass entry
slackrelay auth set --pass slack/bot
# Or keep plaintext explicitly
slackrelay auth set --token xoxb-your-bot-tokenOther env vars are set the same way:
slackrelay auth set --var SLACK_USER_TOKEN --keyring < /dev/tty
slackrelay auth set --var SLACKRELAY_MCP_AUTH_BEARER --op op://Vault/Slackrelay/bearerInspect the current setup — values are redacted, only the scheme and identifier are shown:
slackrelay auth show
# → env file: /Users/you/.config/slackrelay/.env
# SLACK_BOT_TOKEN: os keyring: slackrelay/slack-token
# SLACK_USER_TOKEN: 1password: op://Personal/Slackrelay/user
# SLACKRELAY_MCP_AUTH_BEARER: (not set)@napi-rs/keyring is an optional dependency — keyring:// fails loudly with install instructions if the native binding didn't install on your platform. op:// and pass: require the respective CLI on PATH.
1Password: full walkthrough
If you already have tokens in Slack's OAuth & Permissions page, here's the end-to-end setup using the 1Password desktop app (already the most common case):
Enable CLI integration (one time) — in the 1Password desktop app: Settings → Developer → Integrate with 1Password CLI + Biometric unlock for 1Password CLI. Verify with
op whoami.Store the tokens. In 1Password, create a new item (API Credential or Password works) in any vault. Title it e.g.
Slackrelayand add two fields:| 1Password field name | Slack UI label | Starts with | |---|---|---| |
bot-token| Bot User OAuth Token |xoxb-| |user-token| User OAuth Token |xoxp-|Verify the references resolve (replace
Employeewith whatever vault you used):op read "op://Employee/Slackrelay/bot-token" op read "op://Employee/Slackrelay/user-token"Wire them into slackrelay:
slackrelay auth set --var SLACK_BOT_TOKEN --op "op://Employee/Slackrelay/bot-token" slackrelay auth set --var SLACK_USER_TOKEN --op "op://Employee/Slackrelay/user-token"Each command calls
op readonce to validate, then writes just the reference to~/.config/slackrelay/.env. Your.envnow looks like:SLACK_BOT_TOKEN=op://Employee/Slackrelay/bot-token SLACK_USER_TOKEN=op://Employee/Slackrelay/user-tokenConfirm end-to-end:
slackrelay auth show, thenslackrelay auth-check, thenslackrelay channels --limit 3. The last command actually forces the resolver to run, so you'll see a biometric prompt if 1Password is currently locked.
When you rotate scopes in Slack later, paste the new xoxb-… / xoxp-… back into the same 1Password fields — the references in ~/.config/slackrelay/.env never change.
1Password: biometric prompts vs service accounts
Biometric unlock is per unlocked-state, not per-call. The 1Password CLI talks to the desktop app over a local socket; while the app is unlocked, op read returns instantly with no prompt. When the app auto-locks (desktop setting, usually 10 min idle), the next op read triggers one biometric prompt to unlock it, and then you're good again.
For an interactive CLI on your laptop this is fine — you might see a prompt once per coffee break. For something more automated, you have two options:
| Scenario | What to use | Why | |---|---|---| | Interactive terminal, occasional commands | Desktop app + biometric (the default after step 1) | Zero setup beyond enabling the integration. Prompts are rare. | | AI agent / long-running session / MCP server / CI | 1Password Service Account | No desktop app dependency, no biometric prompts ever, fully headless. |
Service Account setup:
Go to https://my.1password.com/developer-tools/infrastructure-secrets/serviceaccount → Create Service Account. Name it e.g.
slackrelay-agent.Grant it Read access to the vault that holds your
Slackrelayitem (or a dedicated vault with just that item — tighter blast radius).Copy the token — it starts with
ops_.Export it somewhere your CLI / agent will inherit:
# In ~/.zshrc / ~/.bashrc (or your launch agent / systemd unit / Dockerfile) export OP_SERVICE_ACCOUNT_TOKEN="ops_..."The token itself is sensitive — treat it like any other bearer secret. You can keep it in the OS keyring if you want (
security add-generic-passwordon macOS) and have your shell profile read it out on startup.
When OP_SERVICE_ACCOUNT_TOKEN is set, op read uses it instead of the desktop app — no biometric, no desktop dependency. Your existing op://Employee/Slackrelay/bot-token URIs work unchanged. If the env var is not set, op falls back to the desktop app, so you can have both configured simultaneously (service account for automation, biometric for interactive shells).
4. Try It
slackrelay channels # list your channels
slackrelay history '#general' --limit 10 # read recent messages
slackrelay user @alice # look up a user
slackrelay auth-check # see what your token can do5. Set Up Your Agent
# Project-level (committed to git, shared with team)
slackrelay init claude-code # → .claude/skills/slackrelay/SKILL.md
slackrelay init cursor # → .cursor/mcp.json
# User-level (available in all projects)
slackrelay init claude-code --global # → ~/.claude/skills/slackrelay/SKILL.md
slackrelay init cursor --global # → ~/.cursor/mcp.jsonMCP Server Mode
Run slack-relay as a traditional MCP server for clients that can't execute code directly. Supports stdio (local) and Streamable HTTP (remote/self-hosted).
# Stdio (default)
npx slack-relay-mcp
# Streamable HTTP
export SLACKRELAY_MCP_AUTH_BEARER=your-secret-token
slackrelay mcp --transport http --port 8080Client Configuration
Claude Desktop
Config file: ~/Library/Application Support/Claude/claude_desktop_config.json (macOS) or %APPDATA%\Claude\claude_desktop_config.json (Windows)
{
"mcpServers": {
"slack-relay": {
"command": "npx",
"args": ["-y", "slack-relay-mcp"],
"env": {
"SLACK_BOT_TOKEN": "xoxb-your-bot-token",
"SLACK_USER_TOKEN": "xoxp-your-user-token"
}
}
}
}Claude Code (CLI)
claude mcp add --transport stdio -e SLACK_BOT_TOKEN=xoxb-... slack-relay -- npx -y slack-relay-mcpOr add to .mcp.json at your project root (committed to git, shared with team):
{
"mcpServers": {
"slack-relay": {
"command": "npx",
"args": ["-y", "slack-relay-mcp"],
"env": {
"SLACK_BOT_TOKEN": "xoxb-your-bot-token"
}
}
}
}For HTTP (remote server):
claude mcp add --transport http slack-relay https://slack-mcp.you.dev/mcp \
--header "Authorization: Bearer YOUR_TOKEN"Cursor
Config file: .cursor/mcp.json (project) or ~/.cursor/mcp.json (global)
Stdio:
{
"mcpServers": {
"slack-relay": {
"command": "npx",
"args": ["-y", "slack-relay-mcp"],
"env": {
"SLACK_BOT_TOKEN": "xoxb-your-bot-token"
}
}
}
}HTTP (remote):
{
"mcpServers": {
"slack-relay": {
"url": "https://slack-mcp.you.dev/mcp",
"headers": {
"Authorization": "Bearer YOUR_TOKEN"
}
}
}
}OpenAI Codex CLI
Config file: ~/.codex/config.toml (user) or .codex/config.toml (project)
Codex uses TOML, not JSON.
[mcp_servers.slack-relay]
command = "npx"
args = ["-y", "slack-relay-mcp"]
[mcp_servers.slack-relay.env]
SLACK_BOT_TOKEN = "xoxb-your-bot-token"HTTP:
[mcp_servers.slack-relay]
url = "https://slack-mcp.you.dev/mcp"
bearer_token_env_var = "SLACKRELAY_MCP_AUTH_BEARER"Docker (any stdio client)
Any client that supports stdio can use the Docker image:
{
"mcpServers": {
"slack-relay": {
"command": "docker",
"args": [
"run", "-i", "--rm",
"-e", "SLACK_BOT_TOKEN", "-e", "SLACK_USER_TOKEN",
"ghcr.io/gabros20/slack-relay-mcp:latest",
"--transport", "stdio"
],
"env": {
"SLACK_BOT_TOKEN": "xoxb-your-bot-token",
"SLACK_USER_TOKEN": "xoxp-your-user-token"
}
}
}
}Self-Hosting (Streamable HTTP)
For remote clients or multi-user setups, run the HTTP transport behind a reverse proxy with TLS.
# Requires a bearer token for auth
export SLACKRELAY_MCP_AUTH_BEARER=your-secret-token
slackrelay mcp --transport http --host 0.0.0.0 --port 8080Docker (recommended):
docker run -d --name slackrelay \
-p 127.0.0.1:8080:8080 \
--env-file .env \
-v slackrelay-cache:/home/app/.cache/slackrelay \
--restart unless-stopped \
ghcr.io/gabros20/slack-relay-mcp:latestOr with docker compose:
cp .env.example .env # fill in your tokens
docker compose up -dPut Caddy, nginx, or a Cloudflare Tunnel in front for TLS. Then point any HTTP-capable client at https://slack-mcp.you.dev/mcp with the bearer token.
Write Tool Gating
Write tools (send-message, reaction, pins, etc.) are enabled by default for stdio (process-boundary trust) and disabled by default for HTTP (wider blast radius). Override with:
# Enable writes over HTTP
slackrelay mcp --transport http --enable-writes
# Or via env var
SLACKRELAY_MCP_ENABLE_WRITES=1Fine-grained control:
# Only expose specific tools
SLACKRELAY_MCP_TOOLS=list-channels,get-channel-history,search
# Block specific tools
SLACKRELAY_MCP_DISABLE_TOOLS=file-delete,channel-archiveMCP Server Options
| Flag | Env Var | Default | Description |
|---|---|---|---|
| --transport | | stdio | stdio or http |
| --host | SLACKRELAY_MCP_HOST | 127.0.0.1 (or 0.0.0.0 in Docker) | HTTP bind address |
| --port | SLACKRELAY_MCP_PORT | 8080 | HTTP port |
| --enable-writes | SLACKRELAY_MCP_ENABLE_WRITES | off (HTTP) / on (stdio) | Enable write tools |
| --insecure-no-auth | | | Skip bearer auth (dangerous) |
| --tools | SLACKRELAY_MCP_TOOLS | all | Comma-separated allowlist |
| --disable-tools | SLACKRELAY_MCP_DISABLE_TOOLS | none | Comma-separated denylist |
| | SLACKRELAY_MCP_AUTH_BEARER | | Required for HTTP |
All existing SLACK_* env vars (token, read-only, write-channels, safe-search) continue to apply.
MCP Tool Reference
When running as an MCP server, tool names and schemas differ from CLI command names. Some CLI commands are combined into single MCP tools with an action parameter.
Read tools:
| MCP Tool | CLI Equivalent | Notes |
|----------|---------------|-------|
| list-channels | channels | |
| get-channel-info | channel-info | |
| get-channel-history | history | has_files, include_threads, since_last, mark_read params |
| get-thread | thread | |
| search | search | |
| search-realtime | (MCP only) | Uses assistant.search.context API, returns messages/files/channels/users |
| list-files | files | |
| get-file-info | file-info | |
| download-file | download | Defaults to thumb_1024 + base64 in MCP (vs thumb_720 + file-to-disk in CLI) |
| list-users | users | |
| user-profile | user | Also supports set_status to set your Slack status |
| channel-members | who | |
| presence | presence | |
| user-channels | user-channels | |
| usergroups | usergroups | |
| usergroup-members | usergroup-members | |
| unreads | unreads | |
| read-canvas | canvas-read | |
| scheduled-list | scheduled-list | |
Write tools:
| MCP Tool | CLI Equivalent | Notes |
|----------|---------------|-------|
| send-message | send / edit / delete | Combined: use update_ts to edit, delete_ts to delete |
| reply-to-thread | reply | |
| send-ephemeral | send-ephemeral | |
| reaction | react | action: 'add' \| 'remove' \| 'get' |
| pins | pins / pin | action: 'list' \| 'add' \| 'remove' |
| bookmarks | bookmarks / bookmark / bookmark-edit / bookmark-remove | action: 'list' \| 'add' \| 'edit' \| 'remove' |
| upload-file | upload | |
| file-delete | file-delete | |
| schedule-message | schedule | |
| scheduled-delete | scheduled-delete | |
| create-canvas | canvas-create | |
| update-canvas | canvas-update | |
| canvas-delete | canvas-delete | |
| channel-create | channel-create | |
| set-topic | set-topic | |
| set-purpose | set-purpose | |
| channel-invite | channel-invite | |
| channel-archive | channel-archive | |
| mark-read | mark-read | |
| reminders | remind / reminders / reminder-delete | action: 'add' \| 'list' \| 'delete' |
| ask | ask | Takes timeout_ms (number), not --timeout 5m |
| stream-start | stream-start | |
| stream-append | stream-append | |
| stream-stop | stream-stop | |
Code-Execution Mode (Claude Code)
For agents that can run code directly (Claude Code, Aider), the code-execution pattern is more token-efficient than MCP. Instead of loading all tool schemas upfront, the agent discovers and calls TypeScript functions on demand.
slackrelay init claude-codeThis copies a skill file to .claude/skills/slackrelay/SKILL.md that teaches the agent how to use the CLI. The agent reads the skill, then runs commands like slackrelay history '#feedback' --limit 20 directly.
This uses 98% fewer tokens than traditional MCP because the agent only loads the tool definitions it needs, not all 40+ schemas.
Output Formats
Every read command supports three output modes. Use --format or --json to switch.
Summary (default) -- ~50 tokens/message
One line per message. Designed for agents scanning large volumes. Includes file IDs and thread timestamps inline so the agent can immediately chain to download or thread without extra lookups.
2026-04-01 09:23 @alice: Fixed the login bug [files: bug.png (image/png, 229KB, F0ABC123)] [3 replies, ts:1711929600.000100] [reactions: eyes:2, check:1]
2026-04-01 09:25 @bob: Looks good!
2026-04-01 09:30 @alice [file_share]: screenshot.png (image/png, 1.2MB, F0GHI789)
2026-04-01 09:31 [bot:github] PR #142 merged to mainCSV -- ~30 tokens/message
Minimal overhead for bulk processing. File IDs in structured sub-fields.
ts,user,text,files,reactions,replies,thread_ts
1711929600.000100,alice,Fixed the login bug,F0ABC123:bug.png:image/png:229KB,eyes:2;check:1,3,1711929600.000100Full JSON -- ~200 tokens/message
Structured data with download hints, permalinks, and all metadata. Empty fields automatically pruned (10-20% token savings).
{
"ts": "1711929600.000100",
"user_name": "alice",
"text": "Fixed the login bug",
"permalink": "https://team.slack.com/archives/C0123/p1711929600000100",
"files": [{
"id": "F0ABC123",
"name": "bug.png",
"mimetype": "image/png",
"size_human": "229KB",
"is_image": true,
"download_cmd": "slackrelay download F0ABC123 --thumb thumb_720"
}],
"reactions": [{"name": "eyes", "count": 2}],
"reply_count": 3,
"thread_ts": "1711929600.000100"
}Auto-detection: When piped to another program, output defaults to full JSON. In an interactive terminal, defaults to summary. Override with --format or --json.
Command Reference
Reading
| Command | What It Does | Key Flags |
|---------|-------------|-----------|
| channels | List workspace channels | --types public,private --limit 50 |
| channel-info | Get channel metadata | |
| history | Read channel messages | --limit --oldest 7d --has-files --threads --since-last --mark-read --include-system --extract file_ids |
| thread | Read a full thread | |
| search | Search messages or files | --type messages\|files\|all --sort timestamp --limit |
| files | List files in workspace | --channel --types images --from --to |
| file-info | Get file metadata | |
| download | Download a file | --thumb thumb_720 --output ./dir |
| user | Look up a user | Accepts @name, email, or user ID |
| users | List workspace members | --limit --include-deleted |
| pins | List pinned messages | |
| bookmarks | List channel bookmarks | |
| who | List channel members | --limit |
| unreads | Show unread channels | --types --mentions-only |
| canvas-read | Read canvas as markdown | |
| usergroups | List user groups | --include-disabled |
| usergroup-members | List user group members | |
| scheduled-list | List scheduled messages | --channel --limit |
| reminders | List your reminders | |
| presence | Check if user is active/away | |
| user-channels | List channels a user is in | --types |
| commands | List all commands | --json |
Writing
| Command | What It Does | Key Flags |
|---------|-------------|-----------|
| send | Send a message | --markdown --unfurl --table --metadata '{...}' |
| reply | Reply in thread | --markdown |
| edit | Edit a message | |
| delete | Delete a message | |
| upload | Upload a file | --channel --title --thread --comment |
| react | Add emoji reaction | |
| pin | Pin or unpin a message | --remove to unpin |
| bookmark | Add a bookmark | |
| bookmark-edit | Edit a bookmark | --title --link |
| bookmark-remove | Remove a bookmark | |
| schedule | Schedule a message | --at '+2h' |
| scheduled-delete | Delete scheduled message | |
| canvas-create | Create a canvas | --content |
| canvas-update | Update a canvas | --content --op insert_at_end |
| canvas-delete | Delete a canvas | |
| remind | Add a reminder | --at 'tomorrow 9am' |
| reminder-delete | Delete a reminder | |
| channel-create | Create a channel | --private --description |
| set-topic | Set channel topic | |
| set-purpose | Set channel purpose | |
| channel-invite | Invite users to channel | |
| channel-archive | Archive a channel | |
| file-delete | Delete a file | |
| send-ephemeral | Message visible to one user | |
| ask | Ask question, wait for reply | --timeout 5m --from @user --indicator |
Streaming
Stream agent output into Slack in real-time (Slack Chat Streaming API):
| Command | What It Does | Key Flags |
|---------|-------------|-----------|
| stream-start | Begin streaming | --task-mode timeline\|plan |
| stream-append | Append to stream | --task 'step name' --status pending\|running\|complete\|failed |
| stream-stop | Finalize stream | --blocks '{...}' |
# Basic streaming
slackrelay stream-start '#general' 'Analyzing codebase...'
slackrelay stream-append STREAM_ID 'Found 3 issues...'
slackrelay stream-stop STREAM_ID
# With task progress
slackrelay stream-start '#general' 'Deploying...' --task-mode plan
slackrelay stream-append STREAM_ID 'Running tests' --task 'Tests' --status running
slackrelay stream-append STREAM_ID 'Tests passed' --task 'Tests' --status complete
slackrelay stream-stop STREAM_IDUtilities
| Command | What It Does | Example |
|---------|-------------|---------|
| auth-check | Show token capabilities | slackrelay auth-check |
| parse-url | Extract channel + ts from Slack URL | slackrelay parse-url 'https://team.slack.com/...' |
| open | Auto-route Slack URL to thread or history | slackrelay open 'https://team.slack.com/...' |
| mark-read | Mark a channel as read | slackrelay mark-read '#general' |
| completions | Generate shell completions | slackrelay completions --shell bash |
| init | Scaffold agent config | slackrelay init claude-code |
File Downloads
Slack files are downloaded with Bearer authentication, streaming to disk, and automatic thumbnail selection.
# Download a thumbnail (5-30KB, good for agent review)
slackrelay download F0ABC123 --thumb thumb_720
# Download the original full-size file
slackrelay download F0ABC123 --thumb original
# Download to a specific directory
slackrelay download F0ABC123 --output ./screenshots --channel C0123ABCThumbnail sizes: thumb_360, thumb_480, thumb_720 (default), thumb_960, thumb_1024, original
Files are saved to workspace/files/{channel_id}/{file_id}.{ext}. Downloads are deduplicated -- if the file already exists, it returns immediately with cached: true.
Search
Search uses Slack's search syntax. Requires a user token (SLACK_USER_TOKEN).
slackrelay search 'bug has:image in:#feedback after:2026-03-25'
slackrelay search 'from:@alice deploy' --type files
slackrelay search 'urgent' --type all --sort timestamp --limit 50Search operators:
| Operator | Example | Meaning |
|----------|---------|---------|
| from: | from:@alice | Messages from a user |
| in: | in:#feedback | In a specific channel |
| has: | has:image, has:file, has:link | With attachments |
| before: | before:2026-04-01 | Before a date |
| after: | after:2026-03-25 | After a date |
| during: | during:week, during:today | Time period |
Combine freely: from:@client has:image in:#feedback after:2026-03-25
Sending Messages
# Simple message
slackrelay send '#general' 'Deployment complete'
# DM a user (auto-opens DM channel)
slackrelay send '@alice' 'Hey, check the latest PR'
# Reply in a thread
slackrelay reply '#general' 1711929600.000100 'Fixed in latest push'
# Send with standard markdown (auto-converted to Slack mrkdwn)
slackrelay send '#general' '**Bold** and _italic_' --markdown
# Send as a formatted table
slackrelay send '#general' 'Name | Status\nAlice | Done\nBob | WIP' --table
# Enable link unfurling
slackrelay send '#general' 'Check https://example.com' --unfurl
# Preview without sending
slackrelay send '#general' 'Test message' --dry-runSlack mrkdwn vs Markdown: Slack uses *bold* (not **bold**), _italic_, ~strike~, and <url|text> for links. Use the --markdown flag to send standard Markdown instead -- the tool converts it automatically.
Bullet lists are auto-converted to Slack's native rich_text blocks for proper rendering.
Human-in-the-Loop (ask)
Post a question to Slack and block until a human replies. Useful for agents that need approval, clarification, or input from a teammate during autonomous workflows.
# Ask in a channel, wait up to 5 minutes
slackrelay ask '#eng' 'Deploy to prod? (y/n)' --timeout 5m
# Ask a specific user via DM
slackrelay ask '@alice' 'Which branch should I use?'
# Only accept replies from a specific user
slackrelay ask '#eng' 'Approve release?' --from @alice --timeout 10mThe agent posts the question, then polls with adaptive backoff (3s -> 10s -> 30s -> 60s). Returns the first non-bot reply as structured JSON with the answer text, who replied, a permalink, and how long the wait was.
Agent Workflows
Read 100 messages, identify bugs, download screenshots
# 1. Scan channel (~2,500 tokens for 50 messages with files)
slackrelay history '#feedback' --limit 100 --has-files --oldest 7d
# 2. Drill into a specific thread (~500 tokens)
slackrelay thread '#feedback' 1711929600.000100
# 3. Download the screenshot (~50 tokens + file on disk)
slackrelay download F0ABC123 --thumb thumb_720Total: ~5,000 tokens for 100 messages + 10 threads + 10 files. Without optimization, raw Slack API would need ~200,000 tokens.
Catch up on unread channels
# See what's unread
slackrelay unreads --mentions-only
# Read new messages since last visit
slackrelay history '#engineering' --since-last --mark-readDaily channel summarization
slackrelay history '#engineering' --oldest 1d --format full --json
slackrelay send '#engineering-digest' 'Daily Summary: ...'Post and pin important output
slackrelay send '#team' 'Release v2.4.0 shipped successfully'
slackrelay pin '#team' 1711929600.000100
slackrelay react '#team' 1711929600.000100 rocketUpload a report and share in thread
slackrelay upload ./report.pdf --channel '#team' --title 'Q1 Report' --thread 1711929600.000100 --comment 'Updated numbers attached'Environment Variables
| Variable | Required | Default | Description |
|----------|----------|---------|-------------|
| SLACK_BOT_TOKEN | Yes | -- | Bot token (xoxb-...) or user token (xoxp-...) |
| SLACK_USER_TOKEN | No | -- | User token for search. Auto-used when SLACK_BOT_TOKEN is xoxb- |
| SLACK_TEAM_ID | No | -- | Workspace ID. Required for Enterprise Grid org-level tokens |
| SLACK_READ_ONLY | No | false | Disable all write operations |
| SLACK_WRITE_CHANNELS | No | -- | Allowlist (C123,C456) or denylist (!C789) for writes |
| SLACK_SAFE_SEARCH | No | false | Exclude DMs and private channels from search results |
| SLACK_SEARCH_API | No | -- | Set to realtime to use assistant.search.context API |
| SLACK_CACHE_TTL | No | 300 | Cache TTL in seconds (channels/users) |
| SLACK_CACHE_DIR | No | ~/.cache/slackrelay/ | Directory for disk cache |
| SLACKRELAY_MCP_AUTH_BEARER | No | -- | Bearer token for HTTP transport auth |
| SLACKRELAY_MCP_HOST | No | 127.0.0.1 | HTTP bind address |
| SLACKRELAY_MCP_PORT | No | 8080 | HTTP port |
| SLACKRELAY_MCP_ENABLE_WRITES | No | -- | Set to 1 to enable write tools over HTTP |
| SLACKRELAY_MCP_TOOLS | No | -- | Comma-separated tool allowlist |
| SLACKRELAY_MCP_DISABLE_TOOLS | No | -- | Comma-separated tool denylist |
| SLACKRELAY_IN_CONTAINER | No | -- | Set to 1 to default HTTP host to 0.0.0.0 |
Global Flags
| Flag | Description |
|------|-------------|
| --json | Force JSON output (overrides default format) |
| --format <mode> | Output format: summary, csv, or full |
| --dry-run | Preview write operations without executing |
| --verbose | Show API timing, rate limit state, cache hits |
| --help, -h | Show help for any command |
| --version, -v | Show version |
Safety Controls
Read-Only Mode
export SLACK_READ_ONLY=trueDisables all write operations. Prevents prompt injection attacks where a malicious Slack message instructs the agent to post content.
Channel Write Restrictions
# Only allow writes to these channels
export SLACK_WRITE_CHANNELS="C0123,C0456"
# Allow all channels EXCEPT these
export SLACK_WRITE_CHANNELS="!C0789,!C0012"Mixing allowlist and denylist is rejected at startup.
Safe Search
export SLACK_SAFE_SEARCH=trueAuto-excludes DMs and private channels from search results.
Programmatic API
All tools are exported as async TypeScript functions for the code-execution MCP pattern:
import { createSlackClient, getChannelHistory, sendMessage, downloadFile } from 'slack-relay-mcp';
const client = createSlackClient();
// Read messages
const history = await getChannelHistory(
{ channel: '#feedback', limit: 20, has_files: true, format: 'full' },
{ client, cache: new Cache() }
);
// Download a file
const file = await downloadFile(
{ file_id: 'F0ABC123', thumbnail: 'thumb_720' },
{ client, token: process.env.SLACK_BOT_TOKEN }
);
// Send a message
const sent = await sendMessage(
{ channel: '#general', text: 'Deploy complete' },
{ client, cache: new Cache() }
);Or use the generic script runner:
bun run scripts/run.ts get-channel-history '{"channel":"#feedback","limit":20,"has_files":true}'How It Works
slack-relay-mcp follows Anthropic's code-execution MCP pattern: tools are TypeScript files on the filesystem that agents discover, read, and call via code -- not a long-running MCP server process.
- No server. Each command is a fresh script execution.
- Progressive disclosure. Agents list
src/tools/to discover tools, read only what they need. - Data stays in code. Chained operations flow through variables, not the context window.
console.logis the bridge. Only final output returns to the agent.
This gives 98% token savings vs traditional MCP servers that load all tool definitions upfront.
Architecture
src/
├── commands/ 24 command group files (one per domain)
├── tools/ 38 tool files (one per Slack operation)
├── mcp/ MCP server (stdio + Streamable HTTP)
├── utils/ 12 utility modules
│ ├── rate-limiter.ts Token-bucket per API method (56 methods, 4 tiers)
│ ├── cache.ts Atomic disk cache with TTL and refresh-on-miss
│ ├── resolve.ts Channel/user name-to-ID resolution with fuzzy suggestions
│ ├── message-parser.ts Block Kit (20+ types) + mrkdwn + mention resolution
│ ├── file-download.ts Streaming download with redirect handling + HTML guard
│ ├── file-upload.ts Modern 3-step upload flow
│ ├── table-blocks.ts Auto-detect tables in text, convert to Block Kit
│ ├── errors.ts 36 Slack error codes mapped to agent-friendly messages
│ ├── prune.ts Recursive empty-field pruning (10-20% token savings)
│ └── sanitize.ts Filename sanitization + byte formatting
├── client.ts Dual-token Slack client with SIGINT + rate limiting
├── output.ts Summary/CSV/full formatters with pagination + TTY detection
├── types.ts All TypeScript interfaces
└── cli.ts 57 CLI commands via modular CommandDef architectureTests: 1,166 tests, 2,976 assertions.
Token Comparison
| Source | Tokens per message | 100 messages | |--------|-------------------|-------------| | Raw Slack API | ~2,000 | ~200,000 | | slack-relay-mcp full | ~200 | ~20,000 | | slack-relay-mcp summary | ~50 | ~5,000 | | slack-relay-mcp csv | ~30 | ~3,000 |
Agents typically have 25,000-100,000 tokens of context. At 50 tokens/message, an agent can process 500-2,000 messages before running out -- vs 12-50 with raw API JSON.
OAuth Scopes Reference
Bot Token (minimum)
channels:history channels:read groups:history groups:read
files:read files:write users:read
chat:write reactions:read reactions:write
pins:read pins:write bookmarks:read bookmarks:write
canvases:read canvases:writeUser Token (for search)
Add: search:read, users:read.email, im:history, im:read, mpim:history, mpim:read, users.profile:read
Key Technical Details
- Rate limiting: Proactive token-bucket per Slack API method (4 tiers). Reactive 429 handling with
Retry-After. Max 3 retries per request.chat.postMessagehas a dedicated bucket (1/sec/channel). - Caching: Channel and user lists cached to disk with atomic writes. TTL 5 minutes. Refresh-on-miss with 30-second storm guard. Cache-first startup.
- Message parsing: Handles 20+ Block Kit block types including
rich_text,section,header,table,markdown,task_card,plan. Resolves<@USERID>to@name,<#CHANNELID>to#name. Strips HTML entities. Handles email-type files, forwarded threads, and legacy attachments. - File downloads: Streaming to disk (never loaded into memory). Prefers
url_private_download. Handles redirects without leaking auth headers. Detects HTML login page responses. 30-second timeout. Deduplication. Error sentinel files on failure. - File uploads: Modern 3-step flow (
getUploadURLExternal+ POST bytes +completeUploadExternal). Binary support. - Enterprise Grid: Pass
SLACK_TEAM_IDfor org-level tokens. Automatically included on all API calls. - Slack Connect: Detects
is_ext_sharedchannels andfile_access: "check_file_info"stubs. External users marked in output. - Build:
tsupto ESM + TypeScript declarations. Runs on Node 18+ or Bun.
License
MIT
