Skip to main content
Glama

Slack MCP

MIT License
823
  • Apple
  • Linux
api.go20.2 kB
package provider import ( "context" "encoding/json" "errors" "io/ioutil" "os" "strings" "github.com/korotovsky/slack-mcp-server/pkg/limiter" "github.com/korotovsky/slack-mcp-server/pkg/provider/edge" "github.com/korotovsky/slack-mcp-server/pkg/transport" "github.com/rusq/slackdump/v3/auth" "github.com/slack-go/slack" "go.uber.org/zap" "golang.org/x/time/rate" ) const usersNotReadyMsg = "users cache is not ready yet, sync process is still running... please wait" const channelsNotReadyMsg = "channels cache is not ready yet, sync process is still running... please wait" const defaultUA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36" var AllChanTypes = []string{"mpim", "im", "public_channel", "private_channel"} var PrivateChanType = "private_channel" var PubChanType = "public_channel" var ErrUsersNotReady = errors.New(usersNotReadyMsg) var ErrChannelsNotReady = errors.New(channelsNotReadyMsg) type UsersCache struct { Users map[string]slack.User `json:"users"` UsersInv map[string]string `json:"users_inv"` } type ChannelsCache struct { Channels map[string]Channel `json:"channels"` ChannelsInv map[string]string `json:"channels_inv"` } type Channel struct { ID string `json:"id"` Name string `json:"name"` Topic string `json:"topic"` Purpose string `json:"purpose"` MemberCount int `json:"memberCount"` IsMpIM bool `json:"mpim"` IsIM bool `json:"im"` IsPrivate bool `json:"private"` } type SlackAPI interface { // Standard slack-go API methods AuthTest() (*slack.AuthTestResponse, error) AuthTestContext(ctx context.Context) (*slack.AuthTestResponse, error) GetUsersContext(ctx context.Context, options ...slack.GetUsersOption) ([]slack.User, error) GetUsersInfo(users ...string) (*[]slack.User, error) PostMessageContext(ctx context.Context, channel string, options ...slack.MsgOption) (string, string, error) MarkConversationContext(ctx context.Context, channel, ts string) error // Used to get messages GetConversationHistoryContext(ctx context.Context, params *slack.GetConversationHistoryParameters) (*slack.GetConversationHistoryResponse, error) GetConversationRepliesContext(ctx context.Context, params *slack.GetConversationRepliesParameters) (msgs []slack.Message, hasMore bool, nextCursor string, err error) SearchContext(ctx context.Context, query string, params slack.SearchParameters) (*slack.SearchMessages, *slack.SearchFiles, error) // Used to get channels list from both Slack and Enterprise Grid versions GetConversationsContext(ctx context.Context, params *slack.GetConversationsParameters) ([]slack.Channel, string, error) // Edge API methods ClientUserBoot(ctx context.Context) (*edge.ClientUserBootResponse, error) } type MCPSlackClient struct { slackClient *slack.Client edgeClient *edge.Client authResponse *slack.AuthTestResponse authProvider auth.Provider isEnterprise bool isOAuth bool teamEndpoint string } type ApiProvider struct { transport string client SlackAPI logger *zap.Logger rateLimiter *rate.Limiter users map[string]slack.User usersInv map[string]string usersCache string usersReady bool channels map[string]Channel channelsInv map[string]string channelsCache string channelsReady bool } func NewMCPSlackClient(authProvider auth.Provider, logger *zap.Logger) (*MCPSlackClient, error) { httpClient := transport.ProvideHTTPClient(authProvider.Cookies(), logger) slackClient := slack.New(authProvider.SlackToken(), slack.OptionHTTPClient(httpClient), ) authResp, err := slackClient.AuthTest() if err != nil { return nil, err } authResponse := &slack.AuthTestResponse{ URL: authResp.URL, Team: authResp.Team, User: authResp.User, TeamID: authResp.TeamID, UserID: authResp.UserID, EnterpriseID: authResp.EnterpriseID, BotID: authResp.BotID, } slackClient = slack.New(authProvider.SlackToken(), slack.OptionHTTPClient(httpClient), slack.OptionAPIURL(authResp.URL+"api/"), ) edgeClient, err := edge.NewWithInfo(authResponse, authProvider, edge.OptionHTTPClient(httpClient), ) if err != nil { return nil, err } isEnterprise := authResp.EnterpriseID != "" return &MCPSlackClient{ slackClient: slackClient, edgeClient: edgeClient, authResponse: authResponse, authProvider: authProvider, isEnterprise: isEnterprise, isOAuth: strings.HasPrefix(authProvider.SlackToken(), "xoxp-"), teamEndpoint: authResp.URL, }, nil } func (c *MCPSlackClient) AuthTest() (*slack.AuthTestResponse, error) { if os.Getenv("SLACK_MCP_XOXP_TOKEN") == "demo" || (os.Getenv("SLACK_MCP_XOXC_TOKEN") == "demo" && os.Getenv("SLACK_MCP_XOXD_TOKEN") == "demo") { return &slack.AuthTestResponse{ URL: "https://_.slack.com", Team: "Demo Team", User: "Username", TeamID: "TEAM123456", UserID: "U1234567890", EnterpriseID: "", BotID: "", }, nil } if c.authResponse != nil { return c.authResponse, nil } return c.slackClient.AuthTest() } func (c *MCPSlackClient) AuthTestContext(ctx context.Context) (*slack.AuthTestResponse, error) { return c.slackClient.AuthTestContext(ctx) } func (c *MCPSlackClient) GetUsersContext(ctx context.Context, options ...slack.GetUsersOption) ([]slack.User, error) { return c.slackClient.GetUsersContext(ctx, options...) } func (c *MCPSlackClient) GetUsersInfo(users ...string) (*[]slack.User, error) { return c.slackClient.GetUsersInfo(users...) } func (c *MCPSlackClient) MarkConversationContext(ctx context.Context, channel, ts string) error { return c.slackClient.MarkConversationContext(ctx, channel, ts) } func (c *MCPSlackClient) GetConversationsContext(ctx context.Context, params *slack.GetConversationsParameters) ([]slack.Channel, string, error) { // Please see https://github.com/korotovsky/slack-mcp-server/issues/73 // It seems that `conversations.list` works with `xoxp` tokens within Enterprise Grid setups // and if `xoxc`/`xoxd` defined we fallback to edge client. // In non Enterprise Grid setups we always use `conversations.list` api as it accepts both token types wtf. if c.isEnterprise { if c.isOAuth { return c.slackClient.GetConversationsContext(ctx, params) } else { edgeChannels, _, err := c.edgeClient.GetConversationsContext(ctx, nil) if err != nil { return nil, "", err } var channels []slack.Channel for _, ec := range edgeChannels { if params != nil && params.ExcludeArchived && ec.IsArchived { continue } channels = append(channels, slack.Channel{ IsGeneral: ec.IsGeneral, GroupConversation: slack.GroupConversation{ Conversation: slack.Conversation{ ID: ec.ID, IsIM: ec.IsIM, IsMpIM: ec.IsMpIM, IsPrivate: ec.IsPrivate, Created: slack.JSONTime(ec.Created.Time().UnixMilli()), Unlinked: ec.Unlinked, NameNormalized: ec.NameNormalized, IsShared: ec.IsShared, IsExtShared: ec.IsExtShared, IsOrgShared: ec.IsOrgShared, IsPendingExtShared: ec.IsPendingExtShared, NumMembers: ec.NumMembers, }, Name: ec.Name, IsArchived: ec.IsArchived, Members: ec.Members, Topic: slack.Topic{ Value: ec.Topic.Value, }, Purpose: slack.Purpose{ Value: ec.Purpose.Value, }, }, }) } return channels, "", nil } } return c.slackClient.GetConversationsContext(ctx, params) } func (c *MCPSlackClient) GetConversationHistoryContext(ctx context.Context, params *slack.GetConversationHistoryParameters) (*slack.GetConversationHistoryResponse, error) { return c.slackClient.GetConversationHistoryContext(ctx, params) } func (c *MCPSlackClient) GetConversationRepliesContext(ctx context.Context, params *slack.GetConversationRepliesParameters) (msgs []slack.Message, hasMore bool, nextCursor string, err error) { return c.slackClient.GetConversationRepliesContext(ctx, params) } func (c *MCPSlackClient) SearchContext(ctx context.Context, query string, params slack.SearchParameters) (*slack.SearchMessages, *slack.SearchFiles, error) { return c.slackClient.SearchContext(ctx, query, params) } func (c *MCPSlackClient) PostMessageContext(ctx context.Context, channelID string, options ...slack.MsgOption) (string, string, error) { return c.slackClient.PostMessageContext(ctx, channelID, options...) } func (c *MCPSlackClient) ClientUserBoot(ctx context.Context) (*edge.ClientUserBootResponse, error) { return c.edgeClient.ClientUserBoot(ctx) } func (c *MCPSlackClient) IsEnterprise() bool { return c.isEnterprise } func (c *MCPSlackClient) AuthResponse() *slack.AuthTestResponse { return c.authResponse } func (c *MCPSlackClient) Raw() struct { Slack *slack.Client Edge *edge.Client } { return struct { Slack *slack.Client Edge *edge.Client }{ Slack: c.slackClient, Edge: c.edgeClient, } } func New(transport string, logger *zap.Logger) *ApiProvider { var ( authProvider auth.ValueAuth err error ) // Check for XOXP token first (User OAuth) xoxpToken := os.Getenv("SLACK_MCP_XOXP_TOKEN") if xoxpToken != "" { authProvider, err = auth.NewValueAuth(xoxpToken, "") if err != nil { logger.Fatal("Failed to create auth provider with XOXP token", zap.Error(err)) } 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") 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") } authProvider, err = auth.NewValueAuth(xoxcToken, xoxdToken) if err != nil { logger.Fatal("Failed to create auth provider with XOXC/XOXD tokens", zap.Error(err)) } return newWithXOXC(transport, authProvider, logger) } func newWithXOXP(transport string, authProvider auth.ValueAuth, logger *zap.Logger) *ApiProvider { var ( client *MCPSlackClient err error ) usersCache := os.Getenv("SLACK_MCP_USERS_CACHE") if usersCache == "" { usersCache = ".users_cache.json" } channelsCache := os.Getenv("SLACK_MCP_CHANNELS_CACHE") if channelsCache == "" { channelsCache = ".channels_cache_v2.json" } if os.Getenv("SLACK_MCP_XOXP_TOKEN") == "demo" || (os.Getenv("SLACK_MCP_XOXC_TOKEN") == "demo" && os.Getenv("SLACK_MCP_XOXD_TOKEN") == "demo") { logger.Info("Demo credentials are set, skip.") } else { client, err = NewMCPSlackClient(authProvider, logger) if err != nil { logger.Fatal("Failed to create MCP Slack client", zap.Error(err)) } } return &ApiProvider{ transport: transport, client: client, logger: logger, rateLimiter: limiter.Tier2.Limiter(), users: make(map[string]slack.User), usersInv: map[string]string{}, usersCache: usersCache, channels: make(map[string]Channel), channelsInv: map[string]string{}, channelsCache: channelsCache, } } func newWithXOXC(transport string, authProvider auth.ValueAuth, logger *zap.Logger) *ApiProvider { var ( client *MCPSlackClient err error ) usersCache := os.Getenv("SLACK_MCP_USERS_CACHE") if usersCache == "" { usersCache = ".users_cache.json" } channelsCache := os.Getenv("SLACK_MCP_CHANNELS_CACHE") if channelsCache == "" { channelsCache = ".channels_cache_v2.json" } if os.Getenv("SLACK_MCP_XOXP_TOKEN") == "demo" || (os.Getenv("SLACK_MCP_XOXC_TOKEN") == "demo" && os.Getenv("SLACK_MCP_XOXD_TOKEN") == "demo") { logger.Info("Demo credentials are set, skip.") } else { client, err = NewMCPSlackClient(authProvider, logger) if err != nil { logger.Fatal("Failed to create MCP Slack client", zap.Error(err)) } } return &ApiProvider{ transport: transport, client: client, logger: logger, rateLimiter: limiter.Tier2.Limiter(), users: make(map[string]slack.User), usersInv: map[string]string{}, usersCache: usersCache, channels: make(map[string]Channel), channelsInv: map[string]string{}, channelsCache: channelsCache, } } func (ap *ApiProvider) RefreshUsers(ctx context.Context) error { var ( list []slack.User usersCounter = 0 optionLimit = slack.GetUsersOptionLimit(1000) ) if data, err := ioutil.ReadFile(ap.usersCache); err == nil { var cachedUsers []slack.User if err := json.Unmarshal(data, &cachedUsers); err != nil { ap.logger.Warn("Failed to unmarshal users cache, will refetch", zap.String("cache_file", ap.usersCache), zap.Error(err)) } else { for _, u := range cachedUsers { ap.users[u.ID] = u ap.usersInv[u.Name] = u.ID } ap.logger.Info("Loaded users from cache", zap.Int("count", len(cachedUsers)), zap.String("cache_file", ap.usersCache)) ap.usersReady = true return nil } } users, err := ap.client.GetUsersContext(ctx, optionLimit, ) if err != nil { ap.logger.Error("Failed to fetch users", zap.Error(err)) return err } else { list = append(list, users...) } for _, user := range users { ap.users[user.ID] = user ap.usersInv[user.Name] = user.ID usersCounter++ } users, err = ap.GetSlackConnect(ctx) if err != nil { ap.logger.Error("Failed to fetch users from Slack Connect", zap.Error(err)) return err } else { list = append(list, users...) } for _, user := range users { ap.users[user.ID] = user ap.usersInv[user.Name] = user.ID usersCounter++ } if data, err := json.MarshalIndent(list, "", " "); err != nil { ap.logger.Error("Failed to marshal users for cache", zap.Error(err)) } else { if err := ioutil.WriteFile(ap.usersCache, data, 0644); err != nil { ap.logger.Error("Failed to write cache file", zap.String("cache_file", ap.usersCache), zap.Error(err)) } else { ap.logger.Info("Wrote users to cache", zap.Int("count", usersCounter), zap.String("cache_file", ap.usersCache)) } } ap.usersReady = true return nil } func (ap *ApiProvider) RefreshChannels(ctx context.Context) error { if data, err := ioutil.ReadFile(ap.channelsCache); err == nil { var cachedChannels []Channel if err := json.Unmarshal(data, &cachedChannels); err != nil { ap.logger.Warn("Failed to unmarshal channels cache, will refetch", zap.String("cache_file", ap.channelsCache), zap.Error(err)) } else { for _, c := range cachedChannels { ap.channels[c.ID] = c ap.channelsInv[c.Name] = c.ID } ap.logger.Info("Loaded channels from cache", zap.Int("count", len(cachedChannels)), zap.String("cache_file", ap.channelsCache)) ap.channelsReady = true return nil } } channels := ap.GetChannels(ctx, AllChanTypes) if data, err := json.MarshalIndent(channels, "", " "); err != nil { ap.logger.Error("Failed to marshal channels for cache", zap.Error(err)) } else { if err := ioutil.WriteFile(ap.channelsCache, data, 0644); err != nil { ap.logger.Error("Failed to write cache file", zap.String("cache_file", ap.channelsCache), zap.Error(err)) } else { ap.logger.Info("Wrote channels to cache", zap.Int("count", len(channels)), zap.String("cache_file", ap.channelsCache)) } } ap.channelsReady = true return nil } func (ap *ApiProvider) GetSlackConnect(ctx context.Context) ([]slack.User, error) { boot, err := ap.client.ClientUserBoot(ctx) if err != nil { ap.logger.Error("Failed to fetch client user boot", zap.Error(err)) return nil, err } var collectedIDs []string for _, im := range boot.IMs { if !im.IsShared && !im.IsExtShared { continue } _, ok := ap.users[im.User] if !ok { collectedIDs = append(collectedIDs, im.User) } } res := make([]slack.User, 0, len(collectedIDs)) if len(collectedIDs) > 0 { usersInfo, err := ap.client.GetUsersInfo(strings.Join(collectedIDs, ",")) if err != nil { ap.logger.Error("Failed to fetch users info for shared IMs", zap.Error(err)) return nil, err } for _, u := range *usersInfo { res = append(res, u) } } return res, nil } func (ap *ApiProvider) GetChannelsType(ctx context.Context, channelType string) []Channel { params := &slack.GetConversationsParameters{ Types: []string{channelType}, Limit: 999, ExcludeArchived: true, } var ( channels []slack.Channel chans []Channel nextcur string err error ) for { if err := ap.rateLimiter.Wait(ctx); err != nil { ap.logger.Error("Rate limiter wait failed", zap.Error(err)) return nil } channels, nextcur, err = ap.client.GetConversationsContext(ctx, params) ap.logger.Debug("Fetched channels for ", zap.String("channelType", channelType), zap.Int("count", len(channels)), ) if err != nil { ap.logger.Error("Failed to fetch channels", zap.Error(err)) break } for _, channel := range channels { ch := mapChannel( channel.ID, channel.Name, channel.NameNormalized, channel.Topic.Value, channel.Purpose.Value, channel.User, channel.Members, channel.NumMembers, channel.IsIM, channel.IsMpIM, channel.IsPrivate, ap.ProvideUsersMap().Users, ) chans = append(chans, ch) } if nextcur == "" { break } params.Cursor = nextcur } return chans } func (ap *ApiProvider) GetChannels(ctx context.Context, channelTypes []string) []Channel { if len(channelTypes) == 0 { channelTypes = AllChanTypes } var chans []Channel for _, t := range AllChanTypes { var typeChannels = ap.GetChannelsType(ctx, t) chans = append(chans, typeChannels...) } for _, ch := range chans { ap.channels[ch.ID] = ch ap.channelsInv[ch.Name] = ch.ID } var res []Channel for _, t := range channelTypes { for _, channel := range ap.channels { if t == "public_channel" && !channel.IsPrivate { res = append(res, channel) } if t == "private_channel" && channel.IsPrivate { res = append(res, channel) } if t == "im" && channel.IsIM { res = append(res, channel) } if t == "mpim" && channel.IsMpIM { res = append(res, channel) } } } return res } func (ap *ApiProvider) ProvideUsersMap() *UsersCache { return &UsersCache{ Users: ap.users, UsersInv: ap.usersInv, } } func (ap *ApiProvider) ProvideChannelsMaps() *ChannelsCache { return &ChannelsCache{ Channels: ap.channels, ChannelsInv: ap.channelsInv, } } func (ap *ApiProvider) IsReady() (bool, error) { if !ap.usersReady { return false, ErrUsersNotReady } if !ap.channelsReady { return false, ErrChannelsNotReady } return true, nil } func (ap *ApiProvider) ServerTransport() string { return ap.transport } func (ap *ApiProvider) Slack() SlackAPI { return ap.client } func mapChannel( id, name, nameNormalized, topic, purpose, user string, members []string, numMembers int, isIM, isMpIM, isPrivate bool, usersMap map[string]slack.User, ) Channel { channelName := name finalPurpose := purpose finalTopic := topic finalMemberCount := numMembers if isIM { finalMemberCount = 2 if u, ok := usersMap[user]; ok { channelName = "@" + u.Name finalPurpose = "DM with " + u.RealName } else { channelName = "@" + user finalPurpose = "DM with " + user } finalTopic = "" } else if isMpIM { if len(members) > 0 { finalMemberCount = len(members) var userNames []string for _, uid := range members { if u, ok := usersMap[uid]; ok { userNames = append(userNames, u.RealName) } else { userNames = append(userNames, uid) } } channelName = "@" + nameNormalized finalPurpose = "Group DM with " + strings.Join(userNames, ", ") finalTopic = "" } } else { channelName = "#" + nameNormalized } return Channel{ ID: id, Name: channelName, Topic: finalTopic, Purpose: finalPurpose, MemberCount: finalMemberCount, IsIM: isIM, IsMpIM: isMpIM, IsPrivate: isPrivate, } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/korotovsky/slack-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server