@jtalk22/slack-mcp 4.4.0 → 4.4.2

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
@@ -60,7 +60,7 @@ Six prebuilt templates ship with the package:
60
60
  npx -y @jtalk22/slack-mcp --apply-template oncall-handoff --channels C012345,C067890
61
61
  ```
62
62
 
63
- Available templates: `oncall-handoff`, `support-triage`, `exec-monday`, `sprint-tracker`, `customer-feedback`, `incident-room`. The structural primitives (`slack_workflow_save`, `slack_workflows`) are free forever in OSS; the hosted brain is `$0` to start (no card) and `$9/mo` Pro for unlimited AI tools (scheduled morning catch-up DM rolling out Q2 2026).
63
+ Available templates: `oncall-handoff`, `support-triage`, `exec-monday`, `sprint-tracker`, `customer-feedback`, `incident-room`. The structural primitives (`slack_workflow_save`, `slack_workflows`) are free forever in OSS; the hosted brain is `$0` to start (no card) and `$9/mo` Pro for unlimited AI tools (scheduled morning catch-up DM in development).
64
64
 
65
65
  ## Quick Start per Client
66
66
 
@@ -269,7 +269,7 @@ Hosted tiers at [mcp.revasserlabs.com](https://mcp.revasserlabs.com):
269
269
  |------|-------|-------------|
270
270
  | Self-host | Free (MIT) | Local stdio, all 21 tools (16 read/write Slack + 2 workflow profile primitives + 3 discoverable upgrade stubs to hosted brain) |
271
271
  | Hosted Free | $0 (no card) | Email signup, 1 workspace, 10 smart_search/mo + 3 catch_me_up/mo + 5 triage/day. All 5 workflow profile types. 7-day index retention. |
272
- | Pro | $9/mo | Unlimited AI tools, **scheduled morning catch-up DM** *(rolling out Q2 2026, 8am workspace tz)*, permanent OAuth, 90-day Vectorize, 2 workspaces |
272
+ | Pro | $9/mo | Unlimited AI tools, **scheduled morning catch-up DM** *(in development, 8am workspace tz)*, permanent OAuth, 90-day Vectorize, 2 workspaces |
273
273
  | Team | $49/mo flat | Pro + shared workflow profiles + audit log + 24h support + scheduled catch-up to channel + 5 workspaces |
274
274
  | Ops | from $199/mo (custom) | SLA, custom retention, SOC2 evidence path, multi-tenant isolation, 10+ workspaces, dedicated workflow tuning |
275
275
 
@@ -410,4 +410,4 @@ Not affiliated with Slack Technologies, Inc. Uses browser session credentials
410
410
 
411
411
  ---
412
412
 
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.
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 *(in development)*, 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.
@@ -31,7 +31,7 @@ If `--version` fails here, the issue is install/runtime path, not Slack credenti
31
31
 
32
32
  ## Hosted Version
33
33
 
34
- The hosted version is live at [mcp.revasserlabs.com](https://mcp.revasserlabs.com). Free tier (no card) ships 10 smart_search/mo + 3 catch_me_up/mo + 5 triage/day + all 5 workflow profile types. Pro at $9/mo unlocks unlimited AI tools, the scheduled morning catch-up DM at 8am workspace time *(rolling out Q2 2026)*, permanent OAuth (no 2-week token rotation), and 90-day Vectorize retention. Team at $49/mo flat covers 5 workspaces with shared workflow profiles and audit log. Ops engagement starts at $199/mo (custom) for 10+ workspace organizations with SLA, custom retention, SOC2 evidence, or multi-tenant isolation.
34
+ The hosted version is live at [mcp.revasserlabs.com](https://mcp.revasserlabs.com). Free tier (no card) ships 10 smart_search/mo + 3 catch_me_up/mo + 5 triage/day + all 5 workflow profile types. Pro at $9/mo unlocks unlimited AI tools, the scheduled morning catch-up DM at 8am workspace time *(in development)*, permanent OAuth (no 2-week token rotation), and 90-day Vectorize retention. Team at $49/mo flat covers 5 workspaces with shared workflow profiles and audit log. Ops engagement starts at $199/mo (custom) for 10+ workspace organizations with SLA, custom retention, SOC2 evidence, or multi-tenant isolation.
35
35
 
36
36
  The OSS package keeps the local-machine path. The hosted version adds the AI brain (smart_search, catch_me_up, triage) — these tools also appear in the OSS package as discoverable upgrade stubs that point at the hosted signup.
37
37
 
package/lib/handlers.js CHANGED
@@ -336,14 +336,17 @@ export async function handleListConversations(args) {
336
336
  export async function handleConversationsHistory(args) {
337
337
  const resolveUsers = args.resolve_users !== false;
338
338
  const includeRichMessageFields = parseBool(args.include_rich_message_fields);
339
- const result = await slackAPI("conversations.history", {
339
+ const historyParams = {
340
340
  channel: args.channel_id,
341
341
  limit: args.limit || 50,
342
342
  oldest: args.oldest,
343
343
  latest: args.latest,
344
- inclusive: true,
345
344
  include_all_metadata: parseBool(args.include_all_metadata)
346
- });
345
+ };
346
+ // Only opt into boundary-inclusive reads when a boundary was actually
347
+ // provided — otherwise leave Slack's default behavior untouched.
348
+ if (args.oldest || args.latest) historyParams.inclusive = true;
349
+ const result = await slackAPI("conversations.history", historyParams);
347
350
 
348
351
  const messages = await Promise.all((result.messages || []).map(async (msg) => {
349
352
  const userName = resolveUsers ? await resolveUser(msg.user) : msg.user;
@@ -384,15 +387,18 @@ export async function handleGetFullConversation(args) {
384
387
 
385
388
  // Fetch all messages with pagination
386
389
  while (hasMore && allMessages.length < maxMessages) {
387
- const result = await slackAPI("conversations.history", {
390
+ const historyParams = {
388
391
  channel: args.channel_id,
389
392
  limit: Math.min(100, maxMessages - allMessages.length),
390
393
  oldest: args.oldest,
391
394
  latest: args.latest,
392
395
  cursor,
393
- inclusive: true,
394
396
  include_all_metadata: parseBool(args.include_all_metadata)
395
- });
397
+ };
398
+ // Boundary-inclusive only when a boundary was actually provided —
399
+ // otherwise leave Slack's default behavior untouched.
400
+ if (args.oldest || args.latest) historyParams.inclusive = true;
401
+ const result = await slackAPI("conversations.history", historyParams);
396
402
 
397
403
  for (const msg of result.messages || []) {
398
404
  const userName = await resolveUser(msg.user);
@@ -738,13 +744,32 @@ export async function handleConversationsUnreads(args) {
738
744
  /**
739
745
  * Search users handler - client-side filter on users.list
740
746
  */
747
+ // Explicit scan cap for client-side user search: stop paginating after
748
+ // scanning this many workspace users (or when the cursor is exhausted) and
749
+ // flag the result as truncated so total_matches is never misreported as
750
+ // complete.
751
+ const USERS_SEARCH_MAX_SCANNED = 1000;
752
+
741
753
  export async function handleUsersSearch(args) {
742
- const query = (args.query || "").toLowerCase();
754
+ const rawQuery = typeof args.query === "string" ? args.query : "";
755
+ const query = rawQuery.trim().toLowerCase();
743
756
  const limit = args.limit || 20;
744
757
 
745
- // Fetch all users (paginated)
758
+ // An empty/whitespace query would match every user in the workspace.
759
+ if (!query) {
760
+ return asMcpJson({
761
+ status: "error",
762
+ code: "invalid_arguments",
763
+ message: "query must be a non-empty string (an empty query would match all users).",
764
+ next_action: "Provide a name, display name, real name, or email fragment to search for."
765
+ }, true);
766
+ }
767
+
768
+ // Fetch users (paginated) and filter client-side
746
769
  const allUsers = [];
747
770
  let cursor;
771
+ let scannedUsers = 0;
772
+ let truncated = false;
748
773
 
749
774
  do {
750
775
  const result = await slackAPI("users.list", {
@@ -753,6 +778,7 @@ export async function handleUsersSearch(args) {
753
778
  });
754
779
 
755
780
  for (const u of (result.members || [])) {
781
+ scannedUsers++;
756
782
  if (u.deleted || u.is_bot || u.id === "USLACKBOT") continue;
757
783
 
758
784
  const searchFields = [
@@ -776,13 +802,18 @@ export async function handleUsersSearch(args) {
776
802
  }
777
803
 
778
804
  cursor = result.response_metadata?.next_cursor;
805
+ if (cursor && scannedUsers >= USERS_SEARCH_MAX_SCANNED) {
806
+ truncated = true;
807
+ break;
808
+ }
779
809
  if (cursor) await sleep(100);
780
- } while (cursor && allUsers.length < 500);
810
+ } while (cursor);
781
811
 
782
812
  return asMcpJson({
783
813
  query: args.query,
784
814
  count: Math.min(allUsers.length, limit),
785
815
  total_matches: allUsers.length,
816
+ truncated,
786
817
  users: allUsers.slice(0, limit)
787
818
  });
788
819
  }
@@ -841,7 +872,7 @@ const HOSTED_UPGRADE_PAYLOAD = {
841
872
  signup_url: "https://mcp.revasserlabs.com/signup",
842
873
  upgrade_url: "https://mcp.revasserlabs.com/pricing",
843
874
  free_tier_quota: "10 smart_search + 3 catch_me_up per month, 5 triage per day",
844
- pro_value_prop: "Pro $9/mo unlocks unlimited AI tools (scheduled morning catch-up DM at 8am workspace time rolling out Q2 2026).",
875
+ pro_value_prop: "Pro $9/mo unlocks unlimited AI tools (scheduled morning catch-up DM at 8am workspace time in development).",
845
876
  };
846
877
 
847
878
  export async function handleSmartSearch(args) {
@@ -873,3 +904,35 @@ export async function handleTriage(args) {
873
904
  true
874
905
  );
875
906
  }
907
+
908
+ // ============ Shared Tool Dispatch Map ============
909
+ // Single source of truth mapping every advertised tool name (lib/tools.js)
910
+ // to its handler. Both transports — stdio (src/server.js) and HTTP
911
+ // (src/server-http.js) — dispatch through this map so the advertised tool
912
+ // list and the dispatch surface cannot drift apart.
913
+
914
+ export const TOOL_HANDLERS = Object.freeze({
915
+ slack_token_status: handleTokenStatus,
916
+ slack_health_check: handleHealthCheck,
917
+ slack_refresh_tokens: handleRefreshTokens,
918
+ slack_list_conversations: handleListConversations,
919
+ slack_conversations_history: handleConversationsHistory,
920
+ slack_get_full_conversation: handleGetFullConversation,
921
+ slack_search_messages: handleSearchMessages,
922
+ slack_users_info: handleUsersInfo,
923
+ slack_send_message: handleSendMessage,
924
+ slack_get_thread: handleGetThread,
925
+ slack_list_users: handleListUsers,
926
+ slack_add_reaction: handleAddReaction,
927
+ slack_remove_reaction: handleRemoveReaction,
928
+ slack_conversations_mark: handleConversationsMark,
929
+ slack_conversations_unreads: handleConversationsUnreads,
930
+ slack_users_search: handleUsersSearch,
931
+ // Workflow profile primitives (OSS local JSON store)
932
+ slack_workflow_save: handleWorkflowSave,
933
+ slack_workflows: handleWorkflows,
934
+ // Hosted-only AI tools (OSS = upgrade stubs)
935
+ slack_smart_search: handleSmartSearch,
936
+ slack_catch_me_up: handleCatchMeUp,
937
+ slack_triage: handleTriage,
938
+ });
@@ -63,7 +63,7 @@ function shareLinks() {
63
63
  }
64
64
 
65
65
  function shareNote() {
66
- return `<strong>Verify in 30 seconds:</strong> <code>--version</code>, <code>--doctor</code>, <code>--status</code>. Self-host gives ${PUBLIC_METADATA.selfHostedToolCount} tools with session-based auth. Works with any MCP client — Claude, ChatGPT, Cursor, Copilot, Gemini, Windsurf. Hosted free tier (no card) live at <a href="${PUBLIC_METADATA.canonicalSiteUrl}">mcp.revasserlabs.com</a> — Pro $9/mo unlocks unlimited AI tools (scheduled morning catch-up DM rolling out Q2 2026).`;
66
+ return `<strong>Verify in 30 seconds:</strong> <code>--version</code>, <code>--doctor</code>, <code>--status</code>. Self-host gives ${PUBLIC_METADATA.selfHostedToolCount} tools with session-based auth. Works with any MCP client — Claude, ChatGPT, Cursor, Copilot, Gemini, Windsurf. Hosted free tier (no card) live at <a href="${PUBLIC_METADATA.canonicalSiteUrl}">mcp.revasserlabs.com</a> — Pro $9/mo unlocks unlimited AI tools (scheduled morning catch-up DM in development).`;
67
67
  }
68
68
 
69
69
  function demoLinks() {
@@ -75,7 +75,7 @@ function demoLinks() {
75
75
  }
76
76
 
77
77
  function demoNote() {
78
- return `Self-host free for ${PUBLIC_METADATA.selfHostedToolCount} tools with session-based auth. Works with Claude, ChatGPT, Cursor, Copilot, Gemini, Windsurf, and any other MCP client. No OAuth app, no admin approval. Hosted free tier (no card) live at <a href="${PUBLIC_METADATA.canonicalSiteUrl}" target="_blank" rel="noopener noreferrer">mcp.revasserlabs.com</a> — Pro $9/mo unlocks unlimited AI tools (scheduled morning catch-up DM rolling out Q2 2026).`;
78
+ return `Self-host free for ${PUBLIC_METADATA.selfHostedToolCount} tools with session-based auth. Works with Claude, ChatGPT, Cursor, Copilot, Gemini, Windsurf, and any other MCP client. No OAuth app, no admin approval. Hosted free tier (no card) live at <a href="${PUBLIC_METADATA.canonicalSiteUrl}" target="_blank" rel="noopener noreferrer">mcp.revasserlabs.com</a> — Pro $9/mo unlocks unlimited AI tools (scheduled morning catch-up DM in development).`;
79
79
  }
80
80
 
81
81
  function demoFooterLinks() {
@@ -8,7 +8,7 @@
8
8
  * 4. Chrome auto-extraction (fallback)
9
9
  */
10
10
 
11
- import { readFileSync, writeFileSync, existsSync, renameSync, unlinkSync, chmodSync, copyFileSync, mkdtempSync, statSync, readdirSync } from "fs";
11
+ import { readFileSync, writeFileSync, existsSync, renameSync, unlinkSync, rmSync, chmodSync, copyFileSync, mkdtempSync, statSync, readdirSync } from "fs";
12
12
  import { homedir, platform, tmpdir } from "os";
13
13
  import { join } from "path";
14
14
  import { execFileSync } from "child_process";
@@ -265,13 +265,22 @@ function extractCookieForProfile(profileDir) {
265
265
  const tmpDb = join(tmpDir, 'Cookies');
266
266
  try {
267
267
  copyFileSync(cookiesPath, tmpDb);
268
+ // Chrome keeps recent writes in the SQLite WAL sidecar until the next
269
+ // checkpoint — copy Cookies-wal (and -shm) too when present so the
270
+ // snapshot isn't stale.
271
+ for (const suffix of ['-wal', '-shm']) {
272
+ const sidecarPath = `${cookiesPath}${suffix}`;
273
+ if (existsSync(sidecarPath)) {
274
+ try { copyFileSync(sidecarPath, `${tmpDb}${suffix}`); } catch {}
275
+ }
276
+ }
268
277
 
269
278
  const queryResult = execFileSync('sqlite3', [
270
279
  tmpDb,
271
280
  "SELECT hex(encrypted_value) FROM cookies WHERE host_key LIKE '%.slack.com%' AND name = 'd' LIMIT 1;"
272
281
  ], { encoding: 'utf-8', timeout: 5000 }).trim();
273
282
 
274
- try { unlinkSync(tmpDb); unlinkSync(tmpDir); } catch {}
283
+ try { rmSync(tmpDir, { recursive: true, force: true }); } catch {}
275
284
 
276
285
  if (!queryResult) return null;
277
286
 
@@ -304,7 +313,7 @@ function extractCookieForProfile(profileDir) {
304
313
  if (xoxdIndex < 0) return null;
305
314
  return text.substring(xoxdIndex);
306
315
  } catch {
307
- try { unlinkSync(tmpDb); unlinkSync(tmpDir); } catch {}
316
+ try { rmSync(tmpDir, { recursive: true, force: true }); } catch {}
308
317
  return null;
309
318
  }
310
319
  }
@@ -358,7 +367,10 @@ function extractTokenFromLevelDB(profileDir) {
358
367
  const txt = readFileSync(f.path).toString('binary');
359
368
  XOXC_TOKEN_RE.lastIndex = 0;
360
369
  const matches = txt.match(XOXC_TOKEN_RE);
361
- if (matches && matches.length) return matches[0];
370
+ // LevelDB .log/.ldb files append newer records after older ones, so
371
+ // the LAST match in a file is the most recently cached token — the
372
+ // first match can be a stale, already-rotated token.
373
+ if (matches && matches.length) return matches[matches.length - 1];
362
374
  } catch {
363
375
  continue;
364
376
  }
package/lib/tools.js CHANGED
@@ -537,7 +537,7 @@ export const TOOLS = [
537
537
  },
538
538
  {
539
539
  name: "slack_catch_me_up",
540
- description: "Run a structured catch-up against a saved workflow profile. Returns structured JSON per the profile's workflow_kind: support_inbox returns {open_threads, ack_lag, owner_gaps, escalations, next_actions}; incident_room returns {incident_summary, timeline, open_risks, owner_gaps, next_actions}; exec_brief returns {summary, decisions, risks, asks, action_items}; product_launch_watch returns {launch_signals, feedback_themes, blockers, metrics, next_actions}; custom returns {summary, highlights, open_questions, next_actions}. Hosted-only. Free tier ships 3 calls/month; Pro $9/mo unlocks unlimited (scheduled morning DM at 8am workspace tz rolling out Q2 2026).",
540
+ description: "Run a structured catch-up against a saved workflow profile. Returns structured JSON per the profile's workflow_kind: support_inbox returns {open_threads, ack_lag, owner_gaps, escalations, next_actions}; incident_room returns {incident_summary, timeline, open_risks, owner_gaps, next_actions}; exec_brief returns {summary, decisions, risks, asks, action_items}; product_launch_watch returns {launch_signals, feedback_themes, blockers, metrics, next_actions}; custom returns {summary, highlights, open_questions, next_actions}. Hosted-only. Free tier ships 3 calls/month; Pro $9/mo unlocks unlimited (scheduled morning DM at 8am workspace tz in development).",
541
541
  inputSchema: {
542
542
  type: "object",
543
543
  properties: {
@@ -70,32 +70,47 @@ export function loadStore() {
70
70
  try {
71
71
  const data = JSON.parse(raw);
72
72
  if (!data || typeof data !== "object" || !data.profiles) {
73
- backupCorruptStore(raw, "shape-invalid");
73
+ quarantineCorruptStore("shape-invalid");
74
74
  return emptyStore();
75
75
  }
76
76
  if (data.version !== STORE_VERSION) {
77
- // Future: migration logic. For now, back up the unrecognized version
78
- // before falling back to empty so the old data is recoverable.
79
- backupCorruptStore(raw, `version-${data.version}`);
77
+ // Future: migration logic. For now, quarantine the unrecognized
78
+ // version before falling back to empty so the old data is recoverable.
79
+ quarantineCorruptStore(`version-${data.version}`);
80
80
  return emptyStore();
81
81
  }
82
82
  return data;
83
83
  } catch {
84
- backupCorruptStore(raw, "json-parse-error");
84
+ quarantineCorruptStore("json-parse-error");
85
85
  return emptyStore();
86
86
  }
87
87
  }
88
88
 
89
- function backupCorruptStore(raw, reasonTag) {
89
+ /**
90
+ * Move an unreadable store aside to <store>.corrupt-<timestamp> before
91
+ * loadStore falls back to an empty store. MOVING (not copying) matters:
92
+ * it guarantees the next saveStore cannot clobber the user's only copy of
93
+ * their profiles, and repeated loads don't spawn one backup per call.
94
+ * Warns on stderr (stdout belongs to the MCP stdio protocol).
95
+ */
96
+ function quarantineCorruptStore(reasonTag) {
97
+ const stamp = new Date().toISOString().replace(/[:.]/g, "-");
98
+ const quarantinePath = `${STORE_FILE}.corrupt-${stamp}`;
90
99
  try {
91
- const stamp = new Date().toISOString().replace(/[:.]/g, "-");
92
- const backupPath = `${STORE_FILE}.bak.${reasonTag}.${stamp}`;
93
- writeFileSync(backupPath, raw);
100
+ renameSync(STORE_FILE, quarantinePath);
94
101
  if (platform() === "darwin" || platform() === "linux") {
95
- try { chmodSync(backupPath, 0o600); } catch {}
102
+ try { chmodSync(quarantinePath, 0o600); } catch {}
96
103
  }
97
- } catch {
98
- // Backup is best-effort; do not throw on backup failure.
104
+ console.error(
105
+ `[slack-mcp] WARNING: workflow store ${STORE_FILE} is unreadable (${reasonTag}). ` +
106
+ `Quarantined it to ${quarantinePath} and continuing with an empty store.`
107
+ );
108
+ } catch (e) {
109
+ // Quarantine is best-effort; never let it break loadStore.
110
+ console.error(
111
+ `[slack-mcp] WARNING: workflow store ${STORE_FILE} is unreadable (${reasonTag}) ` +
112
+ `and could not be quarantined: ${e?.message || e}`
113
+ );
99
114
  }
100
115
  }
101
116
 
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@jtalk22/slack-mcp",
3
3
  "mcpName": "io.github.jtalk22/slack-mcp-server",
4
- "version": "4.4.0",
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).",
4
+ "version": "4.4.2",
5
+ "description": "Slack MCP without OAuth 21 tools, session-based, local-first. Free OSS + hosted tier from $9/mo.",
6
6
  "type": "module",
7
7
  "main": "src/server.js",
8
8
  "bin": {
package/public/share.html CHANGED
@@ -124,7 +124,7 @@
124
124
  <a href="https://mcp.revasserlabs.com" rel="noopener" style="background:rgba(240,194,70,0.18);border-color:rgba(240,194,70,0.45);color:#f0c246">Hosted</a>
125
125
  </div>
126
126
 
127
- <p class="note"><strong>Verify in 30 seconds:</strong> <code>--version</code>, <code>--doctor</code>, <code>--status</code>. Self-host gives 21 tools with session-based auth. Works with any MCP client — Claude, ChatGPT, Cursor, Copilot, Gemini, Windsurf. Hosted free tier (no card) live at <a href="https://mcp.revasserlabs.com">mcp.revasserlabs.com</a> — Pro $9/mo unlocks unlimited AI tools (scheduled morning catch-up DM rolling out Q2 2026).</p>
127
+ <p class="note"><strong>Verify in 30 seconds:</strong> <code>--version</code>, <code>--doctor</code>, <code>--status</code>. Self-host gives 21 tools with session-based auth. Works with any MCP client — Claude, ChatGPT, Cursor, Copilot, Gemini, Windsurf. Hosted free tier (no card) live at <a href="https://mcp.revasserlabs.com">mcp.revasserlabs.com</a> — Pro $9/mo unlocks unlimited AI tools (scheduled morning catch-up DM in development).</p>
128
128
  </main>
129
129
  </body>
130
130
  </html>
@@ -113,5 +113,5 @@ if (!profile.channels.length) {
113
113
  console.log("Or call slack_workflow_save from your MCP client to update.");
114
114
  } else {
115
115
  console.log("Profile is ready. Run slack_catch_me_up against it from your MCP client.");
116
- console.log(`(Free tier: 3 catch_me_up calls/month. Pro $9/mo unlocks unlimited; scheduled morning DM rolling out Q2 2026.)`);
116
+ console.log(`(Free tier: 3 catch_me_up calls/month. Pro $9/mo unlocks unlimited; scheduled morning DM in development.)`);
117
117
  }
package/server.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
3
  "name": "io.github.jtalk22/slack-mcp-server",
4
4
  "title": "Slack MCP Server",
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).",
5
+ "description": "Slack MCP without OAuth \u2014 21 tools, session-based, local-first. Free OSS + hosted tier from $9/mo.",
6
6
  "websiteUrl": "https://mcp.revasserlabs.com",
7
7
  "icons": [
8
8
  {
@@ -17,7 +17,7 @@
17
17
  "url": "https://github.com/jtalk22/slack-mcp-server",
18
18
  "source": "github"
19
19
  },
20
- "version": "4.4.0",
20
+ "version": "4.4.2",
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.4.0",
31
+ "version": "4.4.2",
32
32
  "transport": {
33
33
  "type": "stdio"
34
34
  },
@@ -15,24 +15,7 @@ import {
15
15
  } from "@modelcontextprotocol/sdk/types.js";
16
16
 
17
17
  import { TOOLS } from "../lib/tools.js";
18
- import {
19
- handleTokenStatus,
20
- handleHealthCheck,
21
- handleRefreshTokens,
22
- handleListConversations,
23
- handleConversationsHistory,
24
- handleGetFullConversation,
25
- handleSearchMessages,
26
- handleUsersInfo,
27
- handleSendMessage,
28
- handleGetThread,
29
- handleListUsers,
30
- handleAddReaction,
31
- handleRemoveReaction,
32
- handleConversationsMark,
33
- handleConversationsUnreads,
34
- handleUsersSearch,
35
- } from "../lib/handlers.js";
18
+ import { TOOL_HANDLERS } from "../lib/handlers.js";
36
19
  import { RELEASE_VERSION } from "../lib/public-metadata.js";
37
20
 
38
21
  const SERVER_NAME = "slack-mcp-server";
@@ -98,53 +81,24 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
98
81
  const { name, arguments: args } = request.params;
99
82
 
100
83
  try {
101
- switch (name) {
102
- case "slack_token_status":
103
- return await handleTokenStatus();
104
- case "slack_health_check":
105
- return await handleHealthCheck();
106
- case "slack_refresh_tokens":
107
- return await handleRefreshTokens();
108
- case "slack_list_conversations":
109
- return await handleListConversations(args);
110
- case "slack_conversations_history":
111
- return await handleConversationsHistory(args);
112
- case "slack_get_full_conversation":
113
- return await handleGetFullConversation(args);
114
- case "slack_search_messages":
115
- return await handleSearchMessages(args);
116
- case "slack_users_info":
117
- return await handleUsersInfo(args);
118
- case "slack_send_message":
119
- return await handleSendMessage(args);
120
- case "slack_get_thread":
121
- return await handleGetThread(args);
122
- case "slack_list_users":
123
- return await handleListUsers(args);
124
- case "slack_add_reaction":
125
- return await handleAddReaction(args);
126
- case "slack_remove_reaction":
127
- return await handleRemoveReaction(args);
128
- case "slack_conversations_mark":
129
- return await handleConversationsMark(args);
130
- case "slack_conversations_unreads":
131
- return await handleConversationsUnreads(args);
132
- case "slack_users_search":
133
- return await handleUsersSearch(args);
134
- default:
135
- return {
136
- content: [{
137
- type: "text",
138
- text: JSON.stringify({
139
- status: "error",
140
- code: "unknown_tool",
141
- message: `Unknown tool: ${name}`,
142
- next_action: "Call tools/list to inspect available tool names."
143
- }, null, 2)
144
- }],
145
- isError: true
146
- };
84
+ // Shared dispatch map keeps this transport in lockstep with the stdio
85
+ // server and the advertised TOOLS list.
86
+ const handler = TOOL_HANDLERS[name];
87
+ if (!handler) {
88
+ return {
89
+ content: [{
90
+ type: "text",
91
+ text: JSON.stringify({
92
+ status: "error",
93
+ code: "unknown_tool",
94
+ message: `Unknown tool: ${name}`,
95
+ next_action: "Call tools/list to inspect available tool names."
96
+ }, null, 2)
97
+ }],
98
+ isError: true
99
+ };
147
100
  }
101
+ return await handler(args);
148
102
  } catch (error) {
149
103
  return {
150
104
  content: [{
package/src/server.js CHANGED
@@ -30,27 +30,9 @@ import { RELEASE_VERSION } from "../lib/public-metadata.js";
30
30
  import { checkTokenHealth } from "../lib/slack-client.js";
31
31
  import { TOOLS } from "../lib/tools.js";
32
32
  import {
33
- handleTokenStatus,
33
+ TOOL_HANDLERS,
34
34
  handleHealthCheck,
35
- handleRefreshTokens,
36
35
  handleListConversations,
37
- handleConversationsHistory,
38
- handleGetFullConversation,
39
- handleSearchMessages,
40
- handleUsersInfo,
41
- handleSendMessage,
42
- handleGetThread,
43
- handleListUsers,
44
- handleAddReaction,
45
- handleRemoveReaction,
46
- handleConversationsMark,
47
- handleConversationsUnreads,
48
- handleUsersSearch,
49
- handleWorkflowSave,
50
- handleWorkflows,
51
- handleSmartSearch,
52
- handleCatchMeUp,
53
- handleTriage,
54
36
  } from "../lib/handlers.js";
55
37
 
56
38
  // Background refresh interval (4 hours)
@@ -231,86 +213,24 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
231
213
  const { name, arguments: args } = request.params;
232
214
 
233
215
  try {
234
- switch (name) {
235
- case "slack_token_status":
236
- return await handleTokenStatus();
237
-
238
- case "slack_health_check":
239
- return await handleHealthCheck();
240
-
241
- case "slack_refresh_tokens":
242
- return await handleRefreshTokens();
243
-
244
- case "slack_list_conversations":
245
- return await handleListConversations(args);
246
-
247
- case "slack_conversations_history":
248
- return await handleConversationsHistory(args);
249
-
250
- case "slack_get_full_conversation":
251
- return await handleGetFullConversation(args);
252
-
253
- case "slack_search_messages":
254
- return await handleSearchMessages(args);
255
-
256
- case "slack_users_info":
257
- return await handleUsersInfo(args);
258
-
259
- case "slack_send_message":
260
- return await handleSendMessage(args);
261
-
262
- case "slack_get_thread":
263
- return await handleGetThread(args);
264
-
265
- case "slack_list_users":
266
- return await handleListUsers(args);
267
-
268
- case "slack_add_reaction":
269
- return await handleAddReaction(args);
270
-
271
- case "slack_remove_reaction":
272
- return await handleRemoveReaction(args);
273
-
274
- case "slack_conversations_mark":
275
- return await handleConversationsMark(args);
276
-
277
- case "slack_conversations_unreads":
278
- return await handleConversationsUnreads(args);
279
-
280
- case "slack_users_search":
281
- return await handleUsersSearch(args);
282
-
283
- // Workflow profile primitives (OSS local JSON store)
284
- case "slack_workflow_save":
285
- return await handleWorkflowSave(args);
286
-
287
- case "slack_workflows":
288
- return await handleWorkflows(args);
289
-
290
- // Hosted-only AI tools (OSS = upgrade stubs)
291
- case "slack_smart_search":
292
- return await handleSmartSearch(args);
293
-
294
- case "slack_catch_me_up":
295
- return await handleCatchMeUp(args);
296
-
297
- case "slack_triage":
298
- return await handleTriage(args);
299
-
300
- default:
301
- return {
302
- content: [{
303
- type: "text",
304
- text: JSON.stringify({
305
- status: "error",
306
- code: "unknown_tool",
307
- message: `Unknown tool: ${name}`,
308
- next_action: "Use tools/list to discover available tools."
309
- }, null, 2)
310
- }],
311
- isError: true
312
- };
216
+ // Shared dispatch map (lib/handlers.js) keeps this transport in lockstep
217
+ // with the HTTP server and the advertised TOOLS list.
218
+ const handler = TOOL_HANDLERS[name];
219
+ if (!handler) {
220
+ return {
221
+ content: [{
222
+ type: "text",
223
+ text: JSON.stringify({
224
+ status: "error",
225
+ code: "unknown_tool",
226
+ message: `Unknown tool: ${name}`,
227
+ next_action: "Use tools/list to discover available tools."
228
+ }, null, 2)
229
+ }],
230
+ isError: true
231
+ };
313
232
  }
233
+ return await handler(args);
314
234
  } catch (error) {
315
235
  if (error?.code === "token_auth_failed") {
316
236
  return {
@@ -5,6 +5,12 @@
5
5
  "priority_people": [],
6
6
  "retention_mode": "ephemeral",
7
7
  "summary_cadence": "daily_8am",
8
- "structured_keys": ["open_threads", "ack_lag", "owner_gaps", "escalations", "next_actions"],
9
- "_template_notes": "Apply with: slack-mcp --apply-template support-triage --channels C0SUPPORT,C0ESCALATIONS. summary_cadence=daily_8am needs Pro or Team for the scheduled morning DM (rolling out Q2 2026). Free tier can run on_demand."
8
+ "structured_keys": [
9
+ "open_threads",
10
+ "ack_lag",
11
+ "owner_gaps",
12
+ "escalations",
13
+ "next_actions"
14
+ ],
15
+ "_template_notes": "Apply with: slack-mcp --apply-template support-triage --channels C0SUPPORT,C0ESCALATIONS. summary_cadence=daily_8am needs Pro or Team for the scheduled morning DM (in development). Free tier can run on_demand."
10
16
  }