74 Commits

Author SHA1 Message Date
Paymahn Moghadasian 6a972a1f05 feat: add slack-cli, a no-daemon CLI over the slack-mcp-server engine
Turn the forked slack-mcp-server into a CLI so running many agents no
longer means one resident MCP process each. Every command is a
short-lived process that reads the shared on-disk cache.

- rename module to github.com/paymog/slack-cli (go install/homebrew/ldflags)
- internal/toolcall: invoke the upstream tool handlers in-process; the only
  mcp-go coupling lives here, so pkg/handler and pkg/provider are reused
  byte-for-byte (clean upstream merges, fork-and-extend)
- internal/{cli,cmds,config,credstore,runtime,output}: cobra command tree,
  keyring-backed credential profiles, provider bootstrap, result printing
- 21 tools as subcommands (channels, conversations, users, usergroups,
  saved, reactions, attachments, cache); write tools keep their env gating
- goreleaser + homebrew release workflow; ships a skills/slack-cli skill
- unit tests for config/credstore/toolcall; MCP server still builds

The MCP server (cmd/slack-mcp-server) is kept intact.
2026-06-26 11:04:05 -05:00
Dmitrii Korotovskii 342e3e3cd2 Merge pull request #269 from gkatz2/feature/targeted-user-cache-patch
Add targeted user cache patch on miss
2026-05-14 23:01:12 +02:00
George Bashi 55842fe2c8 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.
2026-05-14 22:20:31 +02:00
Dmitrii Korotovskii 69cd994d9d Merge pull request #240 from jonzarecki/feat/saved-items
feat: add saved_list, saved_update, and saved_clear_completed tools
2026-05-14 22:11:31 +02:00
Francis J. Lacoste df22121447 fix: address code review findings from multi-agent analysis
- Fix infinite loop on cold start when API returns zero results: return
  error instead of nil when no existing cache is available, so the
  watcher calls Fatal rather than spinning IsReady() forever
- Secure temp file handling: use os.CreateTemp for unpredictable names
  (prevents symlink attacks) and clean up temp files on any failure
- Restrict cache file permissions from 0644 to 0600 and cache directory
  from 0755 to 0700 (cache contains user PII)
- Extract atomicWriteFile helper to deduplicate temp+rename pattern
- Fix stale docstring: getCacheTTL default is 24h not 1h

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-14 21:44:44 +02:00
Francis J. Lacoste 6e93a40174 fix: address PR #225 review feedback for SWR cache
- Restore IsReady() polling loop before ServeStdio() to fix cold-start
  regression where tool calls fail for 60-90s on first run (no cache)
- Add fetchUsersMu/fetchChannelsMu mutexes to serialize fetchAndStore*
  calls, preventing race between ForceRefresh and background refresh
- Use atomic file writes (temp + os.Rename) to prevent corrupt cache
  files on crash
- Guard against empty API results overwriting valid cache
- Guard against empty cache files being treated as valid data
- Fix typo: TestRefreshingFlagPreventsConucrrentRefreshes → Concurrent

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-14 21:44:44 +02:00
Francis J. Lacoste f16fb0fe2b feat: stale-while-revalidate cache for large Slack workspaces
On large workspaces (41K+ users), the server blocks for ~90 seconds
during startup while fetching all users/channels from the Slack API,
exceeding MCP client connection timeouts.

Changes:
- Load expired cache files immediately, mark server ready, then refresh
  in background via goroutine (stale-while-revalidate pattern)
- Convert usersReady/channelsReady to atomic.Bool for race-free reads
- Add refreshingUsers/refreshingChannels atomic.Bool to coalesce
  concurrent background refreshes via CompareAndSwap
- Remove stdio IsReady() polling loop (no longer needed)
- Increase default cache TTL from 1h to 24h
- Document SLACK_MCP_CACHE_TTL and SLACK_MCP_MIN_REFRESH_INTERVAL
  env vars in docs/03-configuration-and-usage.md

