Skip to content

feat(conversations): add conversations_threads tool (Slack "Threads" view) - #347

Open
panevesht wants to merge 2 commits into
korotovsky:masterfrom
panevesht:feat/threads-view
Open

panevesht wants to merge 2 commits into
korotovsky:masterfrom
panevesht:feat/threads-view

Conversation

@panevesht

@panevesht panevesht commented Aug 18, 2026 •

Copy link
Copy Markdown

Summary

New read-only tool conversations_threads: lists the threads you are subscribed to — Slack's "Threads" view — newest activity first, with each thread's unread reply count and the unread (or latest) replies. By default it returns only threads with unread replies from the last 30 days.

Until now there was no way to see which threads have unread replies: conversations_unreads covers channels/DMs only, and thread unread state lives in a separate per-thread read cursor. (Independent of #346, which adds per-thread marking; together they close the loop list → read → mark. The tool text deliberately does not reference that parameter so it is correct on master as-is.)

Design

  • New edge call SubscriptionsThreadGetView → Slack's subscriptions.thread.getView (the endpoint behind the Threads view; same undocumented browser-session family as client.counts / saved.*), with current_ts (cursor), limit, and the optional server-side channel_id filter; typed response (threads[] with root_msg, latest_replies, unread_replies; total_unread_replies, new_threads_count, has_more, max_ts).
  • Observed constraints that shape the tool: Slack serves 10 threads per page whatever limit says, and there is no server-side unread-only filter. The handler therefore pages newest-first and stops at the time bound (since), the end of the view, or a 50-page cap (500 threads), always returning the continuation cursor in the last row's Cursor column — so no scan is lossy and the LLM can keep going.
  • Parameters: filter (unread default | all, declared as an enum), channel_id (#name/@name resolved like elsewhere), since (1d/7d/2w/1m — days/weeks/months, default 30d, or all), limit (default 20, max 200), include_replies (default 3, max 20, 0 to omit), cursor (repeat the other filters when continuing). Slack timestamps are compared numerically; the cursor is validated as a Slack timestamp (six fractional digits). An empty result explains why the scan stopped (time window / page cap / end of view).
  • Output CSV: Channel, ChannelID, ThreadTs, RootUser, RootTime, RootText, ReplyCount, UnreadReplies, LatestReplyTime, LastRead, Replies, Cursor. Root and replies go through the same user-resolution/text pipeline as the other conversation tools (convertMessagesFromHistory); Replies holds the unread replies when there are any, otherwise the latest ones, as time author: text joined by ||.
  • Registered only for browser session tokens (xoxc/xoxd), like the other edge-API tools; ReadOnlyHint. Uses the Tier-3 limiter between pages.
  • Kept out of docs/03 tool lists on purpose to avoid conflicting with feat(saved): add saved_add and saved_delete tools #345, which already edits those lines; happy to add it there once one of the two lands.
File Change
pkg/provider/edge/threads.go NEW — SubscriptionsThreadGetView + response types
pkg/provider/edge/threads_test.go NEW — HTTP-level unit tests with a fixture mirroring the live response
pkg/provider/api.go Added to SlackAPI + MCPSlackClient wrapper
pkg/handler/threads.go NEW — ThreadsHandler: params, collectThreads (paging / filter / bound / cursor / cap), buildThreadRows (pure, injected renderer), noThreadsMessage
pkg/handler/threads_test.go NEW — paging/filter/bound/cursor/page-cap tests via a fake pager, row building & CSV round trip, zero-match text, param parsing, helpers
pkg/server/server.go, pkg/server/server_test.go Tool registration, ValidToolNames
README.md Documented the tool

Token compatibility

Token conversations_threads
xoxc/xoxd Tested, works
xoxp Not registered (guard)
xoxb Not registered (guard)

Testing

  • make test / go vet pass; unit tests as listed above.
  • End-to-end through the MCP server over stdio with xoxc/xoxd on a live workspace: default call (5 unread threads in ~1 s, names/text/replies resolved), cursor continuation (strictly older threads, no overlap), filter=all since=1d include_replies=0, server-side channel_id filter, a channel without threads (friendly "no matching threads"), and validation errors for bad filter / since / unknown #channel.

🤖 Generated with Claude Code

panevesht and others added 2 commits August 18, 2026 17:01
…view)

There was no way to see which threads have unread replies:
conversations_unreads covers channels/DMs only, and thread unread state
lives in a separate per-thread cursor. Add a read-only tool backed by
Slack's subscriptions.thread.getView — the endpoint behind the "Threads"
view — which returns subscribed threads newest-activity-first with the
user's unread replies per thread.

- Edge call SubscriptionsThreadGetView (current_ts cursor, limit,
  optional server-side channel_id filter) with a typed response
  (threads with root_msg, latest_replies, unread_replies; counts;
  has_more; max_ts).
- Tool conversations_threads: filter=unread|all (default unread),
  optional channel_id (#name/@name resolved), since (1d/7d/2w/1m,
  default 30d, or "all"), limit (default 20, max 200), include_replies
  (default 3, max 20) and cursor. Slack serves this view 10 threads per
  page regardless of limit and has no server-side unread filter, so the
  handler pages newest-first, stops at the time bound / end of view /
  a 50-page cap, and returns the continuation cursor in the last row so
  no scan is ever lossy. Output is CSV: Channel, ChannelID, ThreadTs,
  RootUser, RootTime, RootText, ReplyCount, UnreadReplies,
  LatestReplyTime, LastRead, Replies (unread replies if any, else latest,
  rendered "time author: text" via the same user/text pipeline as the
  other conversation tools), Cursor.
- Registered only for browser session tokens (xoxc/xoxd), like the other
  edge-API tools; read-only annotation.
- Tests: HTTP-level edge test with a fixture mirroring the live
  response; collectThreads paging/filter/bound/cursor/page-cap tests with
  a fake pager; parameter parsing and helper tests; ValidToolNames.

Verified end-to-end through the MCP server over stdio against a live
workspace: default call, cursor continuation (strictly older threads,
no overlap), filter=all, channel filter, empty result, validation
errors.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…th-safe

Follow-ups from review of the new tool:

- Drop the pointer to "conversations_mark with thread_ts" from the tool
  description and README: that parameter does not exist on master (it is
  proposed separately), and mcp-go ignores unknown arguments, so an LLM
  following the hint would have marked the whole channel read.
- Compare Slack timestamps numerically (slackTsBefore) instead of as
  strings: a since window reaching before 2001 produced a 9-digit bound
  that stopped the scan immediately.
- Fall back to the requested channel_id when root_msg.channel is absent;
  skip entries without any timestamp instead of letting them end the
  scan; emit no cursor when the limit is reached on the last thread of
  the last page.
- Zero-match text now says why the scan stopped (time window, page cap,
  more available, end of view) and what was searched.
- isSlackTimestamp requires exactly six fractional digits (Slack's
  format); filter declared with mcp.Enum; docs spell out d/w/m units and
  that filter/channel_id/since must be repeated when continuing with a
  cursor.
- Row building and the zero-match message are pure functions with
  injected channel-name/render closures, and are unit-tested (CSV shape,
  unread-vs-latest replies, last N, ChannelID fallback, cursor placement,
  CSV round trip); collectThreads tests cover the numeric bound, the
  exhausted-view cursor and malformed entries.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
matt-temporal added a commit to matt-temporal/slack-mcp-server that referenced this pull request Oct 5, 2026
@Seylox

Seylox commented Oct 20, 2026

Copy link
Copy Markdown

Hey @panevesht, thanks so much for this! 🙌 Thread unreads were exactly the piece I was missing.

In case anyone's worried about the red check: it's not this PR. The integration tests run against master instead of the PR head (see #341), and they fail the same way on every PR right now.

If you can't wait for the merge, I've put these two commits on top of current upstream in my fork and will keep it in sync:
https://github.com/Seylox/slack-mcp-server/tree/master

Just clone it and run go build ./cmd/slack-mcp-server (go install won't work with the fork path). Happy to switch back once this lands. 😊

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