mirror of
https://github.com/paymog/slack-cli.git
synced 2026-09-18 23:43:49 +08:00
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:
committed by
Dmitrii Korotovskii
parent
b32ab23eb9
commit
55842fe2c8
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user