From bb9469d3e6cd7016989d42d4c277181f4d061eba Mon Sep 17 00:00:00 2001 From: Sanghoon Hong Date: Sat, 18 Oct 2025 22:09:39 +0900 Subject: [PATCH] Add bot token (xoxb) support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add support for bot tokens as an alternative authentication method - Conditionally register search tool only for non-bot tokens (bots cannot use search.messages API) - Update isOAuth definition to include both xoxp and xoxb tokens for cleaner implementation - Add bot token configuration to manifest-dxt.json for DXT extension - Document bot token limitations in README and authentication setup guide 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .env.dist | 11 +++++- README.md | 5 ++- docs/01-authentication-setup.md | 24 ++++++++++-- docs/03-configuration-and-usage.md | 27 +++++++++++-- manifest-dxt.json | 12 +++++- pkg/provider/api.go | 63 +++++++++++++++++++++++++++--- pkg/server/server.go | 8 +++- 7 files changed, 131 insertions(+), 19 deletions(-) diff --git a/.env.dist b/.env.dist index 4245400..1115ca9 100644 --- a/.env.dist +++ b/.env.dist @@ -1,3 +1,12 @@ +# User OAuth Token (starts with xoxp-) +SLACK_MCP_XOXP_TOKEN="" # Replace with your user OAuth token + +# Bot OAuth Token (starts with xoxb-) +SLACK_MCP_XOXB_TOKEN="" # Replace with your bot token + +# Session-based tokens (browser tokens) SLACK_MCP_XOXC_TOKEN="" # Replace with your actual token SLACK_MCP_XOXD_TOKEN="" # Replace with your actual token -SLACK_MCP_SSE_API_KEY="" # Replace with your actual API key + +# SSE API Key for authentication +SLACK_MCP_SSE_API_KEY="" # Replace with your actual API key \ No newline at end of file diff --git a/README.md b/README.md index c34405a..f9d620b 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,8 @@ Add a message to a public channel, private channel, or direct message (DM, or IM ### 4. conversations_search_messages Search messages in a public channel, private channel, or direct message (DM, or IM) conversation using filters. All filters are optional, if not provided then search_query is required. + +> **Note**: This tool is not available when using bot tokens (`xoxb-*`). Bot tokens cannot use the `search.messages` API. - **Parameters:** - `search_query` (string, optional): Search query to filter messages. Example: 'marketing report' or full URL of Slack message e.g. 'https://slack.com/archives/C1234567890/p1234567890123456', then the tool will return a single message matching given URL, herewith all other parameters will be ignored. - `filter_in_channel` (string, optional): Filter messages in a specific channel by its ID or name. Example: `C1234567890` or `#general`. If not provided, all channels will be searched. @@ -123,6 +125,7 @@ Fetches a CSV directory of all users in the workspace. | `SLACK_MCP_XOXC_TOKEN` | Yes* | `nil` | Slack browser token (`xoxc-...`) | | `SLACK_MCP_XOXD_TOKEN` | Yes* | `nil` | Slack browser cookie `d` (`xoxd-...`) | | `SLACK_MCP_XOXP_TOKEN` | Yes* | `nil` | User OAuth token (`xoxp-...`) — alternative to xoxc/xoxd | +| `SLACK_MCP_XOXB_TOKEN` | Yes* | `nil` | Bot token (`xoxb-...`) — alternative to xoxp/xoxc/xoxd. Bot has limited access (invited channels only, no search) | | `SLACK_MCP_PORT` | No | `13080` | Port for the MCP server to listen on | | `SLACK_MCP_HOST` | No | `127.0.0.1` | Host for the MCP server to listen on | | `SLACK_MCP_API_KEY` | No | `nil` | Bearer token for SSE and HTTP transports | @@ -139,7 +142,7 @@ Fetches a CSV directory of all users in the workspace. | `SLACK_MCP_CHANNELS_CACHE` | No | `.channels_cache_v2.json` | Path to the channels cache file. Used to cache Slack channel information to avoid repeated API calls on startup. | | `SLACK_MCP_LOG_LEVEL` | No | `info` | Log-level for stdout or stderr. Valid values are: `debug`, `info`, `warn`, `error`, `panic` and `fatal` | -*You need either `xoxp` **or** both `xoxc`/`xoxd` tokens for authentication. +*You need one of: `xoxp` (user), `xoxb` (bot), or both `xoxc`/`xoxd` tokens for authentication. ### Limitations matrix & Cache diff --git a/docs/01-authentication-setup.md b/docs/01-authentication-setup.md index 9fccb68..a93a232 100644 --- a/docs/01-authentication-setup.md +++ b/docs/01-authentication-setup.md @@ -2,7 +2,11 @@ Open up your Slack in your browser and login. -#### Lookup `SLACK_MCP_XOXC_TOKEN` +> **Note**: You only need one of the following: an `xoxp-*` User OAuth token, an `xoxb-*` Bot token, or both `xoxc-*` and `xoxd-*` session tokens. User/Bot tokens are more secure and do not require a browser session. If multiple are provided, priority is `xoxp` > `xoxb` > `xoxc/xoxd`. + +#### Option 1: Using `SLACK_MCP_XOXC_TOKEN`/`SLACK_MCP_XOXD_TOKEN` (Browser session) + +##### Lookup `SLACK_MCP_XOXC_TOKEN` - Open your browser's Developer Console. - In Firefox, under `Tools -> Browser Tools -> Web Developer tools` in the menu bar @@ -16,7 +20,7 @@ Open up your Slack in your browser and login. Token value is printed right after the executed command (it starts with `xoxc-`), save it somewhere for now. -#### Lookup `SLACK_MCP_XOXD_TOKEN` +##### Lookup `SLACK_MCP_XOXD_TOKEN` - Switch to "Application" tab and select "Cookies" in the left navigation pane. - Find the cookie with the name `d`. That's right, just the letter `d`. @@ -24,7 +28,7 @@ Token value is printed right after the executed command (it starts with - Press Ctrl+C or Cmd+C to copy it's value to clipboard. - Save it for later. -#### Alternative: Using `SLACK_MCP_XOXP_TOKEN` (User OAuth) +#### Option 2: Using `SLACK_MCP_XOXP_TOKEN` (User OAuth) Instead of using browser-based tokens (`xoxc`/`xoxd`), you can use a User OAuth token: @@ -47,6 +51,7 @@ Instead of using browser-based tokens (`xoxc`/`xoxd`), you can use a User OAuth 3. Install the app to your workspace 4. Copy the "User OAuth Token" (starts with `xoxp-`) +##### App manifest (preconfigured scopes) To create the app from a manifest with permissions preconfigured, use the following code snippet: ```json @@ -81,6 +86,17 @@ To create the app from a manifest with permissions preconfigured, use the follow } ``` -> **Note**: You only need **either** XOXP token **or** both XOXC/XOXD tokens. XOXP user tokens are more secure and don't require browser session extraction. +#### Option 3: Using `SLACK_MCP_XOXB_TOKEN` (Bot Token) + +You can also use a Bot token instead of a User token: + +1. Go to [api.slack.com/apps](https://api.slack.com/apps) and create a new app +2. Under "OAuth & Permissions", add Bot Token Scopes (same as User scopes above, except `search:read`) +3. Install the app to your workspace +4. Copy the "Bot User OAuth Token" (starts with `xoxb-`) +5. **Important**: Bot must be invited to channels for access + +> **Note**: Bot tokens cannot use `search.messages` API, so `conversations_search_messages` tool will not be available. + See next: [Installation](02-installation.md) diff --git a/docs/03-configuration-and-usage.md b/docs/03-configuration-and-usage.md index 260667f..4760b7f 100644 --- a/docs/03-configuration-and-usage.md +++ b/docs/03-configuration-and-usage.md @@ -10,8 +10,8 @@ For [Claude Desktop](https://claude.ai/download) users, you can use the DXT exte 2. Click on the `Extensions` tab. 3. Drag and drop the downloaded .dxt file to install it and click "Install". 5. Fill all required configuration fields - - Authentication method: `xoxc/xoxd` or `xoxp`. - - Value for `SLACK_MCP_XOXC_TOKEN` and `SLACK_MCP_XOXD_TOKEN` in case of `xoxc/xoxd` method, or `SLACK_MCP_XOXP_TOKEN` in case of `xoxp`. + - Authentication method: `xoxc/xoxd`, `xoxp`, or `xoxb`. + - Value for `SLACK_MCP_XOXC_TOKEN` and `SLACK_MCP_XOXD_TOKEN` in case of `xoxc/xoxd` method, `SLACK_MCP_XOXP_TOKEN` in case of `xoxp`, or `SLACK_MCP_XOXB_TOKEN` in case of `xoxb`. - You may also enable `Add Message Tool` to allow posting messages to channels. - You may also change User-Agent if needed if you have Enterprise Slack. 6. Enable MCP Server. @@ -27,6 +27,7 @@ Below are prepared configurations: - `npx` and `xoxc/xoxd` method: [![Install MCP Server](https://cursor.com/deeplink/mcp-install-light.svg)](cursor://anysphere.cursor-deeplink/mcp/install?name=slack-mcp-server&config=eyJjb21tYW5kIjogIm5weCAteSBzbGFjay1tY3Atc2VydmVyQGxhdGVzdCAtLXRyYW5zcG9ydCBzdGRpbyIsImVudiI6IHsiU0xBQ0tfTUNQX1hPWENfVE9LRU4iOiAieG94Yy0uLi4iLCAiU0xBQ0tfTUNQX1hPWERfVE9LRU4iOiAieG94ZC0uLi4ifSwiZGlzYWJsZWQiOiBmYWxzZSwiYXV0b0FwcHJvdmUiOiBbXX0%3D) - `npx` and `xoxp` method: [![Install MCP Server](https://cursor.com/deeplink/mcp-install-light.svg)](cursor://anysphere.cursor-deeplink/mcp/install?name=slack-mcp-server&config=eyJjb21tYW5kIjogIm5weCAteSBzbGFjay1tY3Atc2VydmVyQGxhdGVzdCAtLXRyYW5zcG9ydCBzdGRpbyIsImVudiI6IHsiU0xBQ0tfTUNQX1hPWFBfVE9LRU4iOiAieG94cC0uLi4ifSwiZGlzYWJsZWQiOiBmYWxzZSwiYXV0b0FwcHJvdmUiOiBbXX0%3D) + - `npx` and `xoxb` method: [![Install MCP Server](https://cursor.com/deeplink/mcp-install-light.svg)](cursor://anysphere.cursor-deeplink/mcp/install?name=slack-mcp-server&config=eyJjb21tYW5kIjogIm5weCAteSBzbGFjay1tY3Atc2VydmVyQGxhdGVzdCAtLXRyYW5zcG9ydCBzdGRpbyIsImVudiI6IHsiU0xBQ0tfTUNQX1hPWEJfVE9LRU4iOiAieG94Yi0uLi4ifSwiZGlzYWJsZWQiOiBmYWxzZSwiYXV0b0FwcHJvdmUiOiBbXX0%3D) > [!IMPORTANT] > Remember to replace tokens in the configuration with your own tokens, as they are just examples. @@ -60,7 +61,27 @@ Open your `claude_desktop_config.json` and add the mcp server to the list of `mc } ``` -**Option 2: Using XOXC/XOXD Tokens** +**Option 2: Using XOXB Token (Bot)** +``` json +{ + "mcpServers": { + "slack": { + "command": "npx", + "args": [ + "-y", + "slack-mcp-server@latest", + "--transport", + "stdio" + ], + "env": { + "SLACK_MCP_XOXB_TOKEN": "xoxb-..." + } + } + } +} +``` + +**Option 3: Using XOXC/XOXD Tokens** ``` json { "mcpServers": { diff --git a/manifest-dxt.json b/manifest-dxt.json index b92ae56..8830b1a 100644 --- a/manifest-dxt.json +++ b/manifest-dxt.json @@ -31,6 +31,7 @@ "SLACK_MCP_XOXC_TOKEN": "${user_config.xoxc_token}", "SLACK_MCP_XOXD_TOKEN": "${user_config.xoxd_token}", "SLACK_MCP_XOXP_TOKEN": "${user_config.xoxp_token}", + "SLACK_MCP_XOXB_TOKEN": "${user_config.xoxb_token}", "SLACK_MCP_USER_AGENT": "${user_config.user_agent}", "SLACK_MCP_ADD_MESSAGE_TOOL": "${user_config.add_message_tool}", "SLACK_MCP_USERS_CACHE": "${HOME}/.users_cache.json", @@ -51,8 +52,8 @@ "user_config": { "auth_method": { "type": "string", - "title": "Authentication Method: xoxc/xoxd or xoxp", - "description": "Select the authentication method for the Slack MCP Server. You can choose between 'xoxc/xoxd' (browser based) or 'xoxp' (OAuth2).", + "title": "Authentication Method: xoxc/xoxd, xoxp, or xoxb", + "description": "Select the authentication method for the Slack MCP Server. You can choose between 'xoxc/xoxd' (browser based), 'xoxp' (User OAuth), or 'xoxb' (Bot OAuth).", "default": "", "required": true }, @@ -77,6 +78,13 @@ "sensitive": true, "required": false }, + "xoxb_token": { + "type": "string", + "title": "Value for xoxb token (Bot)", + "description": "Bot OAuth token for channel-specific automation. Bot must be invited to channels for access. Note: Search functionality is not available with bot tokens.", + "sensitive": true, + "required": false + }, "user_agent": { "type": "string", "title": "Value for user agent", diff --git a/pkg/provider/api.go b/pkg/provider/api.go index d45dc6e..f645718 100644 --- a/pkg/provider/api.go +++ b/pkg/provider/api.go @@ -79,6 +79,7 @@ type MCPSlackClient struct { isEnterprise bool isOAuth bool + isBotToken bool teamEndpoint string } @@ -135,6 +136,13 @@ func NewMCPSlackClient(authProvider auth.Provider, logger *zap.Logger) (*MCPSlac } isEnterprise := authResp.EnterpriseID != "" + token := authProvider.SlackToken() + + // Token type detection + // isOAuth: Official OAuth tokens (xoxp or xoxb) - uses Standard API + // isBotToken: Bot token - determines feature availability (e.g., search) + isOAuth := strings.HasPrefix(token, "xoxp-") || strings.HasPrefix(token, "xoxb-") + isBotToken := strings.HasPrefix(token, "xoxb-") return &MCPSlackClient{ slackClient: slackClient, @@ -142,7 +150,8 @@ func NewMCPSlackClient(authProvider auth.Provider, logger *zap.Logger) (*MCPSlac authResponse: authResponse, authProvider: authProvider, isEnterprise: isEnterprise, - isOAuth: strings.HasPrefix(authProvider.SlackToken(), "xoxp-"), + isOAuth: isOAuth, + isBotToken: isBotToken, teamEndpoint: authResp.URL, }, nil } @@ -268,6 +277,10 @@ func (c *MCPSlackClient) AuthResponse() *slack.AuthTestResponse { return c.authResponse } +func (c *MCPSlackClient) IsBotToken() bool { + return c.isBotToken +} + func (c *MCPSlackClient) Raw() struct { Slack *slack.Client Edge *edge.Client @@ -287,8 +300,23 @@ func New(transport string, logger *zap.Logger) *ApiProvider { err error ) - // Check for XOXP token first (User OAuth) + // Read all environment variables xoxpToken := os.Getenv("SLACK_MCP_XOXP_TOKEN") + xoxbToken := os.Getenv("SLACK_MCP_XOXB_TOKEN") + xoxcToken := os.Getenv("SLACK_MCP_XOXC_TOKEN") + xoxdToken := os.Getenv("SLACK_MCP_XOXD_TOKEN") + + // Warn if both user and bot tokens are set + if xoxpToken != "" && xoxbToken != "" { + logger.Warn( + "Both SLACK_MCP_XOXP_TOKEN and SLACK_MCP_XOXB_TOKEN are set. "+ + "Using User token (xoxp) for full features. "+ + "Bot token will be ignored.", + zap.String("context", "console"), + ) + } + + // Priority 1: XOXP token (User OAuth) if xoxpToken != "" { authProvider, err = auth.NewValueAuth(xoxpToken, "") if err != nil { @@ -298,12 +326,24 @@ func New(transport string, logger *zap.Logger) *ApiProvider { return newWithXOXP(transport, authProvider, logger) } - // Fall back to XOXC/XOXD tokens (session-based) - xoxcToken := os.Getenv("SLACK_MCP_XOXC_TOKEN") - xoxdToken := os.Getenv("SLACK_MCP_XOXD_TOKEN") + // Priority 2: XOXB token (Bot) + if xoxbToken != "" { + authProvider, err = auth.NewValueAuth(xoxbToken, "") + if err != nil { + logger.Fatal("Failed to create auth provider with XOXB token", zap.Error(err)) + } + logger.Info("Using Bot token authentication", + zap.String("context", "console"), + zap.String("token_type", "xoxb"), + ) + + return newWithXOXB(transport, authProvider, logger) + } + + // Priority 3: XOXC/XOXD tokens (session-based) if xoxcToken == "" || xoxdToken == "" { - logger.Fatal("Authentication required: Either SLACK_MCP_XOXP_TOKEN (User OAuth) or both SLACK_MCP_XOXC_TOKEN and SLACK_MCP_XOXD_TOKEN (session-based) environment variables must be provided") + logger.Fatal("Authentication required: Either SLACK_MCP_XOXP_TOKEN, SLACK_MCP_XOXB_TOKEN, or both SLACK_MCP_XOXC_TOKEN and SLACK_MCP_XOXD_TOKEN must be provided") } authProvider, err = auth.NewValueAuth(xoxcToken, xoxdToken) @@ -356,6 +396,12 @@ func newWithXOXP(transport string, authProvider auth.ValueAuth, logger *zap.Logg } } +func newWithXOXB(transport string, authProvider auth.ValueAuth, logger *zap.Logger) *ApiProvider { + // Bot tokens do not support demo mode, but otherwise share the same + // initialization logic as user OAuth tokens. + return newWithXOXP(transport, authProvider, logger) +} + func newWithXOXC(transport string, authProvider auth.ValueAuth, logger *zap.Logger) *ApiProvider { var ( client *MCPSlackClient @@ -676,6 +722,11 @@ func (ap *ApiProvider) Slack() SlackAPI { return ap.client } +func (ap *ApiProvider) IsBotToken() bool { + client, ok := ap.client.(*MCPSlackClient) + return ok && client != nil && client.IsBotToken() +} + func mapChannel( id, name, nameNormalized, topic, purpose, user string, members []string, diff --git a/pkg/server/server.go b/pkg/server/server.go index 95e1c30..5451c49 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -93,7 +93,7 @@ func NewMCPServer(provider *provider.ApiProvider, logger *zap.Logger) *MCPServer ), ), conversationsHandler.ConversationsAddMessageHandler) - s.AddTool(mcp.NewTool("conversations_search_messages", + conversationsSearchTool := mcp.NewTool("conversations_search_messages", mcp.WithDescription("Search messages in a public channel, private channel, or direct message (DM, or IM) conversation using filters. All filters are optional, if not provided then search_query is required."), mcp.WithString("search_query", mcp.Description("Search query to filter messages. Example: 'marketing report' or full URL of Slack message e.g. 'https://slack.com/archives/C1234567890/p1234567890123456', then the tool will return a single message matching given URL, herewith all other parameters will be ignored."), @@ -133,7 +133,11 @@ func NewMCPServer(provider *provider.ApiProvider, logger *zap.Logger) *MCPServer mcp.DefaultNumber(20), mcp.Description("The maximum number of items to return. Must be an integer between 1 and 100."), ), - ), conversationsHandler.ConversationsSearchHandler) + ) + // Only register search tool for non-bot tokens (bot tokens cannot use search.messages API) + if !provider.IsBotToken() { + s.AddTool(conversationsSearchTool, conversationsHandler.ConversationsSearchHandler) + } channelsHandler := handler.NewChannelsHandler(provider, logger)