Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -112,11 +112,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 @@ -126,6 +169,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 @@ -142,7 +189,8 @@ Fetches a CSV directory of all users in the workspace.
| `SLACK_MCP_CHANNELS_CACHE` | No | `~/Library/Caches/slack-mcp-server/channels_cache_v2.json` (macOS)<br>`~/.cache/slack-mcp-server/channels_cache_v2.json` (Linux)<br>`%LocalAppData%/slack-mcp-server/channels_cache_v2.json` (Windows) | 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 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
175 changes: 142 additions & 33 deletions cmd/slack-mcp-server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@ import (
"context"
"flag"
"fmt"
"net/http"
"os"
"strconv"
"strings"
"sync"

"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 @@ -39,15 +42,63 @@ 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 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 @@ -67,25 +118,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
handler := s.ServeSSEWithOAuth(":"+port, oauthHandler)

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
sseServer := 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 := sseServer.Start(addr); err != nil {
logger.Fatal("Server error",
zap.String("context", "console"),
zap.Error(err),
)
}
}
case "http":
host := os.Getenv("SLACK_MCP_HOST")
Expand All @@ -97,25 +177,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 oauthEnabled && oauthHandler != nil {
// OAuth mode: use combined handler
handler := s.ServeHTTPWithOAuth(":"+port, oauthHandler)

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 := 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
httpServer := 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 := httpServer.Start(addr); err != nil {
logger.Fatal("Server error",
zap.String("context", "console"),
zap.Error(err),
)
}
}
default:
logger.Fatal("Invalid transport type",
Expand Down
Loading