Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
a5c9f3c
feat: Add OAuth 2.0 multi-user support
wentaoyang-moloco Nov 5, 2025
c0772b1
test: Skip environment-dependent integration test
wentaoyang-moloco Nov 5, 2025
5bee63d
feat: Disable bot posting - users always post as themselves
wentaoyang-moloco Nov 6, 2025
b5e630e
docs: Add OAuth quick-start guide to README
wentaoyang-moloco Jan 8, 2026
3f5a224
Merge upstream master - resolve conflicts
wentaoyang-moloco Jan 8, 2026
2f7b9a9
chore: Remove internal reference from test comment
wentaoyang-moloco Jan 8, 2026
664c02c
feat: GovSlack compatibility and OAuth improvements
aron-muon Jan 28, 2026
831513f
Unskip integration test for conversations
aron-muon Jan 28, 2026
147bc44
Merge branch 'koro-master' into aron/oauth-improvements
aron-muon Jan 28, 2026
e13c9f5
fix: returning 401 response when token is not included in oauth mode
aron-muon Jan 28, 2026
869a859
fix: regex stripping pound character
aron-muon Jan 28, 2026
521b1db
feat: added health check to sse, http modes, as well as in oauth
aron-muon Jan 28, 2026
d9aec36
fix: validation errors and bug fixes
aron-muon Jan 28, 2026
26298d1
feat: add reaction tool
xav-ie Dec 17, 2025
01eb7ae
feat: add reactions_remove tool
Flare576 Jan 29, 2026
33ea855
refactor: rename parseParamsToolAddReaction to parseParamsToolReaction
Flare576 Jan 29, 2026
3ebf126
Add destructive hints to reaction tools, use dedicated SLACK_MCP_REAC…
Flare576 Jan 29, 2026
9c53cda
feat: add BotName, FileCount, HasMedia fields to message output
Flare576 Jan 29, 2026
b763519
feat: add files_get tool for downloading file content
Flare576 Jan 29, 2026
9990f2d
feat: add FileIDs field to message output
Flare576 Jan 29, 2026
4f59f9a
refactor: rename files_get to attachment_get_data for Slack terminolo…
Flare576 Jan 29, 2026
92aecd4
Merge branch 'master' into aron/oauth-improvements
aron-muon Jan 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
.idea
.env
oauth.env
.users_cache.json
.channels_cache.json
.channels_cache_v2.json
Expand All @@ -13,3 +14,4 @@
/npm/slack-mcp-server/LICENSE
/npm/slack-mcp-server/README.md
docker-compose.release.yml
slack-mcp-server
56 changes: 52 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,11 +122,54 @@ Fetches a CSV directory of all users in the workspace.
- `userName`: Slack username (e.g., `john`)
- `realName`: User’s real name (e.g., `John Doe`)

## Setup Guide

- [Authentication Setup](docs/01-authentication-setup.md)
## Quick Start

### OAuth Mode (Recommended for Multi-User)

**Best for**: Teams, production deployments, multiple users

1. **Deploy the server** (Cloud Run, Docker, etc.)
2. **Create a Slack app** at https://api.slack.com/apps
3. **Add user scopes**: `channels:history`, `channels:read`, `groups:history`, `groups:read`, `im:history`, `im:read`, `im:write`, `mpim:history`, `mpim:read`, `mpim:write`, `users:read`, `chat:write`, `search:read`
4. **Set redirect URI**: `https://your-server-url/oauth/callback`
5. **Configure environment variables**:
```bash
SLACK_MCP_OAUTH_ENABLED=true
SLACK_MCP_OAUTH_CLIENT_ID=your_client_id
SLACK_MCP_OAUTH_CLIENT_SECRET=your_client_secret
SLACK_MCP_OAUTH_REDIRECT_URI=https://your-server-url/oauth/callback
```
6. **Users authenticate**: Visit `/oauth/authorize` → Get personal token
7. **Use in MCP client**:
```json
{
"slack": {
"command": "npx",
"args": ["-y", "mcp-remote", "https://your-server-url/sse", "--header", "Authorization: Bearer xoxp-user-token"]
}
}
```

**Benefits**:
- ✅ One-time OAuth flow (no token expiration)
- ✅ No manual browser cookie extraction
- ✅ Each user acts as themselves
- ✅ Secure multi-user support

### Legacy Mode (Single User)

**Best for**: Personal use, quick testing

See [Authentication Setup](docs/01-authentication-setup.md) for extracting browser tokens.

---

## Full Setup Guides

