Skip to main content
Glama

Slack MCP

MIT License
696
  • Apple
  • Linux
channels.go8.41 kB
package handler import ( "context" "encoding/base64" "fmt" "sort" "strings" "github.com/gocarina/gocsv" "github.com/korotovsky/slack-mcp-server/pkg/provider" "github.com/korotovsky/slack-mcp-server/pkg/server/auth" "github.com/korotovsky/slack-mcp-server/pkg/text" "github.com/mark3labs/mcp-go/mcp" "go.uber.org/zap" ) type Channel struct { ID string `json:"id"` Name string `json:"name"` Topic string `json:"topic"` Purpose string `json:"purpose"` MemberCount int `json:"memberCount"` Cursor string `json:"cursor"` } type ChannelsHandler struct { apiProvider *provider.ApiProvider validTypes map[string]bool logger *zap.Logger } func NewChannelsHandler(apiProvider *provider.ApiProvider, logger *zap.Logger) *ChannelsHandler { validTypes := make(map[string]bool, len(provider.AllChanTypes)) for _, v := range provider.AllChanTypes { validTypes[v] = true } return &ChannelsHandler{ apiProvider: apiProvider, validTypes: validTypes, logger: logger, } } func (ch *ChannelsHandler) ChannelsResource(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) { ch.logger.Debug("ChannelsResource called", zap.Any("params", request.Params)) // mark3labs/mcp-go does not support middlewares for resources. if authenticated, err := auth.IsAuthenticated(ctx, ch.apiProvider.ServerTransport(), ch.logger); !authenticated { ch.logger.Error("Authentication failed for channels resource", zap.Error(err)) return nil, err } var channelList []Channel if ready, err := ch.apiProvider.IsReady(); !ready { ch.logger.Error("API provider not ready", zap.Error(err)) return nil, err } ar, err := ch.apiProvider.Slack().AuthTest() if err != nil { ch.logger.Error("Auth test failed", zap.Error(err)) return nil, err } ws, err := text.Workspace(ar.URL) if err != nil { ch.logger.Error("Failed to parse workspace from URL", zap.String("url", ar.URL), zap.Error(err), ) return nil, fmt.Errorf("failed to parse workspace from URL: %v", err) } channels := ch.apiProvider.ProvideChannelsMaps().Channels ch.logger.Debug("Retrieved channels from provider", zap.Int("count", len(channels))) for _, channel := range channels { channelList = append(channelList, Channel{ ID: channel.ID, Name: channel.Name, Topic: channel.Topic, Purpose: channel.Purpose, MemberCount: channel.MemberCount, }) } csvBytes, err := gocsv.MarshalBytes(&channelList) if err != nil { ch.logger.Error("Failed to marshal channels to CSV", zap.Error(err)) return nil, err } return []mcp.ResourceContents{ mcp.TextResourceContents{ URI: "slack://" + ws + "/channels", MIMEType: "text/csv", Text: string(csvBytes), }, }, nil } func (ch *ChannelsHandler) ChannelsHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { ch.logger.Debug("ChannelsHandler called") if ready, err := ch.apiProvider.IsReady(); !ready { ch.logger.Error("API provider not ready", zap.Error(err)) return nil, err } sortType := request.GetString("sort", "popularity") types := request.GetString("channel_types", provider.PubChanType) cursor := request.GetString("cursor", "") limit := request.GetInt("limit", 0) ch.logger.Debug("Request parameters", zap.String("sort", sortType), zap.String("channel_types", types), zap.String("cursor", cursor), zap.Int("limit", limit), ) // MCP Inspector v0.14.0 has issues with Slice type // introspection, so some type simplification makes sense here channelTypes := []string{} for _, t := range strings.Split(types, ",") { t = strings.TrimSpace(t) if ch.validTypes[t] { channelTypes = append(channelTypes, t) } else if t != "" { ch.logger.Warn("Invalid channel type ignored", zap.String("type", t)) } } if len(channelTypes) == 0 { ch.logger.Debug("No valid channel types provided, using defaults") channelTypes = append(channelTypes, provider.PubChanType) channelTypes = append(channelTypes, provider.PrivateChanType) } ch.logger.Debug("Validated channel types", zap.Strings("types", channelTypes)) if limit == 0 { limit = 100 ch.logger.Debug("Limit not provided, using default", zap.Int("limit", limit)) } if limit > 999 { ch.logger.Warn("Limit exceeds maximum, capping to 999", zap.Int("requested", limit)) limit = 999 } var ( nextcur string channelList []Channel ) allChannels := ch.apiProvider.ProvideChannelsMaps().Channels ch.logger.Debug("Total channels available", zap.Int("count", len(allChannels))) channels := filterChannelsByTypes(allChannels, channelTypes) ch.logger.Debug("Channels after filtering by type", zap.Int("count", len(channels))) var chans []provider.Channel chans, nextcur = paginateChannels( channels, cursor, limit, ) ch.logger.Debug("Pagination results", zap.Int("returned_count", len(chans)), zap.Bool("has_next_page", nextcur != ""), ) for _, channel := range chans { channelList = append(channelList, Channel{ ID: channel.ID, Name: channel.Name, Topic: channel.Topic, Purpose: channel.Purpose, MemberCount: channel.MemberCount, }) } switch sortType { case "popularity": ch.logger.Debug("Sorting channels by popularity (member count)") sort.Slice(channelList, func(i, j int) bool { return channelList[i].MemberCount > channelList[j].MemberCount }) default: ch.logger.Debug("No sorting applied", zap.String("sort_type", sortType)) } if len(channelList) > 0 && nextcur != "" { channelList[len(channelList)-1].Cursor = nextcur ch.logger.Debug("Added cursor to last channel", zap.String("cursor", nextcur)) } csvBytes, err := gocsv.MarshalBytes(&channelList) if err != nil { ch.logger.Error("Failed to marshal channels to CSV", zap.Error(err)) return nil, err } return mcp.NewToolResultText(string(csvBytes)), nil } func filterChannelsByTypes(channels map[string]provider.Channel, types []string) []provider.Channel { logger := zap.L() var result []provider.Channel typeSet := make(map[string]bool) for _, t := range types { typeSet[t] = true } publicCount := 0 privateCount := 0 imCount := 0 mpimCount := 0 for _, ch := range channels { if typeSet["public_channel"] && !ch.IsPrivate && !ch.IsIM && !ch.IsMpIM { result = append(result, ch) publicCount++ } if typeSet["private_channel"] && ch.IsPrivate && !ch.IsIM && !ch.IsMpIM { result = append(result, ch) privateCount++ } if typeSet["im"] && ch.IsIM { result = append(result, ch) imCount++ } if typeSet["mpim"] && ch.IsMpIM { result = append(result, ch) mpimCount++ } } logger.Debug("Channel filtering complete", zap.Int("total_input", len(channels)), zap.Int("total_output", len(result)), zap.Int("public_channels", publicCount), zap.Int("private_channels", privateCount), zap.Int("ims", imCount), zap.Int("mpims", mpimCount), ) return result } func paginateChannels(channels []provider.Channel, cursor string, limit int) ([]provider.Channel, string) { logger := zap.L() sort.Slice(channels, func(i, j int) bool { return channels[i].ID < channels[j].ID }) startIndex := 0 if cursor != "" { if decoded, err := base64.StdEncoding.DecodeString(cursor); err == nil { lastID := string(decoded) for i, ch := range channels { if ch.ID > lastID { startIndex = i break } } logger.Debug("Decoded cursor", zap.String("cursor", cursor), zap.String("decoded_id", lastID), zap.Int("start_index", startIndex), ) } else { logger.Warn("Failed to decode cursor", zap.String("cursor", cursor), zap.Error(err), ) } } endIndex := startIndex + limit if endIndex > len(channels) { endIndex = len(channels) } paged := channels[startIndex:endIndex] var nextCursor string if endIndex < len(channels) { nextCursor = base64.StdEncoding.EncodeToString([]byte(channels[endIndex-1].ID)) logger.Debug("Generated next cursor", zap.String("last_id", channels[endIndex-1].ID), zap.String("next_cursor", nextCursor), ) } logger.Debug("Pagination complete", zap.Int("total_channels", len(channels)), zap.Int("start_index", startIndex), zap.Int("end_index", endIndex), zap.Int("page_size", len(paged)), zap.Bool("has_more", nextCursor != ""), ) return paged, nextCursor }

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