Fixes startup timeout on large workspaces. Server now starts in under
1 second regardless of workspace size when a cache file exists.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-14 21:44:44 +02:00
Greg Katz 2d19f8c752 Add targeted user cache patch on miss
When a user ID is not in the in-memory cache, message rendering
degrades to raw IDs and paramFormatUser fails the tool call
entirely. This is common on Enterprise Grid workspaces where the
user cache (50K+ users) can be hours stale.

On cache miss, fetch the single user via users.info and patch the
snapshot atomically. This costs one API call instead of rebuilding
the entire user cache. Disk persistence is skipped; the next full
refresh cycle handles it.

Fixes #268

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-14 12:03:17 -07:00
Dmitrii Korotovskii 9e9f84cfba Merge pull request #219 from georgebashi/feat/conversations-join-leave
feat: add conversations_join and conversations_leave tools
2026-05-14 20:47:36 +02:00
Dmitrii Korotovskii 5e72c6bb91 Merge pull request #263 from Christian-Sidak/feat/users-search-by-id
feat: support direct user ID lookup in users_search
2026-05-14 20:47:31 +02:00
Dmitrii Korotovskii 5690a6a3ab Merge pull request #297 from aharon-br/master
fix: handle Slack refresh token format (xoxe)
2026-05-14 20:47:17 +02:00
Harry Richman a851750aaa Update api.go
re https://github.com/korotovsky/slack-mcp-server/issues/115
2026-05-13 16:25:14 +01:00
Mustafa Mujahid 8256b2f343 fix: non-blocking stdio startup and --no-cache flag
Remove the busy-wait loop that blocks the stdio transport from
responding to MCP initialize until users/channels caches are fully
loaded. On large workspaces this causes MCP clients with connection
timeouts (e.g. 30-60s) to drop the server.

Changes:
- stdio transport now starts the MCP server immediately, matching the
  existing SSE/HTTP behavior. Caches continue loading in a background
  goroutine. Tool calls made before caches are ready return a graceful
  "not ready" error (already handled by the error recovery middleware).
- Add --no-cache CLI flag to skip cache loading entirely for
  environments that only use channel/user IDs (never #name or @name
  lookups). This makes startup instant regardless of workspace size.
- Add SkipCache() method on ApiProvider that marks both caches as
  ready without loading data.

Fixes #271
2026-04-10 15:39:36 +02:00
32134178csa 869899c18e feat: support direct user ID lookup in users_search tool
When the query matches a Slack user ID pattern (e.g., U07VCEPP4N5),
use the users.info API for direct lookup instead of searching by
name/email/display name. This is useful when you already have a
user ID (from message metadata, mentions, etc.) and need to resolve
it to user details.

Falls back to existing search behavior for non-ID queries.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 20:42:23 -07:00
Jonathan Zarecki 3617d5ee65 feat: add saved_list, saved_update, and saved_clear_completed tools
Add three new tools for managing Slack's "Save for Later" panel,
replacing the deprecated stars.* API (March 2023). Uses internal
saved.list, saved.update, and saved.clearCompleted endpoints.

- saved_list: List saved items with filter (saved/completed/archived),
  auto-pagination, and optional message content fetching
- saved_update: Mark items as completed and/or set due dates
- saved_clear_completed: Bulk-clear all completed items

All tools require browser session tokens (xoxc/xoxd) and are guarded
at registration time for bot/OAuth tokens.

Includes unit tests for CSV format, field extraction, timestamp
formatting, parameter validation, and response parsing.

Made-with: Cursor
2026-03-08 22:07:54 +02:00
George Bashi 110ae5a42f feat: add conversations_join and conversations_leave tools
Add two new MCP tools for channel membership management:

- conversations_join: Join public channels via conversations.join API.
  Idempotent — joining a channel you're already in is a no-op.
  Requires channels:join (xoxb) or channels:write (xoxp) scope.

- conversations_leave: Leave channels, group conversations, or DMs
  via conversations.leave API. Marked as destructive.
  Requires channels:manage (xoxb) or channels:write (xoxp) scope.
  On Enterprise Grid with session tokens (xoxc/xoxd), routes through
  the edge API to bypass enterprise_is_restricted errors.