- [Authentication Setup](docs/01-authentication-setup.md) - Legacy mode with browser tokens
- [Installation](docs/02-installation.md)
- [Configuration and Usage](docs/03-configuration-and-usage.md)
- [OAuth Multi-User Setup](docs/04-oauth-setup.md) - Complete OAuth guide with deployment instructions

### Environment Variables (Quick Reference)

Expand All @@ -136,6 +179,10 @@ Fetches a CSV directory of all users in the workspace.
| `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_OAUTH_ENABLED` | No | `false` | Enable OAuth 2.0 mode for multi-user support (requires OAuth credentials below) |
| `SLACK_MCP_OAUTH_CLIENT_ID` | Yes** | `nil` | Slack OAuth app Client ID (required when OAuth enabled) |
| `SLACK_MCP_OAUTH_CLIENT_SECRET` | Yes** | `nil` | Slack OAuth app Client Secret (required when OAuth enabled) |
| `SLACK_MCP_OAUTH_REDIRECT_URI` | Yes** | `nil` | OAuth callback URL (required when OAuth enabled, must use HTTPS) |
| `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 |
Expand All @@ -153,7 +200,8 @@ Fetches a CSV directory of all users in the workspace.
| `SLACK_MCP_LOG_LEVEL` | No | `info` | Log-level for stdout or stderr. Valid values are: `debug`, `info`, `warn`, `error`, `panic` and `fatal` |
| `SLACK_MCP_GOVSLACK` | No | `nil` | Set to `true` to enable [GovSlack](https://slack.com/solutions/govslack) mode. Routes API calls to `slack-gov.com` endpoints instead of `slack.com` for FedRAMP-compliant government workspaces. |

*You need one of: `xoxp` (user), `xoxb` (bot), or both `xoxc`/`xoxd` tokens for authentication.
*You need one of: `xoxp` (user), `xoxb` (bot), or both `xoxc`/`xoxd` tokens for legacy mode authentication.
**For OAuth mode, set `SLACK_MCP_OAUTH_ENABLED=true` and provide Client ID, Secret, and Redirect URI instead.

### Limitations matrix & Cache

Expand Down
176 changes: 143 additions & 33 deletions cmd/slack-mcp-server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@ import (
"context"
"flag"
"fmt"
"net/http"
"os"
"strconv"
"strings"
"sync"
"time"

"github.com/korotovsky/slack-mcp-server/pkg/handler"
"github.com/korotovsky/slack-mcp-server/pkg/oauth"
"github.com/korotovsky/slack-mcp-server/pkg/provider"
"github.com/korotovsky/slack-mcp-server/pkg/server"
"github.com/mattn/go-isatty"
Expand Down Expand Up @@ -40,15 +43,64 @@ func main() {
)
}

p := provider.New(transport, logger)
s := server.NewMCPServer(p, logger)
// Check if OAuth mode is enabled
oauthEnabled := os.Getenv("SLACK_MCP_OAUTH_ENABLED") == "true"

go func() {
var once sync.Once
var s *server.MCPServer
var oauthHandler *server.OAuthHandler
var oauthManager oauth.OAuthManager
var p *provider.ApiProvider

newUsersWatcher(p, &once, logger)()
newChannelsWatcher(p, &once, logger)()
}()
if oauthEnabled {
// OAuth Mode
logger.Info("OAuth mode enabled", zap.String("context", "console"))

// Validate OAuth configuration
clientID := os.Getenv("SLACK_MCP_OAUTH_CLIENT_ID")
clientSecret := os.Getenv("SLACK_MCP_OAUTH_CLIENT_SECRET")
redirectURI := os.Getenv("SLACK_MCP_OAUTH_REDIRECT_URI")

if clientID == "" || clientSecret == "" || redirectURI == "" {
logger.Fatal("OAuth enabled but missing required credentials",
zap.String("context", "console"),
zap.Bool("has_client_id", clientID != ""),
zap.Bool("has_client_secret", clientSecret != ""),
zap.Bool("has_redirect_uri", redirectURI != ""),
)
}

// Create OAuth components
tokenStorage := oauth.NewMemoryStorage()
oauthManager = oauth.NewManager(clientID, clientSecret, redirectURI, tokenStorage)

// Create OAuth handler for HTTP endpoints
oauthHandler = server.NewOAuthHandler(oauthManager, logger)

// Create handlers with OAuth support
conversationsHandler := handler.NewConversationsHandlerWithOAuth(tokenStorage, logger)
channelsHandler := handler.NewChannelsHandlerWithOAuth(tokenStorage, logger)

// Create MCP server with OAuth middleware
s = server.NewMCPServerWithOAuth(conversationsHandler, channelsHandler, oauthManager, logger)

logger.Info("OAuth server initialized",
zap.String("context", "console"),
zap.String("redirect_uri", redirectURI),
)
} else {
// Legacy Mode (existing code)
logger.Info("Legacy mode enabled", zap.String("context", "console"))

p := provider.New(transport, logger)
s = server.NewMCPServer(p, logger)

go func() {
var once sync.Once

newUsersWatcher(p, &once, logger)()
newChannelsWatcher(p, &once, logger)()
}()
}

switch transport {
case "stdio":
Expand All @@ -74,25 +126,54 @@ func main() {
port = strconv.Itoa(defaultSsePort)
}

sseServer := s.ServeSSE(":" + port)
logger.Info(
fmt.Sprintf("SSE server listening on %s", fmt.Sprintf("%s:%s/sse", host, port)),
zap.String("context", "console"),
zap.String("host", host),
zap.String("port", port),
)
addr := host + ":" + port

if oauthEnabled && oauthHandler != nil {
// OAuth mode: use combined handler with HTTP-level auth
handler := s.ServeSSEWithOAuth(":"+port, oauthHandler, oauthManager)

if ready, _ := p.IsReady(); !ready {
logger.Info("Slack MCP Server is still warming up caches",
logger.Info("OAuth endpoints enabled",
zap.String("context", "console"),
zap.String("authorize_url", fmt.Sprintf("http://%s/oauth/authorize", addr)),
zap.String("callback_url", fmt.Sprintf("http://%s/oauth/callback", addr)),
)
}

if err := sseServer.Start(host + ":" + port); err != nil {
logger.Fatal("Server error",
logger.Info(
fmt.Sprintf("SSE server listening on %s/sse", addr),
zap.String("context", "console"),
zap.Error(err),
zap.String("host", host),
zap.String("port", port),
)

if err := http.ListenAndServe(addr, handler); err != nil {
logger.Fatal("Server error",
zap.String("context", "console"),
zap.Error(err),
)
}
} else {
// Legacy mode
handler := s.ServeSSE(":" + port)

logger.Info(
fmt.Sprintf("SSE server listening on %s/sse", addr),
zap.String("context", "console"),
zap.String("host", host),
zap.String("port", port),
)

if ready, _ := p.IsReady(); !ready {
logger.Info("Slack MCP Server is still warming up caches",
zap.String("context", "console"),
)
}

if err := http.ListenAndServe(addr, handler); err != nil {
logger.Fatal("Server error",
zap.String("context", "console"),
zap.Error(err),
)
}
}
case "http":
host := os.Getenv("SLACK_MCP_HOST")
Expand All @@ -104,25 +185,54 @@ func main() {
port = strconv.Itoa(defaultSsePort)
}

httpServer := s.ServeHTTP(":" + port)
logger.Info(
fmt.Sprintf("HTTP server listening on %s", fmt.Sprintf("%s:%s", host, port)),
zap.String("context", "console"),
zap.String("host", host),
zap.String("port", port),
)
addr := host + ":" + port

if ready, _ := p.IsReady(); !ready {
logger.Info("Slack MCP Server is still warming up caches",
if oauthEnabled && oauthHandler != nil {
// OAuth mode: use combined handler with HTTP-level auth
handler := s.ServeHTTPWithOAuth(":"+port, oauthHandler, oauthManager)

logger.Info("OAuth endpoints enabled",
zap.String("context", "console"),
zap.String("authorize_url", fmt.Sprintf("http://%s/oauth/authorize", addr)),
zap.String("callback_url", fmt.Sprintf("http://%s/oauth/callback", addr)),
)
}

if err := httpServer.Start(host + ":" + port); err != nil {
logger.Fatal("Server error",
logger.Info(
fmt.Sprintf("HTTP server listening on %s", addr),
zap.String("context", "console"),
zap.Error(err),
zap.String("host", host),
zap.String("port", port),
)

if err := http.ListenAndServe(addr, handler); err != nil {
logger.Fatal("Server error",
zap.String("context", "console"),
zap.Error(err),
)
}
} else {
// Legacy mode
handler := s.ServeHTTP(":" + port)

logger.Info(
fmt.Sprintf("HTTP server listening on %s", addr),
zap.String("context", "console"),
zap.String("host", host),
zap.String("port", port),
)

if ready, _ := p.IsReady(); !ready {
logger.Info("Slack MCP Server is still warming up caches",
zap.String("context", "console"),
)
}

if err := http.ListenAndServe(addr, handler); err != nil {
logger.Fatal("Server error",
zap.String("context", "console"),
zap.Error(err),
)
}
}
default:
logger.Fatal("Invalid transport type",
Expand Down
Loading