mcp server
slk-mcp
Slack MCP server for Claude Code, GitHub Copilot, Cursor, JetBrains, and any MCP-compatible client. Morning recaps, decision detection, multi-channel digests.
Set up
Runs on your computer. Pick your agent and operating system.
This project does not publish an install method we can turn into a config. Follow the README instead.
README
synced Oct 27, 2026slk-mcp
Slack MCP server for Claude Code, GitHub Copilot, Cursor, JetBrains IDEs, and any MCP-compatible client.
What it does: morning recaps across channels, smart unread summaries, mentions tracking, decision detection, message search, thread reading, and posting.
Written in Go — single ~10 MB binary, ~15 MB Docker image, no runtime, no zombie processes.
Quick start
1. Create a Slack App
Go to api.slack.com/apps → Create New App → From scratch. Pick one of the setups below depending on how you want slk-mcp to act.
Setup A — user token only (recommended for personal use)
Use this if you want slk-mcp to act as you: posts appear under your name, DMs and private channels work out of the box, full unread/mentions support. You don't need to invite anything to channels.
Under OAuth & Permissions → User Token Scopes, add:
channels:history,channels:readgroups:history,groups:readim:history,im:read,mpim:history,mpim:readusers:readusers:read.email(optional — email column inlist_users with_email/email_domain)search:readchat:write(optional — forpost_message)reactions:write(optional — foradd_reaction)reactions:read(optional — completes long reactor lists inget_message reactors=true)canvases:write(optional — foredit_canvas)
Install to Workspace, copy the User OAuth Token (xoxp-...) → SLACK_USER_TOKEN.
Setup B — bot token (shared team use)
Use this if you want slk-mcp to act as a bot: posts appear as the bot, the bot must be invited to channels. Unread/mentions tools stay hidden unless you also add a user token.
Under OAuth & Permissions → Bot Token Scopes, add:
channels:history,channels:readgroups:history,groups:readusers:readchat:write(optional — forpost_message)reactions:write(optional — foradd_reaction)reactions:read(optional — completes long reactor lists inget_message reactors=true)
Optional — add User Token Scopes to unlock get_unread_summary, get_mentions, mark_read:
channels:history,groups:history,im:history,mpim:historyusers.profile:read,search:read
Install to Workspace, copy:
xoxb-...— Bot User OAuth Token →SLACK_TOKENxoxp-...— User OAuth Token →SLACK_USER_TOKEN(optional)
Invite the bot to channels: /invite @your-bot.
2. Install in Claude Code
Setup A (user token only — recommended):
claude mcp add slack \
-e SLACK_USER_TOKEN=xoxp-your-user-token \
-- docker run --rm -i \
-e SLACK_USER_TOKEN \
velesnitski/slk-mcp -transport stdio
Or via config file (~/.claude.json):
{
"mcpServers": {
"slack": {
"command": "docker",
"args": ["run", "--rm", "-i",
"-e", "SLACK_USER_TOKEN",
"velesnitski/slk-mcp",
"-transport", "stdio"],
"env": {
"SLACK_USER_TOKEN": "xoxp-your-user-token"
}
}
}
}
Setup B (bot token): add -e SLACK_TOKEN=xoxb-... instead of (or alongside) the user token. Bot must be invited to each channel; unread/mentions/DM tools stay hidden unless a user token is also set.
3. Try it
- "Summarise my unread messages"
- "What happened in #general overnight?"
- "Morning recap"
- "Any mentions of me in the last 24 hours?"
- "What decisions were made this week across my channels?"
- "Search for messages about database migration"
- "What did alex say about the deployment?"
Tools
Without user token (bot-token only)
| Tool | Description |
|---|---|
list_channels | List channels the bot can see, ordered by member count |
get_channel_info | Topic, purpose, member count, created date |
get_channel_digest | Compact digest of one channel — or a DM, addressed as @handle or a bare U… user id; with_replies=true inlines thread discussions |
read_canvas | Read canvases in a channel/DM (tab + shared); date/match select per-meeting notes, list_only lists them |
get_multi_channel_digest | Digest across multiple channels |
get_morning_recap | Decisions + activity across channels |
search_messages | Workspace search (Slack syntax) |
find_decisions | Messages that look like decisions (keywords + reactions) |
get_thread | Full thread replies |
get_message | One message verbatim with metadata; reactors=true names who reacted, per emoji and as one roster |
get_user_messages | Recent messages from a specific user |
post_message | Post a message (disabled in SLACK_READ_ONLY) |
add_reaction | Add an emoji reaction (disabled in SLACK_READ_ONLY) |
delete_message | Delete a message by permalink or channel+ts — only ones this token posted (disabled in SLACK_READ_ONLY) |
set_status | Set/clear your custom status (text + emoji + auto-expiry) and optional presence; you-global by default (disabled in SLACK_READ_ONLY, needs user token) |
set_presence | Flip presence away/auto WITHOUT touching your status; you-global by default (disabled in SLACK_READ_ONLY, needs user token) |
set_dnd | Pause/resume notifications — Do Not Disturb snooze (actually silences, unlike a status); you-global by default (disabled in SLACK_READ_ONLY, needs user token + dnd:write) |
download_audio | Download audio attachments (voice messages) to local temp files for transcription |
view_image | Fetch image attachments (screenshots, photos, cards) and return them inline so the model can see them |
read_document | Read text attachments (HTML, Markdown, TXT, CSV, JSON, source) inline; HTML is flattened to plain text, keeping embedded JSON/Markdown payloads. PDFs are text-extracted, with pages that are only pictures named so they can be read visually; keep_file returns any attachment as a raw local file |
transcribe_audio | Voice message, audio/video clip, or recorded huddle → text via a local whisper.cpp install; degrades to download_audio behaviour when the toolchain is missing |
analyze_audio_tone | Vocal tone of a voice message — loudness range (LRA) + native pitch (f0 mean/variability) — to gauge calm vs agitated/shouting; needs only ffmpeg |
With user token (SLACK_USER_TOKEN)
| Tool | Description |
|---|---|
get_unread_summary | Unread messages across all joined channels, sorted by volume |
get_mentions | Messages that mention you; summary=true returns load stats (by sender / channel, DM split) instead of the list |
mark_read | Mark a channel read through a given timestamp |
edit_canvas | Edit a canvas as you: append/prepend, insert before/after, replace or delete a section found by its text, replace the whole canvas, rename; dry_run previews (disabled in SLACK_READ_ONLY, needs canvases:write) |
list_scheduled_messages | Your pending scheduled (queued-to-send) messages with send time + channel; you-global by default |
How-to: transcribe voice messages
Slack voice notes are audio/mp4 (.m4a) attachments, fetched with the
server's own token (the token never leaves the server process; requires
the files:read scope, or Slack answers with its sign-in page and
the tool reports a scope error).
A permalink to a thread's root also reaches a voice note posted as a reply: if the linked message carries no audio, the thread is scanned for the newest matching attachment — so you can copy the link on the parent message and still transcribe the reply.
Scopes per resolution mode: a permalink or file URL needs only
files:read; latest-mode (channel with no timestamp) also
needs im:history to read the DM, plus users:read and
im:write when the channel is an @handle. On a missing scope the
tool now names the workspace and the exact scope to add — so in a
multi-workspace setup you can tell which app to reinstall.
One call — transcribe_audio runs the whole pipeline under the
hood when whisper.cpp is
installed:
# one-time setup (same as below); then:
# transcribe_audio { "permalink": "https://<workspace>.slack.com/archives/D.../p...", "language": "ru" }
# → transcript text. Missing toolchain? The tool falls back to
# download_audio behaviour: local file path + a setup hint.
#
# No permalink and no ts? Point at the conversation and get its newest
# voice note (latest-mode) — ideal for empty-text memos search can't find:
# transcribe_audio { "channel": "@teammate", "from": "me", "language": "ru" }
# channel: a DM as @handle, or a #name / C·G·D id. from: @handle or "me".
Optional overrides: SLACK_FFMPEG_BIN, SLACK_WHISPER_BIN (defaults:
PATH lookup of ffmpeg / whisper-cli) and SLACK_WHISPER_MODEL
(default: ~/.cache/whisper/ggml-small.bin).
Manual — download_audio just fetches the file; pair it with any
local speech-to-text engine:
# one-time setup
brew install whisper-cpp ffmpeg
mkdir -p ~/.cache/whisper
curl -L -o ~/.cache/whisper/ggml-small.bin \
"https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-small.bin"
# 1. download the voice note (MCP call — returns a local temp path)
# download_audio { "permalink": "https://<workspace>.slack.com/archives/D.../p..." }
# 2. convert to 16 kHz mono WAV and transcribe (pick your language via -l)
ffmpeg -y -i /tmp/slk-audio-<id>-<name>.m4a -ar 16000 -ac 1 /tmp/voice.wav
whisper-cli -m ~/.cache/whisper/ggml-small.bin -l ru -np /tmp/voice.wav
Model sizes trade accuracy for speed: ggml-base.bin (142 MB, fast),
ggml-small.bin (466 MB, good multilingual balance), ggml-medium.bin
(1.5 GB), ggml-large-v3.bin (3 GB, best). For typical voice notes,
small is enough. An MCP client (e.g. Claude Code) can run the whole
chain from a single message permalink.
Environment variables
| Variable | Required | Description |
|---|---|---|
SLACK_TOKEN | one of | Bot User OAuth Token (xoxb-...). |
SLACK_USER_TOKEN | one of | User OAuth Token (xoxp-...). Required for unread/mentions. At least one of SLACK_TOKEN / SLACK_USER_TOKEN must be set. |
SLACK_WORKSPACE_NAME | No | Cosmetic label for the primary workspace, shown in merged digests (default: primary). |
SLACK_WORKSPACES | No | JSON array of additional workspaces. Each entry: {"name","bot_token","user_token","channels","priority_channels"} (all but at least one token optional). Tokens are workspace-scoped, so each extra Slack space needs its own token pair. Labels live in the JSON values — no per-workspace env keys. See Multiple workspaces. |
SLACK_CHANNELS | No | Default channels for digest/recap (comma-separated). If unset, tools auto-discover the channels you've joined. |
SLACK_PRIORITY_CHANNELS | No | Channels (names or IDs, comma-separated) that get_unread_summary always shows for its window, even when you have already read them — first in the output, marked ★, with · read when nothing in them is new to you. Window: priority_hours (default 24) or everything after the after cursor. Channels it cannot find are named in the output. Per extra workspace: priority_channels. ADR 112. |
SLACK_AUTODISCOVER_LIMIT | No | Cap on auto-discovered channel count when SLACK_CHANNELS is unset (default: 50) |
SLACK_READ_ONLY | No | true to disable post_message, add_reaction, delete_message, mark_read, edit_canvas |
SLACK_FFMPEG_BIN | No | ffmpeg binary for transcribe_audio (default: ffmpeg from PATH) |
SLACK_WHISPER_BIN | No | whisper.cpp binary for transcribe_audio (default: whisper-cli from PATH) |
SLACK_WHISPER_MODEL | No | ggml model file for transcribe_audio (default: ~/.cache/whisper/ggml-small.bin) |
SLACK_DIGEST_HOURS | No | Default digest lookback (default: 24) |
SLACK_MAX_MESSAGES | No | Cap on messages fetched per channel per call (default: 200) |
SLACK_SWEEP_TIMEOUT | No | Seconds get_unread_summary / get_mentions may spend fetching before returning what they have, naming what they skipped (default: 100; 0 = no limit). A workspace that did not finish keeps its delta cursor |
SLACK_SWEEP_EXCLUDE | No | Comma-separated workspace labels that get_unread_summary / get_mentions skip unless asked by name; the output names them and keeps their delta cursor |
SLACK_LOG_FILE | No | Also append logs to this file (created 0600, ~/ expanded, rotated to .1 past 10 MB at startup). Per-phase sweep timings land here, since MCP hosts don't keep a server's stderr |
SLACK_COMPACT | No | false to disable compact output (default: true) |
SLACK_LOG_LEVEL | No | debug, info, warn, error (default: info) |
DISABLED_TOOLS | No | Comma-separated list of tool names to hide |
Multiple workspaces
A single Slack token only sees the workspace it was minted in. To read more
than one workspace, keep your existing SLACK_TOKEN / SLACK_USER_TOKEN as
the primary and add the rest through SLACK_WORKSPACES, a JSON array:
SLACK_WORKSPACES=[
{ "name": "secondary", "user_token": "xoxp-...", "bot_token": "xoxb-..." }
]
nameis a label only — it appears as a[name]heading in merged output and is never used as a credential key. Workspace names live in the JSON values, so adding a workspace never introduces a new env var.- Each entry needs at least one token (
user_tokenfor unread/mentions).channelsis an optional comma-separated allow-list, same asSLACK_CHANNELS;priority_channelsis the per-workspaceSLACK_PRIORITY_CHANNELS.
With more than one workspace configured, get_unread_summary and
get_mentions merge every workspace automatically, each under its own
## [name] heading. Pass workspace: "<name>" to either tool to scope the
call to one workspace. Drill-in tools (get_channel_digest, post_message,
mark_read, …) operate on the primary workspace.
Docker
docker run -d --name slack-mcp \
-e SLACK_USER_TOKEN=xoxp-your-user-token \
-p 8001:8000 \
velesnitski/slk-mcp
Add -e SLACK_TOKEN=xoxb-... alongside if you also want a bot token (Setup B).
Connect Claude Code over the network:
{
"mcpServers": {
"slack": {
"type": "url",
"url": "http://localhost:8001/sse"
}
}
}
Architecture
slk-mcp/
├── main.go # Entry, flags, transport, graceful shutdown
├── internal/
│ ├── config/ # Env-driven configuration + validation
│ ├── logger/ # slog JSON logger (stderr)
│ ├── format/ # Compact LLM-friendly output
│ ├── slack/
│ │ ├── client.go # Composition root
│ │ ├── channels.go # ChannelService — list, resolve, info
│ │ ├── messages.go # MessageService — history, threads, post
│ │ ├── users.go # UserService — cached name resolution
│ │ ├── search.go # SearchService — workspace search
│ │ ├── unread.go # UnreadService — user-token-only
│ │ └── ratelimit/ # 429 retry with Retry-After
│ └── tools/ # MCP tool handlers (one file per domain)
└── Dockerfile # Multi-stage, non-root, Alpine
Services are composed on slack.Client; each tool handler depends only on what it needs. Rate-limiting wraps every API call. context.Context propagates from tool handlers to the Slack SDK.
Decision detection
find_decisions and get_morning_recap detect decisions by:
Keywords in text: "decided", "approved", "let's go with", "agreed", "confirmed", "moving forward", "final answer"
Reactions on messages: :white_check_mark:, :heavy_check_mark:, :eyes:, :thumbsup:
Security
- Tokens passed via environment variables — never hardcoded.
- Stdio transport has zero network exposure.
- Read-only mode available (
SLACK_READ_ONLY=true). - Bot only sees channels it's invited to.
- User token only needed for personal workflow features (unread/mentions) — bot token covers everything else.
- No message content is logged, only tool names, channel names, and timings.
Development
go build -o slk-mcp .
go test ./...
go vet ./...
A Makefile wraps the common flows: make build (version-stamped), make test,
and make install (build + sync the /mcp dialog label to slack v<version>).
The /mcp listing shows each server by its config key in ~/.claude.json
(read at session start), not the server-reported name — make sync-label
keeps that key current so the dialog never displays a stale version. After
make install, restart Claude Code (a /mcp reconnect isn't enough). ADR 024.
Beyond go test: make fuzz (FUZZTIME per target, default 30s) explores the
fuzz targets for every parser of untrusted input, and make mutate runs
mutation testing with gremlins and fails if any mutant survives. ADR 118.
Compatibility (1.0)
SemVer applies to the machine surface: tool names, argument names/types/defaults, call semantics, and environment-variable names. A breaking change to any of these bumps the major version.
The rendered output text is not contractual — digests, summaries,
headers, and section labels target an LLM and will keep changing for
density in minor/patch releases. Do not regex the human-readable body;
integrate against the typed arguments and the structured tokens the
tools emit (permalinks, the cursor: timestamp, issue IDs under
include_refs). See ADR 037.
License
MIT