Ian Hsiao | Blog

Enabling Slack MCP in Codex and Claude Code (workaround)

April 30, 2026·

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 via bearer_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:

  1. Pre-registered client information (the client already has client_id / client_secret).
  2. Client ID Metadata Documents (server advertises support).
  3. 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)

Claude Code (anthropics/claude-code)

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 via slack.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 all curl and codex commands 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=1From scratch → pick the workspace → Create App.

2. Add a redirect URL. Sidebar → OAuth & PermissionsRedirect URLs → add http://localhost:8080/callbackSave 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 → ScopesUser 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 InformationApp 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>/generalApp CredentialsRegenerate. 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:

  1. user_optional listed scopes already in user. Optional means "user can deny while still authorizing" — putting the same scope in both arrays is contradictory. Removed user_optional entirely; move scopes there explicitly only if you want to mark them deniable.
  2. identify is a legacy v1 scope. Slack OAuth v2 doesn't use it. Removed.
  3. profile is not a Slack scope name. It's an OIDC concept that got pasted in by mistake. Replaced with users.profile:read.
  4. Bot scopes need a bot user. Including assistant:write in bot implicitly satisfies the validator complaint that app_home and assistant_view need 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

Loading comments…