mcp-slack
An MCP server for reading Slack through your own browser session, forworkspaces where installing a Slack app isn't an option. Ten tools: sixprimitives, three multi-call operations, one write that is off by default.
It authenticates as you, not as a bot. Everything you can see, it can read;anything it posts is indistinguishable from a message you typed. Browser-sessionauth is not an officially supported Slack integration path, so check yourworkspace's policies first.
The tools take explicit ranges and neutral defaults — no assumed reportingcadence, channel naming scheme, or truncation limit — so workflows can be builton top rather than baked in.
Install
Copy the d cookie from your browser (developer tools → Application → Cookies →https://app.slack.com) into ~/.slack-tokens.yml:
slack:
- name: myworkspace.slack.com
token: xoxd-your-cookie-value-here
xoxc: null # auto-populated on first run
The short-lived xoxc API token is derived automatically and written back, solater runs skip that step. Both values are session credentials — chmod 600 thefile, and expect to recopy the cookie whenever your browser session ends.
Then install the server and register it:
uv tool install --editable .
{
"mcpServers": {
"slack": {
"command": "/Users/you/.local/bin/mcp-slack",
"args": [],
"lifecycle": "lazy"
}
}
}
To skip installing, point at the repo-root shim instead — it carries a PEP 723header, so uv resolves dependencies on the fly:"command": "uv", "args": ["run", "/path/to/mcp-slack/server.py"].
Tools
| Tool | Purpose |
|---|---|
slack_search |
Native Slack search syntax (from:@user, in:#channel, after:, has:link) |
slack_channel_history |
Messages from one channel over a time window |
slack_thread |
Every reply in a thread |
slack_dm_history |
DM history with one person |
slack_user |
Resolve a username or user ID to a profile |
slack_list_channels |
Channel discovery by glob and member count (expensive) |
Three tools stitch many calls into one result. They exist because theirdeduplication isn't reproducible from outside: a message found by search, bychannel history, and by thread expansion is the same message, and only theserver sees all three passes.
| Tool | Purpose |
|---|---|
slack_user_activity |
Everything one person said or received in a range, with optional surrounding context and thread expansion, grouped by channel |
slack_channels_history |
History for many channels at once, by list or glob, with replies nested under their parents |
slack_profiles |
Batch profiles with custom fields resolved to labels |
Time ranges accept YYYY-MM-DD, an epoch, or a relative offset like -7d.
slack_post_message posts as you, and refuses unlessSLACK_MCP_ALLOW_WRITE=1 is set in the server's environment:
"env": { "SLACK_MCP_ALLOW_WRITE": "1" }
Behavior worth knowing
A channel with no messages can't be resolved by name. Names resolve via
search.messages, becauseconversations.listis throttled to the point ofuselessness on Enterprise Grid — measured at 11m48s of consecutive 429backoffs without reaching the target channel. Pass a channel ID (C…) forempty or archived channels, and prefer a channel name overslack_list_channels, which still enumerates and may returnrate_limited.Glob discovery only sees channels you've joined. It uses
users.conversationsfor the same throttling reason.Failures come back as data, not exceptions:
{"error": "not_found", ...}.Codes arenot_found,rate_limited,auth_failed, andwrite_disabled.An expired cookie shows up asauth_failed.Rate-limit waits are bounded. A cumulative sleep budget (45s, reset eachtool call) means a call returns
rate_limitedrather than hanging. Librarycallers who don't mind waiting can raise it:SlackClient(ws, wait_budget=600).Messages are projected, not passed through. Raw Slack records run toseveral KB each; tools return
ts,time,user,user_name,text, andpermalink, plus thread fields when meaningful. Mentions and links arerewritten to readable text, and permalinks are built locally, so citing amessage costs no extra call.Lookups are cached, message content is not. One JSON file per workspace,with a timestamp per entry:
Cached TTL DM channel ID never assigned once per pair of users user name → ID 30d only a handle change invalidates it user ID → name 7d display names change occasionally channel name → ID 7d renames are rare but real team profile schema 30d effectively static channel member counts 24h drifts slowly, only gates a filter failed lookups 1h stops a typo being re-searched in a loop A negative is only recorded after a search completes and matches nothing, soa rate limit or transport error is never cached as "does not exist".
Library use
The multi-call operations are plain functions in slack_mcp/aggregate.py(user_activity, channels_history, profiles) taking an explicitSlackClient. Import them directly rather than speaking MCP to a subprocess.
License
MIT