feat: add channels_me tool

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.
This commit is contained in:
George Bashi
2026-03-03 15:31:00 -08:00
committed by Dmitrii Korotovskii
parent b32ab23eb9
commit 55842fe2c8
5 changed files with 127 additions and 2 deletions
+2 -2
View File
@@ -261,7 +261,7 @@ docker-compose up -d
| Argument | Required ? | Description | | Argument | Required ? | Description |
|-----------------------------|------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| |-----------------------------|------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `--transport` or `-t` | Yes | Select transport for the MCP Server, possible values are: `stdio`, `sse` | | `--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 ### 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_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_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_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 ### Tool Registration and Permissions
+89
View File
@@ -9,6 +9,7 @@ import (
"github.com/gocarina/gocsv" "github.com/gocarina/gocsv"
"github.com/korotovsky/slack-mcp-server/pkg/provider" "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/server/auth"
"github.com/korotovsky/slack-mcp-server/pkg/text" "github.com/korotovsky/slack-mcp-server/pkg/text"
"github.com/mark3labs/mcp-go/mcp" "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 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 { func filterChannelsByTypes(channels map[string]provider.Channel, types []string) []provider.Channel {
logger := zap.L() logger := zap.L()
+11
View File
@@ -1478,3 +1478,14 @@ func mapChannel(
Members: members, 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,
)
}
+23
View File
@@ -37,6 +37,7 @@ const (
ToolConversationsLeave = "conversations_leave" ToolConversationsLeave = "conversations_leave"
ToolConversationsJoin = "conversations_join" ToolConversationsJoin = "conversations_join"
ToolChannelsList = "channels_list" ToolChannelsList = "channels_list"
ToolChannelsMe = "channels_me"
ToolUsergroupsList = "usergroups_list" ToolUsergroupsList = "usergroups_list"
ToolUsergroupsMe = "usergroups_me" ToolUsergroupsMe = "usergroups_me"
ToolUsergroupsCreate = "usergroups_create" ToolUsergroupsCreate = "usergroups_create"
@@ -61,6 +62,7 @@ var ValidToolNames = []string{
ToolConversationsLeave, ToolConversationsLeave,
ToolConversationsJoin, ToolConversationsJoin,
ToolChannelsList, ToolChannelsList,
ToolChannelsMe,
ToolUsergroupsList, ToolUsergroupsList,
ToolUsergroupsMe, ToolUsergroupsMe,
ToolUsergroupsCreate, ToolUsergroupsCreate,
@@ -418,6 +420,27 @@ func NewMCPServer(provider *provider.ApiProvider, logger *zap.Logger, enabledToo
), channelsHandler.ChannelsHandler) ), 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 // User groups tools
if shouldAddTool(ToolUsergroupsList, enabledTools, "") { if shouldAddTool(ToolUsergroupsList, enabledTools, "") {
s.AddTool(mcp.NewTool(ToolUsergroupsList, s.AddTool(mcp.NewTool(ToolUsergroupsList,
+2
View File
@@ -106,6 +106,7 @@ func TestValidToolNames(t *testing.T) {
ToolConversationsLeave: true, ToolConversationsLeave: true,
ToolConversationsJoin: true, ToolConversationsJoin: true,
ToolChannelsList: true, ToolChannelsList: true,
ToolChannelsMe: true,
ToolUsergroupsList: true, ToolUsergroupsList: true,
ToolUsergroupsMe: true, ToolUsergroupsMe: true,
ToolUsergroupsCreate: true, ToolUsergroupsCreate: true,
@@ -137,6 +138,7 @@ func TestValidToolNames(t *testing.T) {
assert.Equal(t, "conversations_leave", ToolConversationsLeave) assert.Equal(t, "conversations_leave", ToolConversationsLeave)
assert.Equal(t, "conversations_join", ToolConversationsJoin) assert.Equal(t, "conversations_join", ToolConversationsJoin)
assert.Equal(t, "channels_list", ToolChannelsList) assert.Equal(t, "channels_list", ToolChannelsList)
assert.Equal(t, "channels_me", ToolChannelsMe)
assert.Equal(t, "usergroups_list", ToolUsergroupsList) assert.Equal(t, "usergroups_list", ToolUsergroupsList)
assert.Equal(t, "usergroups_me", ToolUsergroupsMe) assert.Equal(t, "usergroups_me", ToolUsergroupsMe)
assert.Equal(t, "usergroups_create", ToolUsergroupsCreate) assert.Equal(t, "usergroups_create", ToolUsergroupsCreate)