Both tools accept channel IDs (Cxxxxxxxxxx) or names (#channel-name)
using the existing resolveChannelID helper.

Updated docs/01-authentication-setup.md with new OAuth scopes
(channels:join, channels:manage) and docs/03-configuration-and-usage.md
with the new tool names in the available tools list.
2026-03-03 15:20:18 -08:00
George Bashi 3b846b9607 fix: merge edge and standard API results for Enterprise Grid channels
On Enterprise Grid with restricted teams, the edge API's SearchChannels
and ClientUserBoot goroutines may fail while IMList succeeds. Since
goroutine errors are non-fatal (partial results kept), the edge API
returns only DMs with err=nil — so the standard API fallback never
triggers, and public/private channels are missing from the cache.

Fix: always supplement edge API results with full pagination of the
standard conversations.list API, deduplicating by channel ID. This
ensures we get DMs/MPIMs from the edge API AND public/private channels
from the standard API regardless of which edge goroutines fail.
2026-03-03 12:36:59 -08:00
George Bashi ba07a6fc37 refactor: fetch all channel types in a single API call
GetChannels previously called GetChannelsType in a loop, once per
channel type (mpim, im, public_channel, private_channel). Both the
standard conversations.list API and the edge API support fetching
all types in a single request, so this loop was making 4 redundant
round-trips.

Extract getChannelsMultiType to accept a slice of channel types,
and call it once with all types from GetChannels. GetChannelsType
is preserved as a thin wrapper for callers that need single-type
fetches.
2026-03-03 11:31:39 -08:00
George Bashi 9c93f5d4cb fix: prevent null cache poisoning from permanently hiding channels
When the Slack API returned no channels (due to upstream failures),
json.MarshalIndent(nil) produced "null" which was written to the
cache file. On subsequent startups, json.Unmarshal("null") into a
slice succeeds with no error, producing an empty slice that was
treated as valid cached data — permanently serving empty results
until the cache TTL expired.

Three defense-in-depth guards:
- Don't write cache file when channel list is empty
- Treat empty/null unmarshaled channels cache as invalid (refetch)
- Same empty-cache guard for users cache (consistency)
2026-03-03 11:31:39 -08:00
George Bashi 2caae3e0ef fix: fall back to standard API when edge API fails on Enterprise Grid
For Enterprise Grid workspaces with xoxc/xoxd session tokens, the
server exclusively used the edge API for channel listing. If the
edge API returned an error, the server propagated it with no
channels.

Add a fallback to the standard conversations.list API, which also
works with xoxc tokens in many Enterprise Grid setups. Cache the
edge failure in an edgeFailed flag so that subsequent pagination
calls go straight to the standard API instead of retrying and
failing on every page.
2026-03-03 11:31:39 -08:00
George Bashi cdcd9fe4d9 fix(edge): collect partial results instead of discarding on errors
The edge API's GetConversationsContext runs 3 concurrent goroutines
(ClientUserBoot, IMList, SearchChannels), then calls ClientCounts
and ConversationsGenericInfo for MPIM discovery.

Previously, ANY failure in the pipeline discarded ALL successfully
fetched channels:
- A goroutine error discarded results from other goroutines
- ClientCounts failing (e.g. team_is_restricted) discarded everything
- ConversationsGenericInfo failing discarded everything

Now errors are non-fatal: channels from successful sources are kept.
Only if all primary sources fail (0 channels collected) is the error
propagated. MPIM discovery failures are always non-fatal since MPIMs
are supplementary to the main channel/IM/DM data.
2026-03-03 11:31:39 -08:00
Manuel Kreutz f50473c33c perf: use users.conversations instead of conversations.list for xoxp scan (#7)
Switch the xoxp unread scan from conversations.list to users.conversations.
users.conversations returns only channels the calling user is a member of,
eliminating non-member public channels and closed DMs that cannot have unreads.

Empirically tested on a 2700-channel workspace:
- 37% fewer channels to scan (1724 vs 2737)
- 85% reduction for public channels (156 vs 1046)
- Zero real unreads missed (verified against client.counts ground truth)
- Both methods return identical channel sets for member channels
- The 10 'missed' channels are all archived with never-visited markers
2026-02-23 09:40:37 -06:00
Manuel Kreutz 43a2ec1145 fix: token-type routing and validation for conversations_unreads (#3)
* fix: pre-check token type to avoid faulty ClientCounts call

client.counts only works with browser session tokens (xoxc/xoxd). OAuth
tokens (xoxp) get 'not_allowed_token_type' and bot tokens (xoxb) have no
concept of user-level unreads.

Instead of calling ClientCounts and catching the error after the fact,
pre-check the token type using the existing IsOAuth()/IsBotToken()
infrastructure and route directly to the appropriate path:
- xoxc/xoxd: fast path via client.counts (unchanged)
- xoxp: conversations.info fallback (no wasted API call)
- xoxb: clear error message

Follows the same pattern used by SearchUsers and GetConversationsContext
which already branch on token type.

Addresses review comments on korotovsky/slack-mcp-server#171.

* fix: exclude conversations_unreads tool for bot tokens

Bot tokens (xoxb) don't support unread tracking — it's a user-level
concept. Don't register the tool at all for bot token users, matching
the same pattern used for conversations_search_messages.

This gives a clean UX: bot users simply don't see the tool, rather
than getting a runtime error.

* fix: cap xoxp fallback scan depth and document limitations

The xoxp path previously scanned ALL channels (~2000+ API calls on large
workspaces). Slack's API has no bulk unread endpoint for xoxp tokens —
every other open-source implementation (agent-kit, NextNotifier, wee-slack)
uses the same per-channel scanning approach.

Changes:
- Cap scan depth to budget*2 per type group (~300 API calls max with
  default max_channels=50, down from ~2000+)
- Return scan metadata (channels scanned, API calls) from each type group
- Prepend an xoxp limitation note to response text so the LLM knows
  results may be partial
- Update tool description to explain xoxc vs xoxp behavior
- Fix inaccurate comment about conversations.list sort order

* fix: validate GetMutedChannels response to surface xoxp failures

Add validate() call after ParseResponse in GetMutedChannels (prefs.go),
matching the pattern used in ClientCounts. Without this, xoxp tokens
receive {ok:false, error:missing_scope} but ParseResponse only checks
HTTP status — the error was silently swallowed, returning nil/nil.

This caused muted channels to leak into xoxp results (17/28 channels
were muted in testing). Now the error propagates to the handler's
existing warn-and-proceed logic.

Also surface the limitation in the xoxp response note so the LLM
knows muted filtering is unavailable.

* fix: add shouldAddTool gating for conversations_unreads and conversations_mark

Both tools were missing Tool* constants, ValidToolNames entries, and
shouldAddTool() wrapping — breaking the established pattern used by
every other tool in the server. This prevented selective enable/disable
via the enabledTools config.

Also fixes indentation on conversations_mark registration block.
2026-02-13 14:01:09 -06:00
Manuel Kreutz 3e0e4ea792 fix: unread count persistence + backfill + muted channel filtering (#2)
* fix: use index-based range loop so unread counts persist

The message-fetch loop uses 'for _, uc := range unreadChannels', so
writes to uc.UnreadCount go to a copy — the original slice element
stays at 0. Messages are fetched correctly (appended to a separate
slice), but the unread count column is silently wrong.

Switch to 'for i := range' and index into the slice directly.
Also aligns struct field formatting (gofmt).

* feat: backfill unread counts via conversations.history

client.counts returns HasUnreads (bool) and MentionCount per channel,
but MentionCount is only non-zero for @mentions. Regular channel
activity shows as 'has unreads' with count 0.

For channels where HasUnreads=true and MentionCount=0, call
conversations.history(oldest=lastRead, limit=20) to count actual
unread messages. DMs don't need this since every message counts
as a mention.

* feat: filter muted channels from unreads by default

Fetches muted channel set from users.prefs.get (all_notifications_prefs)
and excludes them from both ClientCounts and conversations.info fallback
paths. Adds include_muted opt-in parameter to show muted channels when
explicitly requested.

Muted status is only available in users.prefs.get as a nested JSON
string — it is not exposed by client.counts, conversations.info, or
conversations.list.
2026-02-11 19:43:05 -06:00
flare576 e43380f89a fix: validate ok field in ClientCounts to trigger xoxp fallback
ClientCounts was not calling baseResponse.validate() after ParseResponse,
so when xoxp tokens received HTTP 200 with {"ok": false, "error":
"not_allowed_token_type"}, no Go error was raised and the xoxp fallback
path never triggered. Users saw "0 unreads" silently.

This follows the pattern used in search.go which correctly validates.

Credit: @140am for identifying the issue and providing the fix.
2026-02-11 17:46:12 -06:00
flare576 e238772b7e feat: add xoxp token fallback for conversations_unreads using conversations.info
Co-authored-by: niebloomj <5156403+niebloomj@users.noreply.github.com>
2026-02-11 17:11:17 -06:00
Saoud Rizwan f695d2c474 fix: use IsExtShared metadata instead of name prefix for partner channels
- 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.
2026-02-11 17:10:04 -06:00
Saoud Rizwan b1c64345c4 fix: use ClientCounts API for reliable unread detection
- Switch from ClientUserBoot to ClientCounts API
- ClientCounts returns HasUnreads boolean for all channels
- Add ClientCounts to SlackAPI interface
- Process Channels, MPIMs, and IMs separately
2026-02-11 17:10:03 -06:00
Dmitrii Korotovskii 6ddc82863a Merge pull request #183 from Flare576/feat/startup-auth-validation
feat: add startup auth validation and workspace-isolated cache paths
2026-02-12 00:04:15 +01:00
flare576 7afabd5cb9 feat: add startup auth validation and workspace-isolated cache paths
- Validate Slack tokens on startup via AuthTest() - server now fails fast
  with clear error message if tokens are invalid/expired
- Namespace cache files by TeamID (e.g., T12345_users_cache.json) to prevent
  cross-workspace cache contamination when switching between Slack workspaces
- Log authenticated workspace info on startup for debugging
2026-02-10 20:00:16 -06:00
sspiridonov 9afc063f24 feat: add user group management tools
Add five new tools for managing Slack user groups (subteams):

- usergroups_list: List all user groups in the workspace
- usergroups_me: Manage your own membership (list/join/leave)
- usergroups_create: Create a new user group
- usergroups_update: Update group metadata (name, handle, description)
- usergroups_users_update: Replace all members of a group

The usergroups_me tool provides a convenient way to join or leave
groups without needing to know the full member list. It handles
fetching current members and updating the list automatically.

Required OAuth scopes:
- usergroups:read (for list operations)
- usergroups:write (for create/update/join/leave operations)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-08 20:14:45 +01:00
flare576 f0e830e431 feat: add OAuth token fallback for users_search
For xoxp/xoxb tokens, search the local users cache using regex matching
on username, real name, display name, and email. Browser tokens (xoxc/xoxd)
continue using the edge API for real-time search.
2026-02-04 17:41:51 -06:00
flare576 a7b7e58dbd feat: add users_search tool for finding users by name/email
Adds a new users_search tool that searches for Slack users by name,
email, or display name using the Edge API. Returns user details
including UserID, username, real name, display name, email, title,
and DM channel ID (if available in cache).

Note: This feature requires browser session tokens (xoxc/xoxd),
not OAuth tokens (xoxp/xoxb), similar to conversations_unreads.
2026-02-04 17:40:28 -06:00
Lukasz Jagiello 503c752278 feat: add cache TTL and refresh-on-error for channel lookups
Fixes the issue where newly created Slack channels aren't discovered
until the MCP server is restarted.

Key changes:
- Add configurable cache TTL via SLACK_MCP_CACHE_TTL env var (default 1h)
- TTL uses file mtime so it survives server restarts correctly
- Add refresh-on-error: when channel lookup fails, refresh cache and retry once
- Add rate limiting via SLACK_MCP_MIN_REFRESH_INTERVAL (default 30s)
- Use atomic.Pointer[T] for cache maps to eliminate GC pressure on reads
- Thread-safe implementation with proper mutex protection

The refresh-on-error flow:
1. Channel name lookup fails (not in cache)
2. Force refresh channels from Slack API (rate-limited)
3. Retry lookup once
4. Return error if still not found

Performance optimizations:
- Cache config parsed once at init, not on every refresh
- Atomic pointer swap for map access (no copying on reads)
- Skip redundant lookup when rate-limited
- Single lock scope for rate limit check (prevents TOCTOU race)

Env vars:
- SLACK_MCP_CACHE_TTL: Cache duration (e.g., "1h", "30m", "3600", "0" to disable)
- SLACK_MCP_MIN_REFRESH_INTERVAL: Min time between forced refreshes (e.g., "30s")
2026-02-03 00:49:51 -08:00
flare576 81606854f1 feat: add files_get tool for downloading file content
Adds ability to download file content by file ID, addressing maintainer
request on PR #170.

- New files_get tool gated by SLACK_MCP_FILES_TOOL env var
- Text files (text/*, application/json, etc.) returned as plain text
- Binary files returned as base64-encoded content
- 5MB size limit to keep responses reasonable for LLM context
- Returns structured JSON: file_id, filename, mimetype, size, encoding, content
2026-01-29 16:50:05 -06:00
flare576 f483c35aca feat: add reactions_remove tool
Companion to reactions_add (merged in #141). Allows removing emoji
reactions from messages using the same channel/timestamp/emoji params.

Tested with xoxb, xoxc/xoxd token types.
2026-01-29 14:47:10 -06:00
Dmitrii Korotovskii 9844d5b0ad Merge pull request #141 from xav-ie/feat-add-reactions
feat-add-reactions
2026-01-29 21:44:24 +01:00
Aron Gates 4454d67ff6 feat: govslack compatibility 2026-01-28 00:03:28 -03:00
Xavier Ruiz af19d5ee17 feat: add reaction tool 2025-12-18 22:13:51 -05:00
Dmitrii Korotovskii 454398a7f7 go format 2025-12-09 22:59:13 +01:00
Dmitrii Korotovskii 3668595335 Merge pull request #117 from sanghoon/feat/support_bot_token_reimpl
Add bot token (xoxb) support
2025-11-29 15:47:14 +01:00
redaphid 44e6ba319c default cache file creation to os cache dir 2025-11-10 19:00:39 -07:00
Benjamin Ertl 0e5bfda24d Fix: Preserve user IDs in channel cache for DM name display
DM channels now show proper names like @testuser instead of just @
after cache reload. Fixed by adding User and Members fields to Channel struct
and updating IM/UserBootChannel conversion methods.
2025-10-29 16:11:42 +01:00
Sanghoon Hong bb9469d3e6 Add bot token (xoxb) support
- Add support for bot tokens as an alternative authentication method
- Conditionally register search tool only for non-bot tokens (bots cannot use search.messages API)
- Update isOAuth definition to include both xoxp and xoxb tokens for cleaner implementation
- Add bot token configuration to manifest-dxt.json for DXT extension
- Document bot token limitations in README and authentication setup guide

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-19 01:26:48 +09:00
Dmitrii Korotovskii 7d7d136e8b Fixed #110. Removed chans variable override lead to memory override during channels fetch on pagination after optimizations done in previos PR #107 2025-10-11 11:48:46 +02:00
Dmitrii Korotovskii c42f2d3d1c Update api.go fix cache location 2025-10-08 17:49:58 +02:00
ztNIE 80067ab305 docs: fix typos in comments 2025-09-24 22:00:11 +10:00
Octavian Rinciog b42dd068a8 fix: serialize getting message types to avoid slack rate limits 2025-09-12 13:28:23 +03:00
Dmitrii Korotovskii e271e0affc little cherry-pick from pull/72 by https://github.com/dionny 2025-08-15 10:55:36 +02:00
Dmitrii Korotovskii 32a951b484 tls handshake fingerprinting 2025-07-24 12:13:37 +02:00