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.
The integration test added in #252 was reading row[0], which is the channel
ID column in channels_list CSV output, not the channel name. The CI for
#252 never ran (status checks were empty at merge time), so this slipped
through. Now resolves to the Name column by header lookup, matching the
convention used elsewhere in this file.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When emails are forwarded to Slack channels, message content is stored in
files[] with filetype "email" rather than in text or blocks. This adds
FilesToText() to extract From, CC, and Subject metadata as a fallback
when msg.Text is empty, so these messages no longer appear as blank rows
in conversations_history output.
Closes#191
The sort=popularity option forced fetching every channel the user belongs
to before sorting client-side — hundreds of API calls on large workspaces.
The Slack API doesn't support server-side sorting, so this was inherently
expensive. Remove it and always stop fetching once we have enough results,
using the Slack API's native cursor for pagination.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When sort is not "popularity", stop paginating the Slack API as soon as
we have enough results for the requested limit, and pass through the
API's native cursor. On large workspaces this avoids hundreds of API
calls when only a small number of channels are requested.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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.
The test was added by #272 (d3f7ca3) when AttachmentToText still applied
`(` → `[` and `)` → `]` substitutions, which produced `[text][url]` output.
#281 (5c095ac) removed those substitutions, so AttachmentToText now emits
proper markdown `[text](url)`. The test wasn't updated in #281's merge.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- 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>
- 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>
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>
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>
Add optional `blocks` parameter to conversations_add_message for raw
Slack Block Kit JSON support (rich_text lists, code blocks, etc.).
When blocks is provided it takes precedence over text/content_type for
message rendering. The text parameter serves as notification fallback.
The blocks argument accepts both a JSON string and a raw JSON array to
accommodate different MCP client serialization behaviors.
Also bumps takara2314/slack-go-util from v0.3.0 to v0.4.0 which adds
nested list support to the existing text/markdown conversion path.
The handler was re-fetching the just-posted message via conversations.history
and returning it as CSV. Slack's history endpoint has propagation lag, so when
the message wasn't yet indexed the response was a header-only CSV with no data
rows, which confused LLM clients into thinking the post had failed.
Drop the follow-up history call and return a short success string with channel
and ts (matching ReactionsAddHandler). This removes the race entirely and
saves a tier3 API call per post.
Signed-off-by: Seena Fallah <seenafallah@gmail.com>
Previously, the AttachmentIDs field only contained raw file IDs
(e.g. "F08ABC1234"), making it impossible to identify which file
an ID corresponds to without calling attachment_get_data first.
Now the field includes filenames: "F08ABC1234 (contract.pdf)".
This makes it practical to use AttachmentIDs to selectively
download relevant attachments.
Fixes#260
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
The Slack search API returns both Channel.ID and Permalink on every
SearchMessage, but the MCP server was only using Channel.Name (formatted
as '#channel-name') and discarding the rest. This made it impossible for
LLM agents to construct valid Slack permalink URLs.
- Add Permalink field to Message struct
- Include msg.Channel.ID in the Channel column (format: 'C0515UGHR0R (#channel-name)')
- Pass through msg.Permalink from the Slack API response
Addresses korotovsky/slack-mcp-server#100.
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>
SearchContext was the only Slack API call in conversations.go without
rate limiting or retry logic. Under concurrent load (e.g. parallel
searches across multiple workspaces), this caused immediate failures
when hitting Slack's Tier 2 rate limits.
Wrap the call with limiter.CallWithRetry using a Tier2 rate limiter,
matching the established pattern used by GetConversationHistoryContext
and GetConversationInfoContext in the same file.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Closes#251. Adds keyword filtering to channels_list so users can find
channels without paginating through the entire list. Matches are
case-insensitive substrings; query_targets controls which fields to
search (name by default, optionally topic and purpose).
Previously, attachment_get_data returned image files as base64-encoded
strings inside a JSON text response. For typical images (100-200KB),
the base64 expansion produces 130-270KB of text that exceeds MCP client
token limits, forcing clients to save overflow to temp files and manually
decode base64 — defeating the purpose of the tool.
Use the MCP SDK's NewToolResultImage to return images as native image
content, which MCP clients can render directly. File metadata (file_id,
filename, mimetype, size) is returned as the text component. Non-image
binary files retain the existing base64-in-text behavior.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When include_messages=true, items whose message content could not be
fetched (e.g. channel access denied) were silently dropped from the
output due to a `continue` that skipped the fetched counter.
Changes:
- Move fetched++ before the includeMessages block so all items count
- Let message fetch failures fall through instead of skipping the item
- Add a note to output when some items lack message content
- Fall back to metadata CSV when all message fetches fail
Fixes: 4 of 11 saved items silently dropped on enterprise Slack
Made-with: Cursor
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
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.