Enabling Slack MCP in Codex and Claude Code (workaround)
Codex CLI and Claude Code both refuse to log into Slack's hosted MCP server because they only know how to do Dynamic Client Registration, and Slack doesn't support it. Here's the bearer-token workaround that actually works today, plus the GitHub issues to watch.
TL;DR — Codex CLI and Claude Code both try to authenticate against Slack's hosted MCP server (
https://mcp.slack.com/mcp) using Dynamic Client Registration (DCR). Slack's MCP server doesn't support DCR. The result is a hard wall:Dynamic client registration not supported. This post walks through the workaround that actually works today: register your own Slack app, complete the OAuth code exchange manually to mint a user token, then plug that token into Codex (or Claude Code) as a static bearer token viabearer_token_env_var.
The problem#
The latest MCP authorization spec lists three ways an MCP client can identify itself to an OAuth-protected server, in priority order:
- Pre-registered client information (the client already has
client_id/client_secret). - Client ID Metadata Documents (server advertises support).
- Dynamic Client Registration as a fallback.
Codex CLI and Claude Code both currently jump straight to option 3. That works for hobbyist MCP servers that opt into DCR, but it's a non-starter for enterprise servers that explicitly reject it — Slack, Salesforce, Okta-backed FastMCP servers, Google's Stitch, and others. Slack's docs say it directly: "Slack supports confidential OAuth for MCP clients" (read: pre-registered client_id + client_secret only).
Tracking issues#
Codex CLI (openai/codex)
- #13200 — codex mcp login fails for Slack official MCP with
Dynamic client registration not supported(open) - #19154 — codex mcp login appears to require dynamic client registration; cannot use pre-registered client identity (open)
Claude Code (anthropics/claude-code)
- #18009 — Slack plugin fails to authenticate, "does not support dynamic client registration" (open)
- #26675 — Support pre-configured OAuth client credentials without requiring DCR (open)
- #38102 — MCP OAuth: "does not support dynamic client registration" despite clientId configured (open)
- #51345 — Slack MCP OAuth fails with "Invalid client_id parameter" (open)
- #53253 — Slack MCP plugin OAuth fails: SDK tries dynamic client registration before using pre-registered clientId (open)
- #41664 — Stitch MCP fails with "does not support dynamic client registration" (open)
A community contributor on the Codex side has a working branch (SproutSeeds/codex@987ef9cc) that adds oauth_client_id and oauth_client_secret_env_var config options, but as of this writing it isn't merged. Until upstream support lands in either CLI, the workaround below is the path.
The workaround#
You're going to register your own Slack app, run the OAuth code-exchange flow yourself, get a user token (xoxp-…), and configure Codex to send it as a static bearer. End to end: ~5 minutes.
Agent prompt template#
If you're handing this off to an AI agent (Claude Code, Codex, etc.), the high-level instruction is:
Connect Slack MCP to my CLI. I need to: create a Slack app named
<app-name>in workspace<workspace>, configure user-token scopes for read+write, run an OAuth authorization in my browser, exchange the code for a user token viaslack.com/api/oauth.v2.access, enable MCP server access on the app, then register the server with--bearer-token-env-var SLACK_MCP_TOKEN. Persist the token in~/.zshrc. Walk me through each browser step; run allcurlandcodexcommands yourself.
The agent will need you for: clicking "Allow" in the browser, copying the code query param, and (for Claude Code: running claude mcp add instead of codex mcp add).
Step-by-step#
1. Create a Slack app. Open https://api.slack.com/apps?new_app=1 → From scratch → pick the workspace → Create App.
2. Add a redirect URL. Sidebar → OAuth & Permissions → Redirect URLs → add http://localhost:8080/callback → Save URLs. (No server needs to listen there. The browser will land on a "site can't be reached" page; you'll grab the code from the URL bar.)
3. Add User Token Scopes. Same page → Scopes → User Token Scopes (not Bot Token Scopes — Slack's MCP server requires user tokens). The full set the Slack MCP server can use is in the appendix manifest; a sensible starter:
chat:write
channels:read channels:history
groups:read groups:history
im:read im:history
mpim:read mpim:history
users:read users:read.email
search:read.public search:read.private
Adding scopes later requires re-authorizing, so over-scope rather than under-scope on the first pass.
4. Grab credentials. Sidebar → Basic Information → App Credentials. Copy the Client ID and Client Secret. The Signing Secret is unused for this flow.
5. Authorize. Open this in a browser (substitute your client_id and the same scope list, comma-separated):
https://slack.com/oauth/v2/authorize?client_id=<CLIENT_ID>&user_scope=chat:write,channels:read,channels:history,groups:read,groups:history,im:read,im:history,mpim:read,mpim:history,users:read,users:read.email,search:read.public,search:read.private&redirect_uri=http://localhost:8080/callback
Confirm the workspace dropdown is correct → Allow. The browser lands on http://localhost:8080/callback?code=…&state=. Copy everything between code= and &state=. The code expires in ~10 minutes; move fast.
6. Exchange the code for a user token.
curl -s -X POST https://slack.com/api/oauth.v2.access \ -d "client_id=<CLIENT_ID>" \ -d "client_secret=<CLIENT_SECRET>" \ -d "code=<CODE>" \ -d "redirect_uri=http://localhost:8080/callback"
The response includes authed_user.access_token starting with xoxp-…. That is your bearer.
7. Enable MCP server access on the app. This is the step that is not documented in the obvious place and burns most people. Visit:
https://api.slack.com/apps/<APP_ID>/app-assistant
Toggle MCP server access on and save. Without this, mcp.slack.com/mcp returns:
HTTP 400 — App is not enabled for Slack MCP server access.
8. Sanity-check the bearer token directly against the MCP endpoint.
curl -sS -i -X POST https://mcp.slack.com/mcp \ -H "Authorization: Bearer xoxp-…" \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"manual-test","version":"0.1"}}}'
You want HTTP 200 and a JSON-RPC body containing serverInfo: { name: "Slack MCP" }. If you get that, the bearer path works end to end.
9. Register the server with Codex CLI. If you previously did codex mcp add slack --url … and it left a half-configured entry, remove it first:
codex mcp remove slack codex mcp add slack --url https://mcp.slack.com/mcp --bearer-token-env-var SLACK_MCP_TOKEN
For Claude Code the equivalent is:
claude mcp remove slack claude mcp add --transport http slack https://mcp.slack.com/mcp --header "Authorization: Bearer ${SLACK_MCP_TOKEN}"
(Claude Code doesn't have a first-class --bearer-token-env-var flag at the time of writing; passing the header explicitly works.)
10. Persist the env var.
Plaintext in dotfile (simplest):
echo 'export SLACK_MCP_TOKEN="xoxp-…"' >> ~/.zshrc
macOS Keychain (safer):
security add-generic-password -s SLACK_MCP_TOKEN -a "$USER" -w "xoxp-…" echo 'export SLACK_MCP_TOKEN=$(security find-generic-password -s SLACK_MCP_TOKEN -a "$USER" -w 2>/dev/null)' >> ~/.zshrc
11. Restart your CLI from a fresh terminal. This step gets people. Codex / Claude Code resolve env vars once at startup, so a session that was already running when you exported the token won't see it. Open a new terminal tab (Cmd-T in the same terminal app — not via Spotlight/Raycast/Dock, which on macOS don't source ~/.zshrc), confirm echo ${SLACK_MCP_TOKEN:0:20} prints something, then launch the CLI.
12. Verify inside the session. In Codex run /mcp; in Claude Code run /mcp as well. You should see the Slack server listed with status Bearer token (Codex) or equivalent, plus 9 tools: slack_send_message, slack_schedule_message, slack_search_public, slack_search_public_and_private, slack_search_channels, slack_read_channel, slack_read_thread, slack_read_user_profile, slack_send_message_draft.
After it's working#
- Rotate the client secret. If it ever passed through a chat with an LLM (or any logging surface), regenerate it at
https://api.slack.com/apps/<APP_ID>/general→ App Credentials → Regenerate. The user token stays valid afterwards. - Token rotation off by default. Slack user tokens don't expire unless your app has token rotation enabled. If you turned it on, you'll need to handle refresh yourself or accept periodic re-auth.
- Scope changes require re-auth. Adding scopes later means revisiting Step 5 to mint a new token with the wider scope set.
- Watch the upstream issues. When the Codex / Claude Code SDKs ship pre-registered client config, you can drop the bearer hack and use proper OAuth with your existing app credentials.
Appendix: cleaned app manifest#
The manifest below is the working version after fixing four problems with a draft I started from:
user_optionallisted scopes already inuser. Optional means "user can deny while still authorizing" — putting the same scope in both arrays is contradictory. Removeduser_optionalentirely; move scopes there explicitly only if you want to mark them deniable.identifyis a legacy v1 scope. Slack OAuth v2 doesn't use it. Removed.profileis not a Slack scope name. It's an OIDC concept that got pasted in by mistake. Replaced withusers.profile:read.- Bot scopes need a bot user. Including
assistant:writeinbotimplicitly satisfies the validator complaint thatapp_homeandassistant_viewneed a bot user. Kept as-is.
{ "display_information": { "name": "Slack MCP" }, "features": { "app_home": { "home_tab_enabled": false, "messages_tab_enabled": true, "messages_tab_read_only_enabled": false }, "assistant_view": { "assistant_description": "Read and write messages in Slack via MCP", "suggested_prompts": [] } }, "oauth_config": { "redirect_urls": [ "http://localhost:8080/callback" ], "scopes": { "user": [ "chat:write", "channels:read", "channels:history", "groups:read", "groups:history", "im:read", "im:history", "mpim:read", "mpim:history", "users:read", "users:read.email", "users.profile:read", "search:read.public", "search:read.private", "search:read.im", "search:read.mpim", "search:read.files", "search:read.users", "files:read", "links:read", "lists:read", "reminders:read", "reminders:write", "canvases:read", "canvases:write" ], "bot": [ "assistant:write" ] }, "pkce_enabled": false }, "settings": { "org_deploy_enabled": false, "socket_mode_enabled": false, "token_rotation_enabled": false, "is_mcp_enabled": true } }
You can paste this into the App Manifest view (JSON tab) on api.slack.com to bring an existing app into this shape, or use it as the starting manifest when creating a new app.
Subscribe to updates
Get notified when I publish new posts. No spam, unsubscribe anytime.
Or subscribe via RSS