From 55842fe2c835f535b53829aaef7d291e3629ece5 Mon Sep 17 00:00:00 2001 From: George Bashi Date: Tue, 3 Mar 2026 15:31:00 -0800 Subject: [PATCH] feat: add channels_me tool MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a new MCP tool to list channels the calling user is a member of, using the users.conversations API. Follows the same pattern as usergroups_me vs usergroups_list. Unlike channels_list which returns all workspace channels, channels_me returns only channels the user has joined — useful on large workspaces where channels_list returns thousands of results. Supports channel_types, sort (by popularity), limit, and cursor parameters. --- docs/03-configuration-and-usage.md | 4 +- pkg/handler/channels.go | 89 ++++++++++++++++++++++++++++++ pkg/provider/api.go | 11 ++++ pkg/server/server.go | 23 ++++++++ pkg/server/server_test.go | 2 + 5 files changed, 127 insertions(+), 2 deletions(-) diff --git a/docs/03-configuration-and-usage.md b/docs/03-configuration-and-usage.md index 03e8e7e..c3e4c12 100644 --- a/docs/03-configuration-and-usage.md +++ b/docs/03-configuration-and-usage.md @@ -261,7 +261,7 @@ docker-compose up -d | Argument | Required ? | Description | |-----------------------------|------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `--transport` or `-t` | Yes | Select transport for the MCP Server, possible values are: `stdio`, `sse` | -| `--enabled-tools` or `-e` | No | Comma-separated list of tools to register. If not set, all tools are registered. Runtime permissions (e.g., `SLACK_MCP_ADD_MESSAGE_TOOL`) are still enforced. Available tools: `conversations_history`, `conversations_replies`, `conversations_add_message`, `reactions_add`, `reactions_remove`, `attachment_get_data`, `conversations_search_messages`, `conversations_join`, `conversations_leave`, `conversations_unreads`, `conversations_mark`, `channels_list`, `usergroups_list`, `usergroups_me`, `usergroups_create`, `usergroups_update`, `usergroups_users_update`, `users_search`. | +| `--enabled-tools` or `-e` | No | Comma-separated list of tools to register. If not set, all tools are registered. Runtime permissions (e.g., `SLACK_MCP_ADD_MESSAGE_TOOL`) are still enforced. Available tools: `conversations_history`, `conversations_replies`, `conversations_add_message`, `reactions_add`, `reactions_remove`, `attachment_get_data`, `conversations_search_messages`, `conversations_join`, `conversations_leave`, `conversations_unreads`, `conversations_mark`, `channels_list`, `channels_me`, `usergroups_list`, `usergroups_me`, `usergroups_create`, `usergroups_update`, `usergroups_users_update`, `users_search`. | ### Environment Variables @@ -287,7 +287,7 @@ docker-compose up -d | `SLACK_MCP_CACHE_TTL` | No | `24h` | Cache time-to-live. Supports duration format (`24h`, `30m`) or seconds (`3600`). Set to `0` to disable TTL (cache forever). When the cache expires, stale data is served immediately while a background refresh fetches fresh data. | | `SLACK_MCP_MIN_REFRESH_INTERVAL` | No | `30s` | Minimum interval between forced cache refreshes. Prevents API abuse from repeated force-refresh requests. Supports duration format (`30s`, `1m`) or seconds (`60`). Set to `0` to disable rate limiting. | | `SLACK_MCP_LOG_LEVEL` | No | `info` | Log-level for stdout or stderr. Valid values are: `debug`, `info`, `warn`, `error`, `panic` and `fatal` | -| `SLACK_MCP_ENABLED_TOOLS` | No | `nil` | Comma-separated list of tools to register. If empty, all read-only tools and usergroups tools are registered; write tools (`conversations_add_message`, `reactions_add`, `reactions_remove`, `attachment_get_data`) require their specific env var to be set OR must be explicitly listed here. When a write tool is listed here, it's enabled without channel restrictions. Available tools: `conversations_history`, `conversations_replies`, `conversations_add_message`, `reactions_add`, `reactions_remove`, `attachment_get_data`, `conversations_search_messages`, `conversations_join`, `conversations_leave`, `conversations_unreads`, `conversations_mark`, `channels_list`, `usergroups_list`, `usergroups_me`, `usergroups_create`, `usergroups_update`, `usergroups_users_update`, `users_search`. | +| `SLACK_MCP_ENABLED_TOOLS` | No | `nil` | Comma-separated list of tools to register. If empty, all read-only tools and usergroups tools are registered; write tools (`conversations_add_message`, `reactions_add`, `reactions_remove`, `attachment_get_data`) require their specific env var to be set OR must be explicitly listed here. When a write tool is listed here, it's enabled without channel restrictions. Available tools: `conversations_history`, `conversations_replies`, `conversations_add_message`, `reactions_add`, `reactions_remove`, `attachment_get_data`, `conversations_search_messages`, `conversations_join`, `conversations_leave`, `conversations_unreads`, `conversations_mark`, `channels_list`, `channels_me`, `usergroups_list`, `usergroups_me`, `usergroups_create`, `usergroups_update`, `usergroups_users_update`, `users_search`. | ### Tool Registration and Permissions diff --git a/pkg/handler/channels.go b/pkg/handler/channels.go index 6fc669a..dbd7d23 100644 --- a/pkg/handler/channels.go +++ b/pkg/handler/channels.go @@ -9,6 +9,7 @@ import ( "github.com/gocarina/gocsv" "github.com/korotovsky/slack-mcp-server/pkg/provider" + "github.com/slack-go/slack" "github.com/korotovsky/slack-mcp-server/pkg/server/auth" "github.com/korotovsky/slack-mcp-server/pkg/text" "github.com/mark3labs/mcp-go/mcp" @@ -233,6 +234,94 @@ func (ch *ChannelsHandler) ChannelsHandler(ctx context.Context, request mcp.Call return mcp.NewToolResultText(string(csvBytes)), nil } +func (ch *ChannelsHandler) ChannelsMeHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + ch.logger.Debug("ChannelsMeHandler called") + + sortType := request.GetString("sort", "popularity") + types := request.GetString("channel_types", "public_channel,private_channel") + cursor := request.GetString("cursor", "") + limit := request.GetInt("limit", 0) + + if limit == 0 { + limit = 100 + } + if limit > 999 { + limit = 999 + } + + channelTypes := []string{} + for _, t := range strings.Split(types, ",") { + t = strings.TrimSpace(t) + if ch.validTypes[t] { + channelTypes = append(channelTypes, t) + } + } + if len(channelTypes) == 0 { + channelTypes = []string{provider.PubChanType, provider.PrivateChanType} + } + + // Fetch channels the user is a member of via users.conversations API. + usersMap := ch.apiProvider.ProvideUsersMap().Users + var allChannels []provider.Channel + var apiCursor string + for { + params := &slack.GetConversationsForUserParameters{ + Types: channelTypes, + Limit: 200, + Cursor: apiCursor, + ExcludeArchived: true, + } + channels, nextCursor, err := ch.apiProvider.Slack().GetConversationsForUserContext(ctx, params) + if err != nil { + ch.logger.Error("Failed to fetch user conversations", zap.Error(err)) + return nil, fmt.Errorf("failed to fetch your channels: %v", err) + } + + for _, c := range channels { + allChannels = append(allChannels, provider.MapChannelFromSlack(c, usersMap)) + } + + if nextCursor == "" { + break + } + apiCursor = nextCursor + } + + ch.logger.Debug("Fetched member channels", zap.Int("count", len(allChannels))) + + // Paginate results + paged, nextcur := paginateChannels(allChannels, cursor, limit) + + var channelList []Channel + for _, channel := range paged { + channelList = append(channelList, Channel{ + ID: channel.ID, + Name: channel.Name, + Topic: channel.Topic, + Purpose: channel.Purpose, + MemberCount: channel.MemberCount, + }) + } + + switch sortType { + case "popularity": + sort.Slice(channelList, func(i, j int) bool { + return channelList[i].MemberCount > channelList[j].MemberCount + }) + } + + if len(channelList) > 0 && nextcur != "" { + channelList[len(channelList)-1].Cursor = nextcur + } + + csvBytes, err := gocsv.MarshalBytes(&channelList) + if err != nil { + return nil, err + } + + return mcp.NewToolResultText(string(csvBytes)), nil +} + func filterChannelsByTypes(channels map[string]provider.Channel, types []string) []provider.Channel { logger := zap.L() diff --git a/pkg/provider/api.go b/pkg/provider/api.go index 9d1964b..adf891f 100644 --- a/pkg/provider/api.go +++ b/pkg/provider/api.go @@ -1478,3 +1478,14 @@ func mapChannel( Members: members, } } + +// MapChannelFromSlack converts a slack.Channel to our internal Channel type. +func MapChannelFromSlack(c slack.Channel, usersMap map[string]slack.User) Channel { + return mapChannel( + c.ID, c.Name, c.NameNormalized, + c.Topic.Value, c.Purpose.Value, + c.User, c.Members, c.NumMembers, + c.IsIM, c.IsMpIM, c.IsPrivate, c.IsExtShared, + usersMap, + ) +} diff --git a/pkg/server/server.go b/pkg/server/server.go index b513170..028134e 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -37,6 +37,7 @@ const ( ToolConversationsLeave = "conversations_leave" ToolConversationsJoin = "conversations_join" ToolChannelsList = "channels_list" + ToolChannelsMe = "channels_me" ToolUsergroupsList = "usergroups_list" ToolUsergroupsMe = "usergroups_me" ToolUsergroupsCreate = "usergroups_create" @@ -61,6 +62,7 @@ var ValidToolNames = []string{ ToolConversationsLeave, ToolConversationsJoin, ToolChannelsList, + ToolChannelsMe, ToolUsergroupsList, ToolUsergroupsMe, ToolUsergroupsCreate, @@ -418,6 +420,27 @@ func NewMCPServer(provider *provider.ApiProvider, logger *zap.Logger, enabledToo ), channelsHandler.ChannelsHandler) } + if shouldAddTool(ToolChannelsMe, enabledTools, "") { + s.AddTool(mcp.NewTool(ToolChannelsMe, + mcp.WithDescription("List channels you are a member of. Unlike channels_list which returns all workspace channels, this returns only channels you have joined. Useful on large workspaces where channels_list returns thousands of results."), + mcp.WithTitleAnnotation("My Channels"), + mcp.WithReadOnlyHintAnnotation(true), + mcp.WithString("channel_types", + mcp.Description("Comma-separated channel types. Allowed values: 'mpim', 'im', 'public_channel', 'private_channel'. Default: 'public_channel,private_channel'."), + ), + mcp.WithString("sort", + mcp.Description("Type of sorting. Allowed values: 'popularity' (default) - sort by member count."), + ), + mcp.WithNumber("limit", + mcp.DefaultNumber(100), + mcp.Description("Maximum number of items to return (1-999)."), + ), + mcp.WithString("cursor", + mcp.Description("Cursor for pagination."), + ), + ), channelsHandler.ChannelsMeHandler) + } + // User groups tools if shouldAddTool(ToolUsergroupsList, enabledTools, "") { s.AddTool(mcp.NewTool(ToolUsergroupsList, diff --git a/pkg/server/server_test.go b/pkg/server/server_test.go index a0b7280..3ef5a15 100644 --- a/pkg/server/server_test.go +++ b/pkg/server/server_test.go @@ -106,6 +106,7 @@ func TestValidToolNames(t *testing.T) { ToolConversationsLeave: true, ToolConversationsJoin: true, ToolChannelsList: true, + ToolChannelsMe: true, ToolUsergroupsList: true, ToolUsergroupsMe: true, ToolUsergroupsCreate: true, @@ -137,6 +138,7 @@ func TestValidToolNames(t *testing.T) { assert.Equal(t, "conversations_leave", ToolConversationsLeave) assert.Equal(t, "conversations_join", ToolConversationsJoin) assert.Equal(t, "channels_list", ToolChannelsList) + assert.Equal(t, "channels_me", ToolChannelsMe) assert.Equal(t, "usergroups_list", ToolUsergroupsList) assert.Equal(t, "usergroups_me", ToolUsergroupsMe) assert.Equal(t, "usergroups_create", ToolUsergroupsCreate)