@jtalk22/slack-mcp 3.1.0 → 3.2.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.
Files changed (65) hide show
  1. package/README.md +45 -13
  2. package/docs/SETUP.md +64 -29
  3. package/docs/TROUBLESHOOTING.md +28 -0
  4. package/lib/handlers.js +156 -0
  5. package/lib/slack-client.js +11 -3
  6. package/lib/token-store.js +6 -5
  7. package/lib/tools.js +131 -0
  8. package/package.json +15 -8
  9. package/public/index.html +10 -6
  10. package/public/share.html +6 -5
  11. package/scripts/setup-wizard.js +1 -1
  12. package/server.json +8 -2
  13. package/src/server-http.js +16 -1
  14. package/src/server.js +31 -7
  15. package/src/web-server.js +117 -4
  16. package/docs/CLOUDFLARE-BROWSER-TOOLKIT.md +0 -67
  17. package/docs/COMMUNICATION-STYLE.md +0 -66
  18. package/docs/COMPATIBILITY.md +0 -19
  19. package/docs/DEPLOYMENT-MODES.md +0 -55
  20. package/docs/HN-LAUNCH.md +0 -72
  21. package/docs/INDEX.md +0 -41
  22. package/docs/INSTALL-PROOF.md +0 -18
  23. package/docs/LAUNCH-COPY-v3.0.0.md +0 -101
  24. package/docs/LAUNCH-MATRIX.md +0 -22
  25. package/docs/LAUNCH-OPS.md +0 -71
  26. package/docs/RELEASE-HEALTH.md +0 -77
  27. package/docs/SUPPORT-BOUNDARIES.md +0 -49
  28. package/docs/USE_CASE_RECIPES.md +0 -69
  29. package/docs/WEB-API.md +0 -303
  30. package/docs/images/demo-channel-messages.png +0 -0
  31. package/docs/images/demo-channels.png +0 -0
  32. package/docs/images/demo-claude-mobile-360x800.png +0 -0
  33. package/docs/images/demo-claude-mobile-390x844.png +0 -0
  34. package/docs/images/demo-claude-mobile-poster.png +0 -0
  35. package/docs/images/demo-main-mobile-360x800.png +0 -0
  36. package/docs/images/demo-main-mobile-390x844.png +0 -0
  37. package/docs/images/demo-main.png +0 -0
  38. package/docs/images/demo-messages.png +0 -0
  39. package/docs/images/demo-poster.png +0 -0
  40. package/docs/images/demo-sidebar.png +0 -0
  41. package/docs/images/diagram-oauth-comparison.svg +0 -80
  42. package/docs/images/diagram-session-flow.svg +0 -105
  43. package/docs/images/social-preview-v3.png +0 -0
  44. package/docs/images/web-api-mobile-360x800.png +0 -0
  45. package/docs/images/web-api-mobile-390x844.png +0 -0
  46. package/public/demo-claude.html +0 -1974
  47. package/public/demo-video.html +0 -244
  48. package/public/demo.html +0 -1196
  49. package/scripts/build-mobile-demo.js +0 -168
  50. package/scripts/build-release-health-delta.js +0 -201
  51. package/scripts/build-social-preview.js +0 -189
  52. package/scripts/capture-screenshots.js +0 -152
  53. package/scripts/check-owner-attribution.sh +0 -131
  54. package/scripts/check-public-language.sh +0 -26
  55. package/scripts/check-version-parity.js +0 -218
  56. package/scripts/cloudflare-browser-tool.js +0 -237
  57. package/scripts/collect-release-health.js +0 -162
  58. package/scripts/impact-push-v3.js +0 -781
  59. package/scripts/record-demo.js +0 -163
  60. package/scripts/release-preflight.js +0 -247
  61. package/scripts/setup-git-hooks.sh +0 -15
  62. package/scripts/update-github-social-preview.js +0 -208
  63. package/scripts/verify-core.js +0 -159
  64. package/scripts/verify-install-flow.js +0 -193
  65. package/scripts/verify-web.js +0 -273
