An MCP server for reading Slack through your own browser session, for workspaces where installing a Slack app isn't an option. Ten tools: six primitives, 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-session auth is not an officially supported Slack integration path, so check your workspace's policies first.
The tools take explicit ranges and neutral defaults — no assumed reporting cadence, channel naming scheme, or truncation limit — so workflows can be built on top rather than baked in.
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 runThe short-lived xoxc API token is derived automatically and written back, so
later runs skip that step. Both values are session credentials — chmod 600 the
file, 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 723
header, so uv resolves dependencies on the fly:
"command": "uv", "args": ["run", "/path/to/mcp-slack/server.py"].
| 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 their deduplication isn't reproducible from outside: a message found by search, by channel history, and by thread expansion is the same message, and only the server 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 unless
SLACK_MCP_ALLOW_WRITE=1 is set in the server's environment:
"env": { "SLACK_MCP_ALLOW_WRITE": "1" }-
A channel with no messages can't be resolved by name. Names resolve via
search.messages, becauseconversations.listis throttled to the point of uselessness on Enterprise Grid — measured at 11m48s of consecutive 429 backoffs without reaching the target channel. Pass a channel ID (C…) for empty 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 each tool call) means a call returns
rate_limitedrather than hanging. Library callers who don't mind waiting can raise it:SlackClient(ws, wait_budget=600). -
Messages are projected, not passed through. Raw Slack records run to several KB each; tools return
ts,time,user,user_name,text, andpermalink, plus thread fields when meaningful. Mentions and links are rewritten to readable text, and permalinks are built locally, so citing a message 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, so a rate limit or transport error is never cached as "does not exist".
The multi-call operations are plain functions in slack_mcp/aggregate.py
(user_activity, channels_history, profiles) taking an explicit
SlackClient. Import them directly rather than speaking MCP to a subprocess.
MIT