@jtalk22/slack-mcp 4.2.2 → 4.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -20,7 +20,7 @@ npx -y @jtalk22/slack-mcp --setup
20
20
 
21
21
  ## Why This Exists
22
22
 
23
- Slack's official MCP server requires a registered app, admin approval, and [doesn't work with Claude Code or GitHub Copilot](https://github.com/anthropics/claude-code/issues/30564) due to OAuth/DCR incompatibility. Screenshotting messages is not a workflow.
23
+ Slack's official MCP server is OAuth-first and can require a registered app, admin approval, or client compatibility workarounds. See the tracked [Claude Code/GitHub Copilot compatibility discussion](https://github.com/anthropics/claude-code/issues/30564). Screenshotting messages is not a workflow.
24
24
 
25
25
  This server uses your browser's session tokens instead. If you can see it in Slack, your AI agent can see it too. No app install, no scopes, no admin.
26
26
 
@@ -42,9 +42,9 @@ This server uses your browser's session tokens instead. If you can see it in Sla
42
42
  | Tools | Limited | **21** |
43
43
  | Visible to admins | Yes | **No — session-token transport** |
44
44
 
45
- ## Workflow Primitives (new in 4.2)
45
+ ## Workflow Primitives
46
46
 
47
- Save a workflow profile that binds a `workflow_kind` to channels + priority people + retention + cadence. Stored locally at `~/.slack-mcp-workflows.json`. The hosted brain at [mcp.revasserlabs.com](https://mcp.revasserlabs.com) reads these profiles and returns **structured JSON per workflow_kind** — downstream automation (Linear, Notion, status dashboards) consumes the JSON directly.
47
+ Introduced in 4.2. Save a workflow profile that binds a `workflow_kind` to channels + priority people + retention + cadence. Stored locally at `~/.slack-mcp-workflows.json`. The hosted brain at [mcp.revasserlabs.com](https://mcp.revasserlabs.com) reads these profiles and returns **structured JSON per workflow_kind** — downstream automation (Linear, Notion, status dashboards) consumes the JSON directly.
48
48
 
49
49
  | `workflow_kind` | Returns (structured JSON) |
50
50
  |---|---|
@@ -137,10 +137,10 @@ Or via CLI: `codex mcp add slack -- npx -y @jtalk22/slack-mcp`
137
137
  | `slack_token_status` | Token age, health, and cache stats | read-only |
138
138
  | `slack_refresh_tokens` | Auto-extract fresh tokens from Chrome | read-only* |
139
139
  | `slack_list_conversations` | List DMs and channels | read-only |
140
- | `slack_conversations_history` | Get messages from a channel or DM | read-only |
141
- | `slack_get_full_conversation` | Export full history with threads | read-only |
142
- | `slack_search_messages` | Search across workspace | read-only |
143
- | `slack_get_thread` | Get thread replies | read-only |
140
+ | `slack_conversations_history` | Get messages from a channel or DM | read-only |
141
+ | `slack_get_full_conversation` | Export full history with threads | read-only |
142
+ | `slack_search_messages` | Search across workspace | read-only |
143
+ | `slack_get_thread` | Get thread replies | read-only |
144
144
  | `slack_users_info` | Get user details | read-only |
145
145
  | `slack_list_users` | List workspace users (paginated, 500+) | read-only |
146
146
  | `slack_users_search` | Search users by name, display name, or email | read-only |
@@ -161,6 +161,8 @@ Or via CLI: `codex mcp add slack -- npx -y @jtalk22/slack-mcp`
161
161
 
162
162
  † Hosted stubs return a structured upgrade payload (`signup_url`, `free_tier_quota`, `pro_value_prop`) — no Slack write occurs from OSS. Activate the brain at [mcp.revasserlabs.com](https://mcp.revasserlabs.com) (free tier, no card).
163
163
 
164
+ ‡ Also accepts `include_rich_message_fields` to return attachments, blocks, files, reactions, and metadata — see [Rich Message Fields](#rich-message-fields).
165
+
164
166
  ## Install
165
167
 
166
168
  **Node.js 20+**
@@ -171,6 +173,8 @@ npx -y @jtalk22/slack-mcp --setup
171
173
 
172
174
  The setup wizard handles token extraction and validation.
173
175
 
176
+ After setup, have your client run `slack_health_check` — a workspace name in the response confirms you are connected.
177
+
174
178
  <details>
175
179
  <summary><strong>Claude Desktop (macOS)</strong></summary>
176
180
 
@@ -230,6 +234,8 @@ Add to `~/.claude.json`:
230
234
  }
231
235
  ```
232
236
 
237
+ Or via CLI: `claude mcp add slack -- npx -y @jtalk22/slack-mcp`
238
+
233
239
  </details>
234
240
 
235
241
  <details>
@@ -305,7 +311,8 @@ Session tokens (`xoxc-` + `xoxd-`) from your browser. If you can see it in Slack
305
311
 
306
312
  Tokens expire. The server notices before you do — proactive health monitoring, automatic refresh on macOS, warnings when tokens age out. File writes are atomic (temp file → chmod → rename) to prevent corruption. Concurrent refresh attempts are mutex-locked.
307
313
 
308
- ## What's New in 4.2.0
314
+ <details>
315
+ <summary><strong>What's New in 4.2.0</strong></summary>
309
316
 
310
317
  - **Workflow primitives** — `slack_workflow_save` + `slack_workflows` bind a `workflow_kind` (`incident_room`, `exec_brief`, `support_inbox`, `product_launch_watch`, `custom`) to channels, priority people, retention, and cadence. The hosted brain returns structured JSON per kind — `incident_room` returns `{incident_summary, timeline, open_risks, owner_gaps, next_actions}`, `exec_brief` returns `{summary, decisions, risks, asks, action_items}`. Downstream automation (Linear, Notion, dashboards) consumes the JSON directly.
311
318
  - **Discoverable upgrade stubs** — `slack_smart_search`, `slack_catch_me_up`, `slack_triage` appear in OSS as upgrade payloads pointing at the hosted brain. Response shape is `{signup_url, free_tier_quota, pro_value_prop}` — no interruptions, the AI routes the user cleanly.
@@ -315,6 +322,37 @@ Tokens expire. The server notices before you do — proactive health monitoring,
315
322
 
316
323
  Full release notes on [GitHub releases/latest](https://github.com/jtalk22/slack-mcp-server/releases/latest).
317
324
 
325
+ </details>
326
+
327
+ ## Rich Message Fields
328
+
329
+ Added in 4.4.0. The four read tools marked ‡ above accept `include_rich_message_fields: true`, which surfaces the parts of a message that live outside `text` — `attachments`, `blocks`, `files`, `reactions`, `metadata`, plus `subtype`/`bot_id`/`app_id` (automated/bot/app markers) and `team` (workspace id).
330
+
331
+ An attachment-only alert reads as empty without the flag:
332
+
333
+ ```json
334
+ { "ts": "1767368030.607599", "user": "incident-bot", "text": "" }
335
+ ```
336
+
337
+ With `include_rich_message_fields: true`, the content is surfaced:
338
+
339
+ ```json
340
+ {
341
+ "ts": "1767368030.607599",
342
+ "user": "incident-bot",
343
+ "text": "",
344
+ "subtype": "bot_message",
345
+ "bot_id": "B012345",
346
+ "attachments": [{ "title": "PagerDuty", "text": "P1 — API latency > 2s" }]
347
+ }
348
+ ```
349
+
350
+ Output shape only — no extra permissions. `blocks` can be large, so it is opt-in per call to keep client context lean. For the full developer payload inside `metadata`, also set `include_all_metadata: true` (an independent Slack flag).
351
+
352
+ `slack_search_messages` accepts the flag, but Slack's search API does not return rich fields on matches — read full content with `slack_conversations_history` or `slack_get_thread` on the match's channel and timestamp.
353
+
354
+ Patch by [@rvandam](https://github.com/rvandam) (#143).
355
+
318
356
  ## Hosted HTTP Mode
319
357
 
320
358
  For remote MCP endpoints (Cloudflare Worker, VPS, etc.):
@@ -331,7 +369,7 @@ Details: [docs/DEPLOYMENT-MODES.md](docs/DEPLOYMENT-MODES.md)
331
369
 
332
370
  ## Troubleshooting
333
371
 
334
- **Tokens expired:** Run `npx -y @jtalk22/slack-mcp --setup` or use `slack_refresh_tokens` (macOS).
372
+ **Tokens expired:** Run `npx -y @jtalk22/slack-mcp --setup` or use `slack_refresh_tokens` (macOS). To prevent silent expiration during long Claude-idle windows, set up the optional [token-refresh LaunchAgent](docs/SETUP.md#keep-tokens-fresh-while-claude-is-closed-macos-optional).
335
373
 
336
374
  **DMs not showing:** Use `slack_list_conversations` with `discover_dms=true`.
337
375
 
@@ -343,6 +381,7 @@ More: [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md)
343
381
 
344
382
  - [Setup Guide](docs/SETUP.md)
345
383
  - [API Reference](docs/API.md)
384
+ - [Roadmap](docs/ROADMAP.md)
346
385
  - [Architecture](docs/ARCHITECTURE.md)
347
386
  - [Deployment Modes](docs/DEPLOYMENT-MODES.md)
348
387
  - [Use Case Recipes](docs/USE_CASE_RECIPES.md)
@@ -371,4 +410,4 @@ Not affiliated with Slack Technologies, Inc. Uses browser session credentials
371
410
 
372
411
  ---
373
412
 
374
- Hosted version live at [mcp.revasserlabs.com](https://mcp.revasserlabs.com): Free tier (no card), $9/mo Pro, $49/mo Team flat, Ops from $199/mo. Hosted owns the AI brain (smart_search, catch_me_up, triage), the scheduled morning catch-up DM at 8am workspace time *(rolling out Q2 2026)*, permanent OAuth (no 2-week token rotation), 90-day Vectorize retention, and shared workflow profiles. The OSS package owns local stdio + all 16 read/write Slack tools + workflow profile primitives (slack_workflow_save, slack_workflows). The 3 paid stubs (slack_smart_search, slack_catch_me_up, slack_triage) appear in OSS as discoverable upgrade prompts.
413
+ Hosted version live at [mcp.revasserlabs.com](https://mcp.revasserlabs.com): Free tier (no card), $9/mo Pro, $49/mo Team flat, Ops from $199/mo. Hosted owns the AI brain (smart_search, catch_me_up, triage), the scheduled morning catch-up DM at 8am workspace time *(rolling out Q2 2026)*, permanent OAuth (no 2-week token rotation), 90-day Vectorize retention, and shared workflow profiles. The OSS package owns local stdio + the 16 Slack tools (12 read, 4 write) + workflow profile primitives (slack_workflow_save, slack_workflows). The 3 paid stubs (slack_smart_search, slack_catch_me_up, slack_triage) appear in OSS as discoverable upgrade prompts.
package/docs/API.md CHANGED
@@ -119,9 +119,11 @@ Get messages from a channel or DM.
119
119
  |------|------|---------|-------------|
120
120
  | channel_id | string | *required* | Channel or DM ID |
121
121
  | limit | number | 50 | Messages to fetch (max 100) |
122
- | oldest | string | - | Unix timestamp, get messages after |
123
- | latest | string | - | Unix timestamp, get messages before |
122
+ | oldest | string | - | Unix timestamp, get messages after; matching boundary timestamp is included |
123
+ | latest | string | - | Unix timestamp, get messages before; matching boundary timestamp is included |
124
124
  | resolve_users | boolean | true | Convert user IDs to names |
125
+ | include_rich_message_fields | boolean | false | Include Slack attachments, blocks, metadata, files, and reactions when present |
126
+ | include_all_metadata | boolean | false | Pass Slack's `include_all_metadata` option to `conversations.history` |
125
127
 
126
128
  **Returns:**
127
129
  ```json
@@ -136,12 +138,32 @@ Get messages from a channel or DM.
136
138
  "user_id": "U05GPEVH7J9",
137
139
  "text": "Hello!",
138
140
  "datetime": "2026-01-02T15:33:50.000Z",
139
- "has_thread": false
141
+ "has_thread": false,
142
+ "attachments": [
143
+ {
144
+ "text": "Additional message context"
145
+ }
146
+ ]
140
147
  }
141
148
  ]
142
149
  }
143
150
  ```
144
151
 
152
+ `include_rich_message_fields` changes this tool's **output shape only** — it surfaces fields Slack
153
+ already returns on the message object: `attachments`, `blocks`, `files`, `reactions`, `metadata`,
154
+ plus `subtype`, `bot_id`, `app_id` (markers that flag automated / bot / app messages) and `team`
155
+ (the workspace id, present on all messages). Especially `blocks` can be large, so it's opt-in per
156
+ call to keep MCP client context lean.
157
+
158
+ `include_all_metadata` is a **separate, independent** Slack request flag: its only effect is to add
159
+ the full `event_payload` inside a message's developer `metadata` (without it you still get
160
+ `metadata.event_type`). The two are orthogonal — set `include_rich_message_fields` to see `metadata`
161
+ in the output at all; additionally set `include_all_metadata` for the full payload.
162
+
163
+ Note: `slack_search_messages` matches are thin — Slack's search API does not return these rich
164
+ fields on matches (only `team`). To read rich content for a search hit, call
165
+ `slack_conversations_history` or `slack_get_thread` on the match's channel/ts.
166
+
145
167
  ---
146
168
 
147
169
  ### slack_get_full_conversation
@@ -152,10 +174,12 @@ Export full conversation with threads.
152
174
  | Name | Type | Default | Description |
153
175
  |------|------|---------|-------------|
154
176
  | channel_id | string | *required* | Channel or DM ID |
155
- | oldest | string | - | Unix timestamp start |
156
- | latest | string | - | Unix timestamp end |
177
+ | oldest | string | - | Unix timestamp start; matching boundary timestamp is included |
178
+ | latest | string | - | Unix timestamp end; matching boundary timestamp is included |
157
179
  | max_messages | number | 2000 | Max messages (up to 10000) |
158
180
  | include_threads | boolean | true | Fetch thread replies |
181
+ | include_rich_message_fields | boolean | false | Include Slack attachments, blocks, metadata, files, and reactions when present |
182
+ | include_all_metadata | boolean | false | Pass Slack's `include_all_metadata` option to `conversations.history` and `conversations.replies` |
159
183
  | output_file | string | - | Filename (saved to ~/.slack-mcp-exports/) |
160
184
 
161
185
  **Timestamps:**
@@ -188,6 +212,7 @@ Search messages across the workspace.
188
212
  |------|------|---------|-------------|
189
213
  | query | string | *required* | Search query |
190
214
  | count | number | 20 | Number of results (max 100) |
215
+ | include_rich_message_fields | boolean | false | Include Slack attachments, blocks, metadata, files, and reactions when present |
191
216
 
192
217
  **Query Syntax:**
193
218
  - `from:@username` - From specific user
@@ -249,6 +274,8 @@ Get all replies in a thread.
249
274
  |------|------|---------|-------------|
250
275
  | channel_id | string | *required* | Channel or DM ID |
251
276
  | thread_ts | string | *required* | Thread parent timestamp |
277
+ | include_rich_message_fields | boolean | false | Include Slack attachments, blocks, metadata, files, and reactions when present |
278
+ | include_all_metadata | boolean | false | Pass Slack's `include_all_metadata` option to `conversations.replies` |
252
279
 
253
280
  **Returns:**
254
281
  ```json
package/docs/SETUP.md CHANGED
@@ -138,6 +138,54 @@ slack_health_check
138
138
 
139
139
  You should see your username and team name.
140
140
 
141
+ ## Keep Tokens Fresh While Claude Is Closed (macOS, Optional)
142
+
143
+ The MCP server auto-refreshes tokens every 4 hours while Claude is running. But if you keep Claude closed for a couple weeks, tokens expire silently — your next session opens with `invalid_auth`.
144
+
145
+ A LaunchAgent fixes this. It runs token refresh twice a day regardless of whether Claude is open, as long as Chrome is running with a Slack tab somewhere.
146
+
147
+ Create `~/Library/LaunchAgents/com.yourname.slack-token-refresh.plist`:
148
+
149
+ ```xml
150
+ <?xml version="1.0" encoding="UTF-8"?>
151
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
152
+ <plist version="1.0">
153
+ <dict>
154
+ <key>Label</key>
155
+ <string>com.yourname.slack-token-refresh</string>
156
+ <key>ProgramArguments</key>
157
+ <array>
158
+ <string>/bin/bash</string>
159
+ <string>-c</string>
160
+ <string>export NVM_DIR="$HOME/.nvm" &amp;&amp; [ -s "$NVM_DIR/nvm.sh" ] &amp;&amp; \. "$NVM_DIR/nvm.sh" &amp;&amp; exec npx -y @jtalk22/slack-mcp --refresh-tokens</string>
161
+ </array>
162
+ <key>StartCalendarInterval</key>
163
+ <array>
164
+ <dict><key>Hour</key><integer>6</integer><key>Minute</key><integer>17</integer></dict>
165
+ <dict><key>Hour</key><integer>18</integer><key>Minute</key><integer>17</integer></dict>
166
+ </array>
167
+ <key>RunAtLoad</key><true/>
168
+ <key>StandardErrorPath</key><string>/tmp/slack-token-refresh.log</string>
169
+ </dict>
170
+ </plist>
171
+ ```
172
+
173
+ Load it:
174
+
175
+ ```bash
176
+ launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.yourname.slack-token-refresh.plist
177
+ ```
178
+
179
+ Check it ran:
180
+
181
+ ```bash
182
+ tail /tmp/slack-token-refresh.log
183
+ ```
184
+
185
+ **Why the `bash -c` wrapper:** LaunchAgents start without a TTY, so a bare `node` won't be on PATH if you use nvm. The wrapper sources `nvm.sh` first, then runs the refresher. (If you installed node via Homebrew, replace the whole inner command with `/opt/homebrew/bin/node /opt/homebrew/bin/npx -y @jtalk22/slack-mcp --refresh-tokens`.)
186
+
187
+ **Trade-off:** Chrome must be running for extraction to succeed. If it's not, the LaunchAgent logs "Failed to extract" and tokens stay where they are — which is fine for another 12 hours.
188
+
141
189
  ## Troubleshooting
142
190
 
143
191
  ### "No credentials found"
@@ -151,7 +199,7 @@ Tokens have expired. Run `npx -y @jtalk22/slack-mcp --doctor` and follow the sug
151
199
  ### MCP Server Not Loading
152
200
 
153
201
  1. Check `~/.claude.json` syntax
154
- 2. Verify the path to server.js is correct
202
+ 2. Verify JSON syntax in your client's MCP config
155
203
  3. Restart Claude Code
156
204
 
157
205
  ### Chrome Extraction Fails
package/lib/handlers.js CHANGED
@@ -20,6 +20,7 @@ import {
20
20
  listProfiles as workflowListProfiles,
21
21
  ALLOWED_WORKFLOW_KINDS_LIST,
22
22
  } from "./workflow-store.js";
23
+ import { withRichMessageFields } from "./rich-message-fields.js";
23
24
 
24
25
  // ============ Utilities ============
25
26
 
@@ -334,17 +335,19 @@ export async function handleListConversations(args) {
334
335
  */
335
336
  export async function handleConversationsHistory(args) {
336
337
  const resolveUsers = args.resolve_users !== false;
338
+ const includeRichMessageFields = parseBool(args.include_rich_message_fields);
337
339
  const result = await slackAPI("conversations.history", {
338
340
  channel: args.channel_id,
339
341
  limit: args.limit || 50,
340
342
  oldest: args.oldest,
341
343
  latest: args.latest,
342
- inclusive: true
344
+ inclusive: true,
345
+ include_all_metadata: parseBool(args.include_all_metadata)
343
346
  });
344
347
 
345
348
  const messages = await Promise.all((result.messages || []).map(async (msg) => {
346
349
  const userName = resolveUsers ? await resolveUser(msg.user) : msg.user;
347
- return {
350
+ return withRichMessageFields({
348
351
  ts: msg.ts,
349
352
  user: userName,
350
353
  user_id: msg.user,
@@ -352,7 +355,7 @@ export async function handleConversationsHistory(args) {
352
355
  datetime: formatTimestamp(msg.ts),
353
356
  has_thread: !!msg.thread_ts && msg.reply_count > 0,
354
357
  reply_count: msg.reply_count
355
- };
358
+ }, msg, includeRichMessageFields);
356
359
  }));
357
360
 
358
361
  return {
@@ -374,6 +377,7 @@ export async function handleConversationsHistory(args) {
374
377
  export async function handleGetFullConversation(args) {
375
378
  const maxMessages = Math.min(args.max_messages || 2000, 10000);
376
379
  const includeThreads = args.include_threads !== false;
380
+ const includeRichMessageFields = parseBool(args.include_rich_message_fields);
377
381
  const allMessages = [];
378
382
  let cursor;
379
383
  let hasMore = true;
@@ -386,36 +390,38 @@ export async function handleGetFullConversation(args) {
386
390
  oldest: args.oldest,
387
391
  latest: args.latest,
388
392
  cursor,
389
- inclusive: true
393
+ inclusive: true,
394
+ include_all_metadata: parseBool(args.include_all_metadata)
390
395
  });
391
396
 
392
397
  for (const msg of result.messages || []) {
393
398
  const userName = await resolveUser(msg.user);
394
- const message = {
399
+ const message = withRichMessageFields({
395
400
  ts: msg.ts,
396
401
  user: userName,
397
402
  user_id: msg.user,
398
403
  text: msg.text || "",
399
404
  datetime: formatTimestamp(msg.ts),
400
405
  replies: []
401
- };
406
+ }, msg, includeRichMessageFields);
402
407
 
403
408
  // Fetch thread replies if present
404
409
  if (includeThreads && msg.reply_count > 0) {
405
410
  try {
406
411
  const threadResult = await slackAPI("conversations.replies", {
407
412
  channel: args.channel_id,
408
- ts: msg.ts
413
+ ts: msg.ts,
414
+ include_all_metadata: parseBool(args.include_all_metadata)
409
415
  });
410
416
  // Skip first message (parent)
411
417
  for (const reply of (threadResult.messages || []).slice(1)) {
412
418
  const replyUserName = await resolveUser(reply.user);
413
- message.replies.push({
419
+ message.replies.push(withRichMessageFields({
414
420
  ts: reply.ts,
415
421
  user: replyUserName,
416
422
  text: reply.text || "",
417
423
  datetime: formatTimestamp(reply.ts)
418
- });
424
+ }, reply, includeRichMessageFields));
419
425
  }
420
426
  await sleep(50); // Rate limit
421
427
  } catch (e) {
@@ -470,6 +476,7 @@ export async function handleGetFullConversation(args) {
470
476
  * Search messages handler
471
477
  */
472
478
  export async function handleSearchMessages(args) {
479
+ const includeRichMessageFields = parseBool(args.include_rich_message_fields);
473
480
  const result = await slackAPI("search.messages", {
474
481
  query: args.query,
475
482
  count: args.count || 20,
@@ -477,7 +484,7 @@ export async function handleSearchMessages(args) {
477
484
  sort_dir: "desc"
478
485
  });
479
486
 
480
- const matches = await Promise.all((result.messages?.matches || []).map(async (m) => ({
487
+ const matches = await Promise.all((result.messages?.matches || []).map(async (m) => withRichMessageFields({
481
488
  ts: m.ts,
482
489
  channel: m.channel?.name || m.channel?.id,
483
490
  channel_id: m.channel?.id,
@@ -485,7 +492,7 @@ export async function handleSearchMessages(args) {
485
492
  text: m.text,
486
493
  datetime: formatTimestamp(m.ts),
487
494
  permalink: m.permalink
488
- })));
495
+ }, m, includeRichMessageFields)));
489
496
 
490
497
  return {
491
498
  content: [{
@@ -553,19 +560,21 @@ export async function handleSendMessage(args) {
553
560
  * Get thread handler
554
561
  */
555
562
  export async function handleGetThread(args) {
563
+ const includeRichMessageFields = parseBool(args.include_rich_message_fields);
556
564
  const result = await slackAPI("conversations.replies", {
557
565
  channel: args.channel_id,
558
- ts: args.thread_ts
566
+ ts: args.thread_ts,
567
+ include_all_metadata: parseBool(args.include_all_metadata)
559
568
  });
560
569
 
561
- const messages = await Promise.all((result.messages || []).map(async (msg) => ({
570
+ const messages = await Promise.all((result.messages || []).map(async (msg) => withRichMessageFields({
562
571
  ts: msg.ts,
563
572
  user: await resolveUser(msg.user),
564
573
  user_id: msg.user,
565
574
  text: msg.text || "",
566
575
  datetime: formatTimestamp(msg.ts),
567
576
  is_parent: msg.ts === args.thread_ts
568
- })));
577
+ }, msg, includeRichMessageFields)));
569
578
 
570
579
  return {
571
580
  content: [{
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Rich Slack message fields (opt-in).
3
+ *
4
+ * Slack stores a lot of message content outside the plain `text` field:
5
+ * attachments, Block Kit `blocks`, `metadata`, `files`, and `reactions` — plus
6
+ * `subtype`/`bot_id`/`app_id` (which flag bot / app messages) and `team` (the
7
+ * workspace id, present on every message). An attachment-only or block-only alert
8
+ * looks empty when you read `text` alone; surfacing these fields closes that blind spot.
9
+ *
10
+ * These are fields Slack already returns on the message object, so this is an
11
+ * output-shape opt-in only — independent of Slack's `include_all_metadata` request
12
+ * flag (which separately adds the `event_payload` inside `metadata`). They can be
13
+ * large (especially `blocks`), so callers opt in per request.
14
+ */
15
+
16
+ export const RICH_MESSAGE_KEYS = [
17
+ "attachments", "blocks", "metadata", "files", "reactions",
18
+ "subtype", "bot_id", "app_id", "team"
19
+ ];
20
+
21
+ /**
22
+ * Merge present rich fields from a raw Slack message onto a formatted output.
23
+ *
24
+ * Mutates and returns `output`. No-op when disabled or `msg` is falsy. Only keys
25
+ * actually present on `msg` are copied; `undefined` keys are skipped (a present
26
+ * falsy value such as `[]` is still copied).
27
+ */
28
+ export function withRichMessageFields(output, msg, includeRichMessageFields) {
29
+ if (!includeRichMessageFields || !msg) return output;
30
+
31
+ for (const key of RICH_MESSAGE_KEYS) {
32
+ if (msg[key] !== undefined) {
33
+ output[key] = msg[key];
34
+ }
35
+ }
36
+
37
+ return output;
38
+ }
package/lib/tools.js CHANGED
@@ -93,15 +93,23 @@ export const TOOLS = [
93
93
  },
94
94
  oldest: {
95
95
  type: "string",
96
- description: "Unix timestamp - get messages after this time"
96
+ description: "Unix timestamp - get messages after this time (boundary timestamp included)"
97
97
  },
98
98
  latest: {
99
99
  type: "string",
100
- description: "Unix timestamp - get messages before this time"
100
+ description: "Unix timestamp - get messages before this time (boundary timestamp included)"
101
101
  },
102
102
  resolve_users: {
103
103
  type: "boolean",
104
104
  description: "Convert user IDs to names (default true)"
105
+ },
106
+ include_rich_message_fields: {
107
+ type: "boolean",
108
+ description: "Include Slack message attachments, blocks, metadata, files, and reactions when present"
109
+ },
110
+ include_all_metadata: {
111
+ type: "boolean",
112
+ description: "Pass Slack's include_all_metadata option to conversations.history"
105
113
  }
106
114
  },
107
115
  required: ["channel_id"]
@@ -125,11 +133,11 @@ export const TOOLS = [
125
133
  },
126
134
  oldest: {
127
135
  type: "string",
128
- description: "Unix timestamp start (e.g., 1733011200 = Dec 1, 2025)"
136
+ description: "Unix timestamp start (e.g., 1733011200 = Dec 1, 2025; boundary timestamp included)"
129
137
  },
130
138
  latest: {
131
139
  type: "string",
132
- description: "Unix timestamp end"
140
+ description: "Unix timestamp end (boundary timestamp included)"
133
141
  },
134
142
  max_messages: {
135
143
  type: "number",
@@ -139,6 +147,14 @@ export const TOOLS = [
139
147
  type: "boolean",
140
148
  description: "Fetch thread replies (default true)"
141
149
  },
150
+ include_rich_message_fields: {
151
+ type: "boolean",
152
+ description: "Include Slack message attachments, blocks, metadata, files, and reactions when present"
153
+ },
154
+ include_all_metadata: {
155
+ type: "boolean",
156
+ description: "Pass Slack's include_all_metadata option to conversations.history and conversations.replies"
157
+ },
142
158
  output_file: {
143
159
  type: "string",
144
160
  description: "Filename to save export (saved to ~/.slack-mcp-exports/)"
@@ -166,6 +182,10 @@ export const TOOLS = [
166
182
  count: {
167
183
  type: "number",
168
184
  description: "Number of results (max 100, default 20)"
185
+ },
186
+ include_rich_message_fields: {
187
+ type: "boolean",
188
+ description: "Include Slack message attachments, blocks, metadata, files, and reactions when present"
169
189
  }
170
190
  },
171
191
  required: ["query"]
@@ -239,6 +259,14 @@ export const TOOLS = [
239
259
  thread_ts: {
240
260
  type: "string",
241
261
  description: "Thread parent message timestamp"
262
+ },
263
+ include_rich_message_fields: {
264
+ type: "boolean",
265
+ description: "Include Slack message attachments, blocks, metadata, files, and reactions when present"
266
+ },
267
+ include_all_metadata: {
268
+ type: "boolean",
269
+ description: "Pass Slack's include_all_metadata option to conversations.replies"
242
270
  }
243
271
  },
244
272
  required: ["channel_id", "thread_ts"]
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@jtalk22/slack-mcp",
3
3
  "mcpName": "io.github.jtalk22/slack-mcp-server",
4
- "version": "4.2.2",
4
+ "version": "4.4.0",
5
5
  "description": "Slack MCP without OAuth. 21 tools (16 read/write Slack + 2 workflow profile primitives + 3 hosted-brain upgrade stubs). Free OSS or hosted (free tier no card; $9/mo Pro = unlimited; morning DM rolling out Q2 2026).",
6
6
  "type": "module",
7
7
  "main": "src/server.js",
@@ -13,6 +13,7 @@
13
13
  "slack-mcp-setup": "scripts/setup-wizard.js"
14
14
  },
15
15
  "scripts": {
16
+ "test": "node --test",
16
17
  "start": "node src/server.js",
17
18
  "http": "node src/server-http.js",
18
19
  "web": "node src/web-server.js",
package/public/index.html CHANGED
@@ -190,6 +190,36 @@
190
190
  margin-bottom: 15px;
191
191
  }
192
192
  .modal input:focus { border-color: var(--teal); outline: none; }
193
+ .auth-options {
194
+ display: flex;
195
+ align-items: center;
196
+ gap: 8px;
197
+ margin: -4px 0 16px;
198
+ color: var(--muted);
199
+ font-size: 13px;
200
+ text-align: left;
201
+ }
202
+ .auth-options input { width: auto; margin: 0; }
203
+ .header-row {
204
+ display: flex;
205
+ justify-content: space-between;
206
+ align-items: flex-start;
207
+ gap: 12px;
208
+ margin-bottom: 20px;
209
+ }
210
+ .header-row h1 { margin-bottom: 0; }
211
+ .disconnect-btn {
212
+ display: none;
213
+ padding: 8px 12px;
214
+ background: transparent;
215
+ color: var(--muted);
216
+ border: 1px solid rgba(255, 255, 255, 0.18);
217
+ border-radius: 999px;
218
+ cursor: pointer;
219
+ white-space: nowrap;
220
+ }
221
+ .disconnect-btn.visible { display: inline-block; }
222
+ .disconnect-btn:hover { color: var(--text); border-color: rgba(255, 255, 255, 0.32); }
193
223
  .modal button {
194
224
  width: 100%;
195
225
  padding: 14px;
@@ -213,6 +243,10 @@
213
243
 
214
244
  @media (max-width: 640px) {
215
245
  body { padding: 12px; }
246
+ .header-row {
247
+ flex-direction: column;
248
+ align-items: stretch;
249
+ }
216
250
  h1 {
217
251
  display: flex;
218
252
  flex-direction: column;
@@ -247,14 +281,18 @@
247
281
  <div class="modal">
248
282
  <h2>Enter API Key</h2>
249
283
  <p>Copy the API key from the console where you ran<br><code>npm run web</code></p>
250
- <input type="text" id="modalApiKey" placeholder="smcp_xxxxxxxxxxxx" autofocus>
284
+ <input type="text" id="modalApiKey" placeholder="smcp_xxxxxxxxxxxx" autocomplete="off" autofocus>
285
+ <label class="auth-options"><input type="checkbox" id="rememberApiKey"> Remember this key on this device</label>
251
286
  <button onclick="submitApiKey()">Connect</button>
252
287
  <div id="modalError" class="error"></div>
253
288
  </div>
254
289
  </div>
255
290
 
256
291
  <div class="container">
257
- <h1>Slack Web API <span id="status" class="status"></span></h1>
292
+ <div class="header-row">
293
+ <h1>Slack Web API <span id="status" class="status"></span></h1>
294
+ <button type="button" id="disconnectButton" class="disconnect-btn" onclick="disconnect()">Disconnect</button>
295
+ </div>
258
296
  <div style="background:rgba(240,194,70,0.08);border:1px solid rgba(240,194,70,0.2);border-radius:8px;padding:8px 14px;margin-bottom:16px;display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:8px;font-size:13px;color:#d4c48a">
259
297
  <span>Hosted tiers live — <strong style="color:#f0c246">managed MCP endpoint, OAuth bridge for Claude.ai, encrypted storage</strong></span>
260
298
  <a href="https://mcp.revasserlabs.com" style="color:#f0c246;font-weight:600;text-decoration:none;white-space:nowrap" target="_blank">See tiers &rarr;</a>
@@ -290,18 +328,33 @@
290
328
  let apiKey = null;
291
329
  let currentChannel = null;
292
330
 
293
- // Initialize: check URL param, then localStorage
331
+ function clearStoredApiKey() {
332
+ sessionStorage.removeItem('slackApiKey');
333
+ localStorage.removeItem('slackApiKey');
334
+ }
335
+
336
+ function rememberStoredApiKey(key) {
337
+ sessionStorage.removeItem('slackApiKey');
338
+ localStorage.setItem('slackApiKey', key);
339
+ }
340
+
341
+ function storeSessionApiKey(key) {
342
+ localStorage.removeItem('slackApiKey');
343
+ sessionStorage.setItem('slackApiKey', key);
344
+ }
345
+
346
+ // Initialize: check URL param, session storage, then remembered local storage
294
347
  (function init() {
295
348
  const params = new URLSearchParams(window.location.search);
296
349
  const keyFromUrl = params.get('key');
297
350
 
298
351
  if (keyFromUrl) {
299
- // Save key from magic link and strip from URL
352
+ // Save magic-link keys for this tab only and strip them from the URL.
300
353
  apiKey = keyFromUrl;
301
- localStorage.setItem('slackApiKey', apiKey);
354
+ storeSessionApiKey(apiKey);
302
355
  history.replaceState({}, '', window.location.pathname);
303
356
  } else {
304
- apiKey = localStorage.getItem('slackApiKey');
357
+ apiKey = sessionStorage.getItem('slackApiKey') || localStorage.getItem('slackApiKey');
305
358
  }
306
359
 
307
360
  if (apiKey) {
@@ -329,7 +382,11 @@
329
382
  return;
330
383
  }
331
384
  apiKey = input;
332
- localStorage.setItem('slackApiKey', apiKey);
385
+ if (document.getElementById('rememberApiKey').checked) {
386
+ rememberStoredApiKey(apiKey);
387
+ } else {
388
+ storeSessionApiKey(apiKey);
389
+ }
333
390
  hideModal();
334
391
  connect();
335
392
  }
@@ -346,8 +403,9 @@
346
403
  });
347
404
  if (res.status === 401 || res.status === 403) {
348
405
  // Auth failed - clear stored key and show modal
349
- localStorage.removeItem('slackApiKey');
406
+ clearStoredApiKey();
350
407
  apiKey = null;
408
+ document.getElementById('disconnectButton').classList.remove('visible');
351
409
  showModal('Invalid or expired API key. Check console for current key.');
352
410
  throw new Error('Authentication failed');
353
411
  }
@@ -363,6 +421,7 @@
363
421
  const health = await api('/health');
364
422
  status.textContent = '● ' + health.user + ' @ ' + health.team;
365
423
  status.className = 'status ok';
424
+ document.getElementById('disconnectButton').classList.add('visible');
366
425
  loadConversations('im,mpim');
367
426
  } catch (e) {
368
427
  if (e.message !== 'Authentication failed') {
@@ -371,6 +430,18 @@
371
430
  }
372
431
  }
373
432
  }
433
+ function disconnect() {
434
+ clearStoredApiKey();
435
+ apiKey = null;
436
+ currentChannel = null;
437
+ document.getElementById('disconnectButton').classList.remove('visible');
438
+ document.getElementById('status').textContent = '';
439
+ document.getElementById('conversationList').innerHTML = '<li class="loading">Enter API key to connect</li>';
440
+ document.getElementById('channelName').textContent = 'Select a conversation';
441
+ document.getElementById('messages').innerHTML = '<div class="loading">Messages will appear here</div>';
442
+ showModal('Disconnected. Enter an API key to reconnect.');
443
+ }
444
+
374
445
  async function loadConversations(types, sourceButton = null) {
375
446
  const list = document.getElementById('conversationList');
376
447
  list.innerHTML = '<li class="loading">Loading...</li>';
package/public/share.html CHANGED
@@ -5,9 +5,10 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
6
  <title>Slack MCP Server</title>
7
7
  <meta name="description" content="No OAuth. No admin. 21 Slack tools for Claude, Cursor, Copilot, Gemini, and any MCP client. One command: npx -y @jtalk22/slack-mcp --setup">
8
+ <link rel="canonical" href="https://jtalk22.github.io/slack-mcp-server/public/share.html">
8
9
  <meta property="og:type" content="website">
9
10
  <meta property="og:title" content="Slack MCP Server — No OAuth, no admin, just your browser session">
10
- <meta property="og:description" content="Slack's official MCP needs OAuth + admin. This one uses your browser session. 21 tools, works with Claude, Cursor, Copilot, Gemini.">
11
+ <meta property="og:description" content="OAuth-free Slack MCP using your browser session. 21 tools, works with Claude, Cursor, Copilot, Gemini.">
11
12
  <meta property="og:url" content="https://jtalk22.github.io/slack-mcp-server/public/share.html">
12
13
  <meta property="og:image" content="https://jtalk22.github.io/slack-mcp-server/docs/images/social-preview-v3.png">
13
14
  <meta property="og:image:width" content="1280">
package/server.json CHANGED
@@ -17,7 +17,7 @@
17
17
  "url": "https://github.com/jtalk22/slack-mcp-server",
18
18
  "source": "github"
19
19
  },
20
- "version": "4.2.2",
20
+ "version": "4.4.0",
21
21
  "remotes": [
22
22
  {
23
23
  "type": "streamable-http",
@@ -28,7 +28,7 @@
28
28
  {
29
29
  "registryType": "npm",
30
30
  "identifier": "@jtalk22/slack-mcp",
31
- "version": "4.2.2",
31
+ "version": "4.4.0",
32
32
  "transport": {
33
33
  "type": "stdio"
34
34
  },
package/src/cli.js CHANGED
@@ -37,6 +37,9 @@ if (firstArg === "web") {
37
37
  } else if (firstArg === "--apply-template" || firstArg === "apply-template") {
38
38
  scriptPath = join(__dirname, "../scripts/apply-template.js");
39
39
  scriptArgs = args.slice(1);
40
+ } else if (firstArg === "--refresh-tokens" || firstArg === "refresh-tokens") {
41
+ scriptPath = join(__dirname, "../scripts/token-cli.js");
42
+ scriptArgs = ["auto"];
40
43
  } else if (WIZARD_ARGS.has(firstArg)) {
41
44
  scriptPath = join(__dirname, "../scripts/setup-wizard.js");
42
45
  scriptArgs = args;