package/README.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Slack MCP Server
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/@jtalk22/slack-mcp)](https://www.npmjs.com/package/@jtalk22/slack-mcp)
4
+ [![npm downloads](https://img.shields.io/npm/dm/@jtalk22/slack-mcp)](https://www.npmjs.com/package/@jtalk22/slack-mcp)
5
+ [![MCP Registry](https://img.shields.io/badge/MCP_Registry-v3.2.0-blue)](https://registry.modelcontextprotocol.io)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT)
7
+
3
8
  Session-based Slack MCP for Claude and MCP clients. Local-first `stdio`/`web` with secure-default hosted HTTP in v3.
4
9
 
5
10
  ## Install + Verify
@@ -17,30 +22,50 @@ npx -y @jtalk22/slack-mcp@latest --status
17
22
 
18
23
  Motion proof: [20-second mobile clip](https://jtalk22.github.io/slack-mcp-server/docs/videos/demo-claude-mobile-20s.mp4) · [Live demo walkthrough](https://jtalk22.github.io/slack-mcp-server/public/demo-video.html) · [Share card](https://jtalk22.github.io/slack-mcp-server/public/share.html)
19
24
 
20
- Hosted migration note: `v3.0.0` keeps local `stdio`/`web` flows unchanged; hosted `/mcp` requires `SLACK_MCP_HTTP_AUTH_TOKEN` and `SLACK_MCP_HTTP_ALLOWED_ORIGINS`.
25
+ Hosted migration note: `v3.1.0` keeps local `stdio`/`web` flows unchanged; hosted `/mcp` requires `SLACK_MCP_HTTP_AUTH_TOKEN` and `SLACK_MCP_HTTP_ALLOWED_ORIGINS`.
21
26
 
22
27
  Maintainer/operator: `jtalk22` (`james@revasser.nyc`)
23
- Release: [`v3.1.0`](https://github.com/jtalk22/slack-mcp-server/releases/tag/v3.1.0) · Notes: [v3.1.0 notes](https://github.com/jtalk22/slack-mcp-server/blob/main/.github/v3.1.0-release-notes.md) · Support: [deployment intake](https://github.com/jtalk22/slack-mcp-server/issues/new?template=deployment-intake.md)
28
+ Release: [`v3.2.0`](https://github.com/jtalk22/slack-mcp-server/releases/tag/v3.2.0) · Notes: [v3.2.0 notes](https://github.com/jtalk22/slack-mcp-server/blob/main/.github/v3.2.0-release-notes.md) · Support: [deployment intake](https://github.com/jtalk22/slack-mcp-server/issues/new?template=deployment-intake.md)
24
29
 
25
30
  If this saved you setup time, consider starring the repo. Maintenance support: [GitHub Sponsors](https://github.com/sponsors/jtalk22) · [Ko-fi](https://ko-fi.com/jtalk22) · [Buy Me a Coffee](https://buymeacoffee.com/jtalk22)
26
31
 
27
- ## v3.0.0 at a Glance
32
+ ## v3.2.0 at a Glance
28
33
 
29
- - Hosted HTTP `/mcp` now requires bearer auth by default (`SLACK_MCP_HTTP_AUTH_TOKEN`).
30
- - Hosted HTTP CORS now uses explicit allowlisting (`SLACK_MCP_HTTP_ALLOWED_ORIGINS`).
31
- - Local-first paths (`stdio`, `web`) stay compatible.
32
- - MCP tool names stay stable (no renames/removals).
34
+ - **16 tools** added reactions, mark-as-read, unread inbox, and user search
35
+ - All three transports (stdio, web, hosted HTTP) have full tool parity
36
+ - No MCP tool renames or removals — fully backwards compatible
33
37
 
34
38
  ## Slack MCP Cloud
35
39
 
36
- Skip all setup. One URL, 13 tools, encrypted token storage, managed on Cloudflare edge.
40
+ **No token management. No Docker. No Chrome extensions. One URL and you're connected.**
41
+
42
+ Skip all local setup — paste one URL into Claude and get 16 Slack tools running in under 60 seconds. Encrypted token storage on Cloudflare's global edge (300+ PoPs). The only cloud-hosted session-based Slack MCP on the market.
37
43
 
38
44
  | Plan | Price | Includes |
39
45
  |------|-------|----------|
40
- | Solo | $19/mo | 10 standard tools, encrypted storage, 5K requests/mo |
41
- | Team | $49/mo | 13 tools (incl. compound intelligence), 3 workspaces, 25K requests/mo |
46
+ | Solo | $19/mo | 16 standard tools, AES-256-GCM encrypted storage, 5K requests/mo |
47
+ | Team | $49/mo | 16 standard + 3 AI compound tools, 3 workspaces, 25K requests/mo |
48
+
49
+ [Get Your API Key](https://jtalk22.github.io/slack-mcp-server/cloud.html) — live in 60 seconds. [Privacy Policy](https://jtalk22.github.io/slack-mcp-server/privacy.html).
50
+
51
+ ### Cloud Usage Examples
52
+
53
+ Once configured, ask Claude naturally:
54
+
55
+ 1. **Catch up on a channel** — *"Summarize what happened in #engineering this week"*
56
+ Uses `slack_list_conversations` → `slack_conversations_history` → Claude synthesis.
57
+
58
+ 2. **Find a decision** — *"What did the team decide about the API migration?"*
59
+ Uses `slack_search_messages` with query `"API migration"`, then `slack_get_thread` to pull full context.
60
+
61
+ 3. **Export a conversation** — *"Export my full DM history with Sarah including threads"*
62
+ Uses `slack_list_conversations` to resolve Sarah's DM ID → `slack_get_full_conversation` with `include_threads: true`.
63
+
64
+ 4. **Send a standup update** — *"Post my standup in #daily-standup: shipped auth refactor, reviewing PR #42 today"*
65
+ Uses `slack_send_message` to the target channel.
42
66
 
43
- [Get started](https://jtalk22.github.io/slack-mcp-server/cloud.html) — no Docker, no tokens, no admin approval.
67
+ 5. **AI-powered action items** *(Team plan)**"What action items came out of #product-sync today?"*
68
+ Uses `slack_extract_action_items` to identify owners, deadlines, and commitments.
44
69
 
45
70
  ## 60-Second Hosted Migration
46
71
 
@@ -99,7 +124,9 @@ Instead of authenticating as a bot, this server leverages your existing Chrome s
99
124
  - **Full Export** - Conversations with threads and resolved usernames
100
125
  - **Search** - Query across your entire workspace
101
126
  - **Send Messages** - DMs or channels, with thread support
102
- - **User Directory** - List and search 500+ users with pagination
127
+ - **Reactions** - Add or remove emoji reactions on any message
128
+ - **Unreads** - Priority-sorted unread inbox across all conversations
129
+ - **User Directory** - List, search, and look up 500+ users with pagination
103
130
 
104
131
  ### Stability
105
132
  - **Auto Token Refresh** - Extracts fresh tokens from Chrome automatically *(macOS only)*
@@ -122,6 +149,11 @@ Instead of authenticating as a bot, this server leverages your existing Chrome s
122
149
  | `slack_get_thread` | Get thread replies |
123
150
  | `slack_users_info` | Get user details |
124
151
  | `slack_list_users` | List workspace users (paginated, 500+ supported) |
152
+ | `slack_add_reaction` | Add an emoji reaction to a message |
153
+ | `slack_remove_reaction` | Remove an emoji reaction from a message |
154
+ | `slack_conversations_mark` | Mark a conversation as read up to a timestamp |
155
+ | `slack_conversations_unreads` | Get channels/DMs with unread messages, priority-sorted |
156
+ | `slack_users_search` | Search workspace users by name, display name, or email |
125
157
 
126
158
  ---
127
159
 
@@ -369,7 +401,7 @@ npm run web
369
401
 
370
402
  ```
371
403
  ════════════════════════════════════════════════════════════
372
- Slack Web API Server v3.0.0
404
+ Slack Web API Server v3.2.0
373
405
  ════════════════════════════════════════════════════════════
374
406
 
375
407
  Dashboard: http://localhost:3000/?key=smcp_xxxxxxxxxxxx
package/docs/SETUP.md CHANGED
@@ -1,29 +1,68 @@
1
1
  # Setup Guide
2
2
 
3
- ## Prerequisites
3
+ ## Cloud (Fastest — No Local Setup)
4
+
5
+ If you want to skip local setup entirely, use **Slack MCP Cloud**:
6
+
7
+ 1. Go to [cloud.html](https://jtalk22.github.io/slack-mcp-server/cloud.html) and purchase a plan ($19/mo Solo, $49/mo Team)
8
+ 2. After checkout, you'll receive an API key and ready-to-paste config for Claude Desktop / Claude Code
9
+ 3. No Node.js, no Docker, no token management — one URL, 16 tools
10
+
11
+ **Claude Desktop config (Cloud):**
12
+ ```json
13
+ {
14
+ "mcpServers": {
15
+ "slack": {
16
+ "url": "https://mcp.revasserlabs.com/oauth/mcp"
17
+ }
18
+ }
19
+ }
20
+ ```
21
+
22
+ **Claude Code config (Cloud):**
23
+ ```json
24
+ {
25
+ "mcpServers": {
26
+ "slack": {
27
+ "type": "sse",
28
+ "url": "https://mcp.revasserlabs.com/mcp",
29
+ "headers": {
30
+ "Authorization": "Bearer YOUR_API_KEY"
31
+ }
32
+ }
33
+ }
34
+ }
35
+ ```
36
+
37
+ If you prefer self-hosting (free), continue below.
38
+
39
+ ---
40
+
41
+ ## Self-Hosted Setup
42
+
43
+ ### Prerequisites
4
44
 
5
45
  - Node.js 20+
6
- - Google Chrome (for token extraction)
46
+ - Google Chrome (for token extraction on macOS)
7
47
  - macOS (for Keychain storage - other platforms use file storage only)
8
48
 
9
- ## Installation
10
-
11
- ### 1. Clone or Copy the Project
49
+ ### 1. Install via npm (Recommended)
12
50
 
13
51
  ```bash
14
- cd ~
15
- git clone https://github.com/jtalk22/slack-mcp-server.git
16
- # or if already exists:
17
- cd ~/slack-mcp-server
52
+ npm install -g @jtalk22/slack-mcp
53
+ # or use npx (no install):
54
+ npx -y @jtalk22/slack-mcp --version
18
55
  ```
19
56
 
20
- ### 2. Install Dependencies
57
+ ### 1b. Or Clone the Repository
21
58
 
22
59
  ```bash
60
+ git clone https://github.com/jtalk22/slack-mcp-server.git
61
+ cd slack-mcp-server
23
62
  npm install
24
63
  ```
25
64
 
26
- ### 2.5 Verify Install Path in a Clean Directory
65
+ ### 2. Verify Installation
27
66
 
28
67
  ```bash
29
68
  tmpdir="$(mktemp -d)"
@@ -76,45 +115,41 @@ npm run tokens:auto
76
115
  ```
77
116
  And paste both values when prompted.
78
117
 
79
- ### 4. Configure Claude Desktop (GUI App)
118
+ ### 4. Configure Claude Desktop
80
119
 
81
- Edit `~/Library/Application Support/Claude/claude_desktop_config.json`:
120
+ **macOS:** Edit `~/Library/Application Support/Claude/claude_desktop_config.json`
121
+ **Windows:** Edit `%APPDATA%\Claude\claude_desktop_config.json`
82
122
 
83
123
  ```json
84
124
  {
85
125
  "mcpServers": {
86
126
  "slack": {
87
- "command": "/opt/homebrew/bin/node",
88
- "args": ["/Users/YOUR_USERNAME/slack-mcp-server/src/server.js"],
89
- "env": {
90
- "SLACK_TOKEN": "xoxc-your-token-here",
91
- "SLACK_COOKIE": "xoxd-your-cookie-here",
92
- "PATH": "/opt/homebrew/bin:/usr/bin:/bin"
93
- }
127
+ "command": "npx",
128
+ "args": ["-y", "@jtalk22/slack-mcp"]
94
129
  }
95
130
  }
96
131
  }
97
132
  ```
98
133
 
99
- **Important:**
100
- - Replace `YOUR_USERNAME` with your actual username
101
- - Copy tokens from `~/.slack-mcp-tokens.json` into the env section
102
- - Fully restart Claude Desktop (Cmd+Q, then reopen)
134
+ Fully restart Claude Desktop (Cmd+Q on macOS, then reopen).
103
135
 
104
- **Verify it's working:** Check `~/Library/Logs/Claude/mcp-server-slack.log`
136
+ **Verify it's working:** Check `~/Library/Logs/Claude/mcp-server-slack.log` (macOS)
105
137
 
106
138
  ### 5. Configure Claude Code (CLI)
107
139
 
108
- Edit `~/.claude.json` and add under `mcpServers`:
140
+ ```bash
141
+ claude mcp add slack npx -y @jtalk22/slack-mcp
142
+ ```
143
+
144
+ Or manually edit `~/.claude.json`:
109
145
 
110
146
  ```json
111
147
  {
112
148
  "mcpServers": {
113
149
  "slack": {
114
150
  "type": "stdio",
115
- "command": "node",
116
- "args": ["/Users/YOUR_USERNAME/slack-mcp-server/src/server.js"],
117
- "env": {}
151
+ "command": "npx",
152
+ "args": ["-y", "@jtalk22/slack-mcp"]
118
153
  }
119
154
  }
120
155
  }
@@ -29,6 +29,34 @@ If `--version` fails here, the issue is install/runtime path, not Slack credenti
29
29
 
30
30
  ---
31
31
 
32
+ ## Cloud Issues
33
+
34
+ ### API Key Not Working
35
+
36
+ **Symptom:** `401 Unauthorized` or `403 Forbidden` when using Cloud endpoint.
37
+
38
+ **Solutions:**
39
+ 1. Verify your API key starts with `stmh_` (team) or `smsh_` (solo)
40
+ 2. Check the key hasn't been revoked — contact james@revasser.nyc for key issues
41
+ 3. Ensure you're using the correct endpoint: `https://mcp.revasserlabs.com/mcp`
42
+
43
+ ### Cloud Tools Not Available
44
+
45
+ **Symptom:** Only seeing fewer tools than expected.
46
+
47
+ **Cause:** AI compound tools (`slack_channel_summary`, `slack_extract_action_items`, `slack_find_decisions`) are Team plan only ($49/mo).
48
+
49
+ **Solution:** Upgrade to Team plan for AI compound tools, or use the standard 16 tools available on all plans.
50
+
51
+ ### Cloud Endpoint Health Check
52
+
53
+ ```bash
54
+ curl -s https://mcp.revasserlabs.com/health | jq .
55
+ # Expected: {"status":"healthy","server":"slack-mcp-hosted","version":"0.5.0"}
56
+ ```
57
+
58
+ ---
59
+
32
60
  ## DMs Not Showing Up
33
61
 
34
62
  **Symptom:** `slack_list_conversations` returns channels but no DMs.
package/lib/handlers.js CHANGED
@@ -617,3 +617,159 @@ export async function handleListUsers(args) {
617
617
  }]
618
618
  };
619
619
  }
620
+
621
+ /**
622
+ * Add reaction handler
623
+ */
624
+ export async function handleAddReaction(args) {
625
+ await slackAPI("reactions.add", {
626
+ channel: args.channel_id,
627
+ timestamp: args.timestamp,
628
+ name: args.reaction
629
+ });
630
+
631
+ return {
632
+ content: [{
633
+ type: "text",
634
+ text: JSON.stringify({
635
+ status: "added",
636
+ channel: args.channel_id,
637
+ timestamp: args.timestamp,
638
+ reaction: args.reaction
639
+ }, null, 2)
640
+ }]
641
+ };
642
+ }
643
+
644
+ /**
645
+ * Remove reaction handler
646
+ */
647
+ export async function handleRemoveReaction(args) {
648
+ await slackAPI("reactions.remove", {
649
+ channel: args.channel_id,
650
+ timestamp: args.timestamp,
651
+ name: args.reaction
652
+ });
653
+
654
+ return {
655
+ content: [{
656
+ type: "text",
657
+ text: JSON.stringify({
658
+ status: "removed",
659
+ channel: args.channel_id,
660
+ timestamp: args.timestamp,
661
+ reaction: args.reaction
662
+ }, null, 2)
663
+ }]
664
+ };
665
+ }
666
+
667
+ /**
668
+ * Mark conversation as read handler
669
+ */
670
+ export async function handleConversationsMark(args) {
671
+ await slackAPI("conversations.mark", {
672
+ channel: args.channel_id,
673
+ ts: args.timestamp
674
+ });
675
+
676
+ return asMcpJson({
677
+ status: "marked",
678
+ channel: args.channel_id,
679
+ read_up_to: args.timestamp
680
+ });
681
+ }
682
+
683
+ /**
684
+ * Unread conversations handler - returns channels/DMs with unread messages
685
+ */
686
+ export async function handleConversationsUnreads(args) {
687
+ const types = args.types || "im,mpim,public_channel,private_channel";
688
+ const limit = args.limit || 50;
689
+
690
+ const result = await slackAPI("conversations.list", {
691
+ types,
692
+ limit: 200,
693
+ exclude_archived: true
694
+ });
695
+
696
+ // Filter to conversations with unreads and resolve names
697
+ const unreads = [];
698
+ for (const c of (result.channels || [])) {
699
+ const unreadCount = c.unread_count_display || c.unread_count || 0;
700
+ if (unreadCount === 0) continue;
701
+
702
+ let displayName = c.name;
703
+ if (c.is_im && c.user) {
704
+ displayName = await resolveUser(c.user);
705
+ }
706
+
707
+ unreads.push({
708
+ id: c.id,
709
+ name: displayName,
710
+ type: c.is_im ? "dm" : c.is_mpim ? "group_dm" : c.is_private ? "private_channel" : "public_channel",
711
+ unread_count: unreadCount,
712
+ latest_ts: c.latest?.ts || null
713
+ });
714
+ }
715
+
716
+ // Sort by unread count descending
717
+ unreads.sort((a, b) => b.unread_count - a.unread_count);
718
+
719
+ return asMcpJson({
720
+ total_unread_conversations: unreads.length,
721
+ conversations: unreads.slice(0, limit)
722
+ });
723
+ }
724
+
725
+ /**
726
+ * Search users handler - client-side filter on users.list
727
+ */
728
+ export async function handleUsersSearch(args) {
729
+ const query = (args.query || "").toLowerCase();
730
+ const limit = args.limit || 20;
731
+
732
+ // Fetch all users (paginated)
733
+ const allUsers = [];
734
+ let cursor;
735
+
736
+ do {
737
+ const result = await slackAPI("users.list", {
738
+ limit: 200,
739
+ cursor
740
+ });
741
+
742
+ for (const u of (result.members || [])) {
743
+ if (u.deleted || u.is_bot || u.id === "USLACKBOT") continue;
744
+
745
+ const searchFields = [
746
+ u.name,
747
+ u.real_name,
748
+ u.profile?.display_name,
749
+ u.profile?.email
750
+ ].filter(Boolean).map(s => s.toLowerCase());
751
+
752
+ if (searchFields.some(f => f.includes(query))) {
753
+ allUsers.push({
754
+ id: u.id,
755
+ name: u.name,
756
+ real_name: u.real_name,
757
+ display_name: u.profile?.display_name,
758
+ email: u.profile?.email,
759
+ title: u.profile?.title,
760
+ is_admin: u.is_admin
761
+ });
762
+ }
763
+ }
764
+
765
+ cursor = result.response_metadata?.next_cursor;
766
+ if (cursor) await sleep(100);
767
+ } while (cursor && allUsers.length < 500);
768
+
769
+ return asMcpJson({
770
+ query: args.query,
771
+ count: Math.min(allUsers.length, limit),
772
+ total_matches: allUsers.length,
773
+ users: allUsers.slice(0, limit)
774
+ });
775
+ }
@@ -193,7 +193,8 @@ export async function slackAPI(method, params = {}, options = {}) {
193
193
  // so stringify any arrays/objects before encoding.
194
194
  const safeParams = {};
195
195
  for (const [key, value] of Object.entries(params)) {
196
- safeParams[key] = (typeof value === "object" && value !== null)
196
+ if (value === undefined || value === null) continue;
197
+ safeParams[key] = (typeof value === "object")
197
198
  ? JSON.stringify(value)
198
199
  : String(value);
199
200
  }
@@ -228,7 +229,12 @@ export async function slackAPI(method, params = {}, options = {}) {
228
229
  throw networkError;
229
230
  }
230
231
 
231
- const data = await response.json();
232
+ let data;
233
+ try {
234
+ data = await response.json();
235
+ } catch (parseError) {
236
+ throw new Error(`Slack API ${method} returned non-JSON (HTTP ${response.status}): ${parseError.message}`);
237
+ }
232
238
 
233
239
  if (!data.ok) {
234
240
  // Handle rate limiting with exponential backoff
@@ -297,7 +303,9 @@ export function getUserCacheStats() {
297
303
  * Format a Slack timestamp to ISO string
298
304
  */
299
305
  export function formatTimestamp(ts) {
300
- return new Date(parseFloat(ts) * 1000).toISOString();
306
+ const parsed = parseFloat(ts);
307
+ if (!Number.isFinite(parsed)) return null;
308
+ return new Date(parsed * 1000).toISOString();
301
309
  }
302
310
 
303
311
  /**
@@ -11,7 +11,7 @@
11
11
  import { readFileSync, writeFileSync, existsSync, renameSync, unlinkSync } from "fs";
12
12
  import { homedir, platform } from "os";
13
13
  import { join } from "path";
14
- import { execSync } from "child_process";
14
+ import { execSync, execFileSync } from "child_process";
15
15
 
16
16
  const TOKEN_FILE = join(homedir(), ".slack-mcp-tokens.json");
17
17
  const KEYCHAIN_SERVICE = "slack-mcp-server";
@@ -28,8 +28,9 @@ let lastExtractionError = null;
28
28
  export function getFromKeychain(key) {
29
29
  if (!IS_MACOS) return null; // Keychain is macOS-only
30
30
  try {
31
- const result = execSync(
32
- `security find-generic-password -s "${KEYCHAIN_SERVICE}" -a "${key}" -w 2>/dev/null`,
31
+ const result = execFileSync(
32
+ "security",
33
+ ["find-generic-password", "-s", KEYCHAIN_SERVICE, "-a", key, "-w"],
33
34
  { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }
34
35
  );
35
36
  return result.trim();
@@ -43,11 +44,11 @@ export function saveToKeychain(key, value) {
43
44
  try {
44
45
  // Delete existing entry
45
46
  try {
46
- execSync(`security delete-generic-password -s "${KEYCHAIN_SERVICE}" -a "${key}" 2>/dev/null`, { stdio: 'pipe' });
47
+ execFileSync("security", ["delete-generic-password", "-s", KEYCHAIN_SERVICE, "-a", key], { stdio: 'pipe' });
47
48
  } catch (e) { /* ignore */ }
48
49
 
49
50
  // Add new entry
50
- execSync(`security add-generic-password -s "${KEYCHAIN_SERVICE}" -a "${key}" -w "${value}"`, { stdio: 'pipe' });
51
+ execFileSync("security", ["add-generic-password", "-s", KEYCHAIN_SERVICE, "-a", key, "-w", value], { stdio: 'pipe' });
51
52
  return true;
52
53
  } catch (e) {
53
54
  return false;
package/lib/tools.js CHANGED
@@ -268,5 +268,136 @@ export const TOOLS = [
268
268
  idempotentHint: true,
269
269
  openWorldHint: true
270
270
  }
271
+ },
272
+ {
273
+ name: "slack_add_reaction",
274
+ description: "Add an emoji reaction to a message",
275
+ inputSchema: {
276
+ type: "object",
277
+ properties: {
278
+ channel_id: {
279
+ type: "string",
280
+ description: "Channel or DM ID containing the message"
281
+ },
282
+ timestamp: {
283
+ type: "string",
284
+ description: "Message timestamp to react to"
285
+ },
286
+ reaction: {
287
+ type: "string",
288
+ description: "Emoji name without colons (e.g., 'thumbsup', 'eyes', 'white_check_mark')"
289
+ }
290
+ },
291
+ required: ["channel_id", "timestamp", "reaction"]
292
+ },
293
+ annotations: {
294
+ title: "Add Reaction",
295
+ readOnlyHint: false,
296
+ destructiveHint: false,
297
+ idempotentHint: true,
298
+ openWorldHint: true
299
+ }
300
+ },
301
+ {
302
+ name: "slack_remove_reaction",
303
+ description: "Remove an emoji reaction from a message",
304
+ inputSchema: {
305
+ type: "object",
306
+ properties: {
307
+ channel_id: {
308
+ type: "string",
309
+ description: "Channel or DM ID containing the message"
310
+ },
311
+ timestamp: {
312
+ type: "string",
313
+ description: "Message timestamp to remove reaction from"
314
+ },
315
+ reaction: {
316
+ type: "string",
317
+ description: "Emoji name without colons (e.g., 'thumbsup', 'eyes')"
318
+ }
319
+ },
320
+ required: ["channel_id", "timestamp", "reaction"]
321
+ },
322
+ annotations: {
323
+ title: "Remove Reaction",
324
+ readOnlyHint: false,
325
+ destructiveHint: false,
326
+ idempotentHint: true,
327
+ openWorldHint: true
328
+ }
329
+ },
330
+ {
331
+ name: "slack_conversations_mark",
332
+ description: "Mark a conversation as read up to a specific message timestamp",
333
+ inputSchema: {
334
+ type: "object",
335
+ properties: {
336
+ channel_id: {
337
+ type: "string",
338
+ description: "Channel or DM ID to mark as read"
339
+ },
340
+ timestamp: {
341
+ type: "string",
342
+ description: "Message timestamp to mark as read up to (all messages at or before this are marked read)"
343
+ }
344
+ },
345
+ required: ["channel_id", "timestamp"]
346
+ },
347
+ annotations: {
348
+ title: "Mark as Read",
349
+ readOnlyHint: false,
350
+ destructiveHint: false,
351
+ idempotentHint: true,
352
+ openWorldHint: true
353
+ }
354
+ },
355
+ {
356
+ name: "slack_conversations_unreads",
357
+ description: "Get channels and DMs with unread messages, sorted by unread count (highest first)",
358
+ inputSchema: {
359
+ type: "object",
360
+ properties: {
361
+ types: {
362
+ type: "string",
363
+ description: "Comma-separated types: im, mpim, public_channel, private_channel (default all)",
364
+ default: "im,mpim,public_channel,private_channel"
365
+ },
366
+ limit: {
367
+ type: "number",
368
+ description: "Maximum conversations to return (default 50)"
369
+ }
370
+ }
371
+ },
372
+ annotations: {
373
+ title: "Unread Conversations",
374
+ readOnlyHint: true,
375
+ idempotentHint: true,
376
+ openWorldHint: true
377
+ }
378
+ },
379
+ {
380
+ name: "slack_users_search",
381
+ description: "Search workspace users by name, display name, or email. Case-insensitive partial match.",
382
+ inputSchema: {
383
+ type: "object",
384
+ properties: {
385
+ query: {
386
+ type: "string",
387
+ description: "Search term to match against name, display name, real name, or email"
388
+ },
389
+ limit: {
390
+ type: "number",
391
+ description: "Maximum results to return (default 20)"
392
+ }
393
+ },
394
+ required: ["query"]
395
+ },
396
+ annotations: {
397
+ title: "Search Users",
398
+ readOnlyHint: true,
399
+ idempotentHint: true,
400
+ openWorldHint: true
401
+ }
271
402
  }
272
403
  ];