Skip to content

Conversation

@saoudrizwan
Copy link

@saoudrizwan saoudrizwan commented Dec 25, 2025

Adds two new tools for efficiently managing unread Slack messages 🚀

When you're in a lot of Slack channels (especially with external partner channels), keeping up with unreads becomes overwhelming. The existing conversations_history tool requires you to know which channel to check. These new tools let you:

  1. Get a prioritized view of ALL unread channels in one API call
  2. Filter to only channels where you're @mentioned
  3. Mark channels as read after reviewing them

New Tools

conversations_unreads

Efficiently retrieves unread messages across all channels using a single API call.

Parameters:

Parameter Type Default Description
include_messages bool true Return actual messages or just channel summary
channel_types string "all" Filter: "dm", "group_dm", "partner", "internal", or "all"
max_channels int 50 Limit number of channels returned
max_messages_per_channel int 10 Messages to fetch per channel
mentions_only bool false Only return channels where you have @mentions

Output (CSV):

ChannelID,ChannelName,ChannelType,UnreadCount,LastRead,Latest
D09EDMKJC64,@john,dm,5,,1760563532.527859
C08KX4PV3S6,#ext-partner-company,partner,2,1763493994.468259,1766596058.035729
C09J740AZTR,#engineering,internal,1,1759517692.989219,1762753562.005619

conversations_mark

Marks a channel or DM as read.

Parameters:

Parameter Type Required Description
channel_id string Yes Channel ID, #channel-name, or @username
ts string No Timestamp to mark read up to. If omitted, marks all as read

Technical Implementation

Why client.counts API?

We initially tried using ClientUserBoot but discovered it only returns channels in the user's sidebar, not all channels with unreads. The client.counts API (already implemented in the edge client) is what Slack's web client uses:

  • Returns HasUnreads boolean and MentionCount for ALL channels in one call
  • Separates channels into three arrays: Channels, MPIMs (group DMs), IMs (direct messages)
  • Much more efficient than checking each channel individually

Channel Categorization & Priority

Results are automatically sorted by priority:

  1. DMs (dm) - Direct messages, highest priority
  2. Group DMs (group_dm) - Multi-person direct messages
  3. Partner channels (partner) - Externally shared channels (uses IsExtShared metadata)
  4. Internal channels (internal) - Everything else

Code Changes

pkg/provider/api.go

  • Added IsExtShared field to Channel struct
  • Exposed ClientCounts method on SlackAPI interface
  • Updated mapChannel to pass through IsExtShared

pkg/handler/conversations.go

  • Added UnreadChannel struct for CSV output
  • Added ConversationsUnreadsHandler with priority sorting and filtering
  • Added ConversationsMarkHandler with channel name resolution
  • Added sortChannelsByPriority helper

pkg/server/server.go

  • Registered both new tools with proper annotations

Example Use Cases

1. Morning Inbox Review

"What are my unread messages?"
→ conversations_unreads (returns prioritized list: DMs first, then partner channels, then internal)

2. Priority Inbox - Just @Mentions

"Show me only channels where I'm @mentioned"
→ conversations_unreads with mentions_only: true

3. Check Partner Channels Only

"What's new in external partner channels?"
→ conversations_unreads with channel_types: "partner"

4. Quick Summary Without Messages

"Which channels have unreads?"
→ conversations_unreads with include_messages: false

5. Mark Channel as Read

"Mark #random as read"
→ conversations_mark with channel_id: "#random"

6. Triage Workflow

1. Get unreads summary: conversations_unreads with include_messages: false
2. Review a specific channel: conversations_history
3. Mark it as read: conversations_mark
4. Repeat

Breaking Changes

Users with existing channel caches will need to delete ~/Library/Caches/slack-mcp-server/channels_cache_v2.json (or equivalent) to repopulate with the new IsExtShared field. Otherwise, partner channel detection won't work until the cache is refreshed.

Test Plan

  • conversations_unreads returns prioritized list of unread channels
  • channel_types filter correctly filters by dm/group_dm/partner/internal
  • mentions_only: true filters to only channels with @mentions
  • include_messages: false returns summary without fetching message content
  • Partner channels detected via IsExtShared metadata (not name prefix)
  • conversations_mark marks channels as read using channel ID
  • conversations_mark resolves #channel-name to channel ID
  • conversations_mark resolves @username to DM channel ID
  • conversations_mark without timestamp marks all messages as read

…rieval

- Uses ClientUserBoot to get all channels with LastRead/Latest in one API call
- Filters channels where Latest > LastRead to find unreads
- Prioritizes: DMs > group DMs > partner channels (ext-*) > internal
- Only fetches message history for channels with actual unreads
- Supports filtering by channel type and configurable limits

Addresses issue korotovsky#114
- Switch from ClientUserBoot to ClientCounts API
- ClientCounts returns HasUnreads boolean for all channels
- Add ClientCounts to SlackAPI interface
- Process Channels, MPIMs, and IMs separately
- Strip existing # prefix before adding to avoid ##name
- Use stripped name for ext-/shared- prefix checks for partner type
- Add mentions_only parameter to conversations_unreads to filter
  channels to only those with @mentions (priority inbox)
- Add conversations_mark tool to mark channels/DMs as read
  - Supports channel IDs, #channel names, and @username
  - If no timestamp provided, marks all messages as read
…nels

- Add IsExtShared field to Channel struct in cache
- Pass IsExtShared through mapChannel function
- Use cached.IsExtShared to identify external/partner channels
  instead of checking for ext-/shared- name prefixes

Note: Users may need to delete their channels cache file to repopulate
with the new IsExtShared field.
Copy link
Owner

@korotovsky korotovsky left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @saoudrizwan, thank you for the great contribution!

Since the new API method has been introduced and this MCP supports multiple slack token types, a question from my side: have you checked that ClientCounts also returns data if we provide xoxb or xoxp (as I assume your main setup is xoxc/xoxd pair since you are using undocumented APIs, this is is where this API works the best and has the most coverage)

ch.logger.Debug("ConversationsUnreadsHandler called", zap.Any("params", request.Params))

// Get optional parameters
includeMessages := request.GetBool("include_messages", true)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we extract these parameters into struct + small func as other handlers do?

}

// ConversationsMarkHandler marks a channel as read up to a specific timestamp
func (ch *ConversationsHandler) ConversationsMarkHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To ensure the safe rollout of new tools within existing agentic setups, I usually require any new tool that can add messages or perform write actions to be protected by a new environment variable. This safeguard is disabled by default and must be explicitly enabled by users who have updated Slack and intentionally want to use the tool. Otherwise, other users could be exposed to newly deployed tools with write access enabled by default, which I believe must be avoided for security reasons. Not sure if defining mcp.WithDestructiveHintAnnotation(false), is enough here, because we don't know how all (most) clients implement the MCP protocol.

}

// categorizeChannel determines the type of channel for prioritization
func (ch *ConversationsHandler) categorizeChannel(id, name string, isIM, isMpIM, isPrivate, isExtShared bool) string {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to be not used anywhere

}

// getChannelDisplayName returns a human-readable channel name
func (ch *ConversationsHandler) getChannelDisplayName(id, name string, isIM, isMpIM bool, members []string, usersMap map[string]slack.User, channelsMaps *provider.ChannelsCache) string {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to be not used anywhere

continue
}

// Get channel info from cache to determine type and name
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think would be better to reuse existing helper, it can handle more specific cases like groups and so on: https://github.com/korotovsky/slack-mcp-server/blob/master/pkg/handler/conversations.go#L711-L728

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants