slack-stdio-mcp
v1.3.0
Published
Agent-agnostic MCP stdio bridge to https://mcp.slack.com/mcp (user OAuth, PKCE, token refresh, tool proxy, local file download overlay)
Maintainers
Readme
slack-stdio-mcp
Local MCP stdio bridge to Slack’s hosted MCP server
(https://mcp.slack.com/mcp).
Proxies the official tool catalog (slack_send_message, search, history,
canvas, …) after user OAuth (PKCE) and keeps tokens fresh. It does not
reimplement hosted Slack tools. A small local overlay adds what the hosted
server does not: download a file to disk, and list local vs remote tool names.
| Approach | Typical result |
|----------|----------------|
| Host → HTTP mcp.slack.com with built-in OAuth | Often stuck authenticating |
| Claude Code Slack plugin | Works (partner app + host OAuth) |
| This bridge (stdio + local OAuth/refresh) | Works for Grok, Cursor, Open Code, Codex, Claude, … |
Agent ──stdio MCP──► slack-stdio-mcp ──Bearer──► mcp.slack.com
│
├─ valid access token → reuse
├─ expired + refresh_token → silent refresh
├─ no token → browser OAuth (PKCE)
└─ overlay: download, catalog, edit/delete, unreact, scheduledRequirements
- Node.js ≥ 20 (Windows, macOS, Linux)
- Default OAuth app: Claude’s partner Slack app (no app setup required)
- Client ID:
1601185624273.8899143856786 - Redirect:
http://localhost:3118/callback
- Client ID:
- Own app is optional — see Own Slack app
Install
npx -y slack-stdio-mcpFirst run may open a browser for Slack Allow. Later runs reuse or refresh tokens under the platform credentials directory (see Auth).
| Alternative | Command |
|-------------|---------|
| Latest git main | npx -y github:epdlr/slack-stdio-mcp |
| From clone | git clone … && npm install && npm start |
Configure a host
Prefer npx so you never hardcode a machine path. Put knobs in args
(CLI flags beat env; see Configuration).
Set startup_timeout_sec (or equivalent) ≥ 180 so the first OAuth Allow
is not killed by the host.
Grok Build (~/.grok/config.toml)
[mcp_servers.slack-stdio]
command = "npx"
args = ["-y", "slack-stdio-mcp"]
enabled = true
startup_timeout_sec = 180Own Slack app:
[mcp_servers.slack-stdio]
command = "npx"
args = [
"-y", "slack-stdio-mcp",
"--client-id", "YOUR.CLIENT.ID",
"--oauth-host", "127.0.0.1",
"--oauth-path", "/oauth/callback",
]
enabled = true
startup_timeout_sec = 180Claude Code / Cursor / similar (JSON)
{
"mcpServers": {
"slack-stdio": {
"command": "npx",
"args": ["-y", "slack-stdio-mcp"]
}
}
}Add the same optional flags as in the Grok example when using your own app.
Local clone
{
"mcpServers": {
"slack-stdio": {
"command": "node",
"args": ["/absolute/path/to/slack-stdio-mcp/src/server.mjs"]
}
}
}Auth
On start, if there is no usable token for the active client_id, the bridge
opens a browser (PKCE). Credentials are stored per client_id:
| OS | Default root |
|----|----------------|
| macOS / Linux | ~/.config/slack-stdio-mcp ($XDG_CONFIG_HOME honored) |
| Windows | %APPDATA%\slack-stdio-mcp |
Path: …/by-client/<client_id>.json. Override with --creds-dir /
SLACK_STDIO_CREDS_DIR. Unix modes 0600/0700 when supported.
| Action | How |
|--------|-----|
| OAuth only (no MCP) | npm run auth (from a clone) |
| Skip browser (CI) | --skip-oauth / SLACK_SKIP_OAUTH=1 |
| Inject token | --token / SLACK_MCP_TOKEN |
Token lifecycle
- Load credentials for the current
client_id - Reuse access token if valid (5‑minute skew before
expires_at) - Else refresh via
oauth.v2.access(grant_type=refresh_token) - On refresh failure: clear that app’s file → OAuth (or fail if skip-oauth)
Mid-session session loss
If a Slack tool fails with an auth error (isError: true or thrown error):
- Silent force-refresh + reconnect + one retry
- Else open browser and return
SLACK_REAUTH_REQUIREDplus the authorize URL in the tool result (clickable in chat) - After Allow, the bridge reconnects in the background — retry the tool
Successful tool payloads are never scanned for auth keywords. Settled re-auth flows are not reused; the next start gets a fresh URL.
| Local tool | Purpose |
|------------|---------|
| slack_stdio_reauth | Start re-auth; optional wait: true until Allow |
| slack_stdio_session_status | Pending re-auth + authorize URL if any |
| slack_stdio_download_file | Write a Slack file_id to disk (hosted slack_read_file is often metadata-only for video). Max 50 MB. files:read |
| slack_stdio_catalog | JSON of local overlay names vs the current mcp.slack.com catalog |
| slack_stdio_update_message | Edit a message the user posted (chat.update). Hosted MCP can send only |
| slack_stdio_delete_message | Delete a message the user posted (chat.delete) |
| slack_stdio_remove_reaction | Remove a reaction the user added (reactions.remove). Hosted catalog has add/get |
| slack_stdio_scheduled_messages | action=list or action=cancel for scheduled messages. Hosted slack_schedule_message cannot cancel |
Startup OAuth waits up to SLACK_OAUTH_TIMEOUT_MS (default 180000). On
timeout the process exits 1 (host must restart). Keep host startup timeout
above that value. The authorize URL is always printed on stderr.
Configuration
Precedence: CLI flags > environment > built-in defaults.
| CLI flag | Env | Purpose |
|----------|-----|---------|
| --client-id <id> | SLACK_CLIENT_ID | OAuth app id (default: Claude partner) |
| --client-secret <s> | SLACK_CLIENT_SECRET | Confidential apps only |
| --oauth-host <host> | SLACK_OAUTH_HOST | Redirect host (localhost) |
| --oauth-path <path> | SLACK_OAUTH_PATH | Redirect path (/callback) |
| --oauth-port <port> | SLACK_OAUTH_PORT | Loopback port (3118) |
| --mcp-url <url> | SLACK_MCP_URL | MCP endpoint |
| --profile <name> | SLACK_STDIO_PROFILE | Named store: ~/.slack-stdio-mcp/profiles/<name> (share across repos) |
| --creds-dir <dir> | SLACK_STDIO_CREDS_DIR | Absolute credentials root (wins over --profile) |
| --skip-oauth | SLACK_SKIP_OAUTH=1 | Never open browser |
| --token / --mcp-token | SLACK_MCP_TOKEN | Inject Bearer (tests/CI) |
| -h / --help | — | Help on stderr |
Env only: SLACK_OAUTH_TIMEOUT_MS, SLACK_ALLOW_LEGACY_TOKEN=1 (flat legacy
JSON without client_id).
npx -y slack-stdio-mcp -- --profile user_cl
npx -y slack-stdio-mcp -- --client-id 123.456 --oauth-path /oauth/callback
npx -y slack-stdio-mcp -- --skip-oauth --creds-dir /tmp/empty-credsProfiles: the same --profile name in every host/repo reuses
~/.slack-stdio-mcp/profiles/<name>/… (no absolute paths in config). Grok does
not inject the MCP server key into the process — put the profile string in
args yourself (convention: match your team/workspace name).
Platforms
| | Windows | macOS / Linux |
|--|---------|----------------|
| Credentials | %APPDATA%\slack-stdio-mcp | ~/.config/… or $XDG_CONFIG_HOME |
| Open browser | cmd /c start "" "<url>" (URL quoted for &) | open / xdg-open |
| File modes | omitted (profile ACL) | 0600 / 0700 |
CI: npm test on Ubuntu, Windows, macOS (Node 20 + 22). If the browser cannot
open, paste the authorize URL from stderr.
Own Slack app (optional)
Only if you are not using the default Claude partner app.
- Slack app → OAuth & Permissions → Redirect URLs must match your
--oauth-*/SLACK_OAUTH_*(e.g.http://localhost:3118/callback) - PKCE Opt In (recommended without
client_secret) - Enable MCP under App Assistant / Agents & AI Apps
(else: App is not enabled for Slack MCP server access) - User Token Scopes must match
USER_SCOPESinsrc/oauth-flow.mjs(source of truth; CI checks the README list below)
| Scope | Used for |
|-------|----------|
| search:read.public | Search public channels |
| search:read.private | Search private channels |
| search:read.mpim | Search multi-person DMs |
| search:read.im | Search 1:1 DMs |
| search:read.files | Search files |
| search:read.users | Search users |
| chat:write | Send messages |
| channels:history | Public channel history |
| groups:history | Private channel history |
| mpim:history | Multi-person DM history |
| im:history | 1:1 DM history |
| canvases:read / canvases:write | Canvases |
| users:read / users:read.email | Profiles |
| reactions:write / reactions:read | Reactions |
| emoji:read | Custom emoji |
| files:read | Files |
| channels:write / groups:write / im:write / mpim:write | Open/manage conversations |
| channels:read / groups:read / mpim:read | List/metadata |
Copy-paste (comma-separated; authorize uses space-separated scope, not
user_scope):
search:read.public,search:read.private,search:read.mpim,search:read.im,search:read.files,search:read.users,chat:write,channels:history,groups:history,mpim:history,im:history,canvases:read,canvases:write,users:read,users:read.email,reactions:write,reactions:read,emoji:read,files:read,channels:write,groups:write,im:write,mpim:write,channels:read,groups:read,mpim:readThese are user scopes (xoxp / xoxe.xoxp), not bot scopes. A subset is
fine if you only need some tools. Set SLACK_CLIENT_SECRET only if Slack
rejects public PKCE exchange.
npx -y slack-stdio-mcp -- \
--client-id your.client.id \
--oauth-host 127.0.0.1 \
--oauth-path /oauth/callbackScripts
| Script | Command |
|--------|---------|
| Start bridge | npm start |
| OAuth only | npm run auth |
| Tests | npm test |
| Syntax + English gate | npm run check |
Security
See SECURITY.md.
- Never commit credentials,
.env, or token dumps - Tokens act as the authorizing user — revoke the app in Slack when done
- stdout = MCP JSON-RPC only; human logs go to stderr
Contributing
CONTRIBUTING.md · CHANGELOG.md · docs/ARCHITECTURE.md
