@jtalk22/slack-mcp 4.4.1 → 4.4.3

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 in development).
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 `$19/mo` Pro for unlimited AI tools.
64
64
 
65
65
  ## Quick Start per Client
66
66
 
@@ -268,10 +268,10 @@ Hosted tiers at [mcp.revasserlabs.com](https://mcp.revasserlabs.com):
268
268
  | Tier | Price | What it owns |
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
- | 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** *(in development, 8am workspace tz)*, permanent OAuth, 90-day Vectorize, 2 workspaces |
273
- | Team | $49/mo flat | Pro + shared workflow profiles + audit log + 24h support + scheduled catch-up to channel + 5 workspaces |
274
- | Ops | from $199/mo (custom) | SLA, custom retention, SOC2 evidence path, multi-tenant isolation, 10+ workspaces, dedicated workflow tuning |
271
+ | Hosted Free | $0 (no card) | Email signup, 1 workspace, 2,000 requests/mo + 25 AI tool calls/mo. All 5 workflow profile types. 7-day index retention. |
272
+ | Pro | $19/mo or $190/yr | Unlimited requests (fair use), unlimited AI tool calls, permanent OAuth, email support, 2 workspaces |
273
+ | Team | $49/mo or $490/yr flat | Everything in Pro + shared workflow profiles, 5 workspaces, 24h support |
274
+ | Safeguard | $199/mo waitlist | Agent approval gates, scheduled catch-up DM, workspace memory all *(in development)*. Waitlist only. |
275
275
 
276
276
  </details>
277
277
 
@@ -324,6 +324,22 @@ Full release notes on [GitHub releases/latest](https://github.com/jtalk22/slack-
324
324
 
325
325
  </details>
326
326
 
327
+ ## Token expired? / OAuth Lifeboat
328
+
329
+ Session tokens (`xoxc-` + `xoxd-`) are extracted from your browser, and Slack rotates them roughly every 1-2 weeks. When they die, every tool call fails to authenticate. Instead of surfacing a raw Slack error, this server detects token death — `invalid_auth`, `not_authed`, `token_expired`, `token_revoked`, `account_inactive`, or an HTTP 401 — and returns a recovery message at the moment of pain.
330
+
331
+ **Self-fix — re-extract fresh tokens:**
332
+
333
+ ```bash
334
+ npx -y @jtalk22/slack-mcp --setup
335
+ ```
336
+
337
+ On macOS with a logged-in Slack tab open in Chrome, you can instead call the `slack_refresh_tokens` tool (or run `npm run tokens:auto`). To avoid silent expiration during long idle windows, set up the optional [token-refresh LaunchAgent](docs/SETUP.md#keep-tokens-fresh-while-claude-is-closed-macos-optional).
338
+
339
+ **Permanent fix — no rotation:** the [hosted version](https://mcp.revasserlabs.com/setup?utm_source=lifeboat&utm_medium=npm&utm_campaign=token_death) uses OAuth, which does not rotate every 1-2 weeks. Free tier available, no card.
340
+
341
+ The recovery message appears at most once per process per hour; repeat failures inside that window get a one-line reminder so agents in retry loops don't spam. Set `SLACK_MCP_NO_UPSELL=1` to drop the hosted-option line while keeping the self-fix guidance.
342
+
327
343
  ## Rich Message Fields
328
344
 
329
345
  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).
@@ -410,4 +426,4 @@ Not affiliated with Slack Technologies, Inc. Uses browser session credentials
410
426
 
411
427
  ---
412
428
 
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.
429
+ Hosted version live at [mcp.revasserlabs.com](https://mcp.revasserlabs.com): Free tier (no card — 2,000 requests/mo + 25 AI tool calls/mo), $19/mo Pro (unlimited, permanent OAuth), $49/mo Team flat, and Safeguard $199/mo (waitlist). Hosted owns the AI brain (smart_search, catch_me_up, triage), permanent OAuth (no 2-week token rotation), and shared workflow profiles; the scheduled morning catch-up DM at 8am workspace time is a Safeguard feature *(in development)*. 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.
@@ -8,7 +8,7 @@ Use this guide to choose the right operating mode before rollout.
8
8
  - Choose local `web` for browser workflows and manual Slack browsing.
9
9
  - Choose hosted HTTP only when you need remote execution and can handle token operations.
10
10
  - Choose Smithery/Worker only when your consumers require registry-hosted MCP transport.
11
- - A managed **Hosted** version is live at [mcp.revasserlabs.com](https://mcp.revasserlabs.com) — Free tier (no card), Pro $9/mo, Team $49/mo flat, Ops from $199/mo. See [pricing](https://mcp.revasserlabs.com/pricing).
11
+ - A managed **Hosted** version is live at [mcp.revasserlabs.com](https://mcp.revasserlabs.com) — Free tier (no card), Pro $19/mo, Team $49/mo flat, Safeguard $199/mo (waitlist). See [pricing](https://mcp.revasserlabs.com/pricing).
12
12
 
13
13
  ## Mode Matrix
14
14
 
@@ -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 *(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.
34
+ The hosted version is live at [mcp.revasserlabs.com](https://mcp.revasserlabs.com). Free tier (no card) ships 2,000 requests/mo + 25 AI tool calls/mo + all 5 workflow profile types. Pro at $19/mo (or $190/yr) unlocks unlimited requests and AI tool calls, permanent OAuth (no 2-week token rotation), email support, and 2 workspaces. Team at $49/mo flat (or $490/yr) covers 5 workspaces with shared workflow profiles and 24h support. Safeguard at $199/mo (waitlist only) adds agent approval gates, the scheduled morning catch-up DM at 8am workspace time, and workspace memory — all *(in development)*.
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
@@ -21,6 +21,7 @@ import {
21
21
  ALLOWED_WORKFLOW_KINDS_LIST,
22
22
  } from "./workflow-store.js";
23
23
  import { withRichMessageFields } from "./rich-message-fields.js";
24
+ import { isAuthDeath, buildLifeboatPayload } from "./lifeboat.js";
24
25
 
25
26
  // ============ Utilities ============
26
27
 
@@ -169,6 +170,11 @@ export async function handleHealthCheck() {
169
170
  token_updated: creds.updatedAt || null
170
171
  });
171
172
  } catch (e) {
173
+ // OAuth Lifeboat: a dead session token here is the most common first
174
+ // signal of token death — hand back recovery guidance, not a bare error.
175
+ if (isAuthDeath(e)) {
176
+ return asMcpJson(buildLifeboatPayload(e), true);
177
+ }
172
178
  return asMcpJson({
173
179
  status: "error",
174
180
  code: "auth_failed",
@@ -336,14 +342,17 @@ export async function handleListConversations(args) {
336
342
  export async function handleConversationsHistory(args) {
337
343
  const resolveUsers = args.resolve_users !== false;
338
344
  const includeRichMessageFields = parseBool(args.include_rich_message_fields);
339
- const result = await slackAPI("conversations.history", {
345
+ const historyParams = {
340
346
  channel: args.channel_id,
341
347
  limit: args.limit || 50,
342
348
  oldest: args.oldest,
343
349
  latest: args.latest,
344
- inclusive: true,
345
350
  include_all_metadata: parseBool(args.include_all_metadata)
346
- });
351
+ };
352
+ // Only opt into boundary-inclusive reads when a boundary was actually
353
+ // provided — otherwise leave Slack's default behavior untouched.
354
+ if (args.oldest || args.latest) historyParams.inclusive = true;
355
+ const result = await slackAPI("conversations.history", historyParams);
347
356
 
348
357
  const messages = await Promise.all((result.messages || []).map(async (msg) => {
349
358
  const userName = resolveUsers ? await resolveUser(msg.user) : msg.user;
@@ -384,15 +393,18 @@ export async function handleGetFullConversation(args) {
384
393
 
385
394
  // Fetch all messages with pagination
386
395
  while (hasMore && allMessages.length < maxMessages) {
387
- const result = await slackAPI("conversations.history", {
396
+ const historyParams = {
388
397
  channel: args.channel_id,
389
398
  limit: Math.min(100, maxMessages - allMessages.length),
390
399
  oldest: args.oldest,
391
400
  latest: args.latest,
392
401
  cursor,
393
- inclusive: true,
394
402
  include_all_metadata: parseBool(args.include_all_metadata)
395
- });
403
+ };
404
+ // Boundary-inclusive only when a boundary was actually provided —
405
+ // otherwise leave Slack's default behavior untouched.
406
+ if (args.oldest || args.latest) historyParams.inclusive = true;
407
+ const result = await slackAPI("conversations.history", historyParams);
396
408
 
397
409
  for (const msg of result.messages || []) {
398
410
  const userName = await resolveUser(msg.user);
@@ -738,13 +750,32 @@ export async function handleConversationsUnreads(args) {
738
750
  /**
739
751
  * Search users handler - client-side filter on users.list
740
752
  */
753
+ // Explicit scan cap for client-side user search: stop paginating after
754
+ // scanning this many workspace users (or when the cursor is exhausted) and
755
+ // flag the result as truncated so total_matches is never misreported as
756
+ // complete.
757
+ const USERS_SEARCH_MAX_SCANNED = 1000;
758
+
741
759
  export async function handleUsersSearch(args) {
742
- const query = (args.query || "").toLowerCase();
760
+ const rawQuery = typeof args.query === "string" ? args.query : "";
761
+ const query = rawQuery.trim().toLowerCase();
743
762
  const limit = args.limit || 20;
744
763
 
745
- // Fetch all users (paginated)
764
+ // An empty/whitespace query would match every user in the workspace.
765
+ if (!query) {
766
+ return asMcpJson({
767
+ status: "error",
768
+ code: "invalid_arguments",
769
+ message: "query must be a non-empty string (an empty query would match all users).",
770
+ next_action: "Provide a name, display name, real name, or email fragment to search for."
771
+ }, true);
772
+ }
773
+
774
+ // Fetch users (paginated) and filter client-side
746
775
  const allUsers = [];
747
776
  let cursor;
777
+ let scannedUsers = 0;
778
+ let truncated = false;
748
779
 
749
780
  do {
750
781
  const result = await slackAPI("users.list", {
@@ -753,6 +784,7 @@ export async function handleUsersSearch(args) {
753
784
  });
754
785
 
755
786
  for (const u of (result.members || [])) {
787
+ scannedUsers++;
756
788
  if (u.deleted || u.is_bot || u.id === "USLACKBOT") continue;
757
789
 
758
790
  const searchFields = [
@@ -776,13 +808,18 @@ export async function handleUsersSearch(args) {
776
808
  }
777
809
 
778
810
  cursor = result.response_metadata?.next_cursor;
811
+ if (cursor && scannedUsers >= USERS_SEARCH_MAX_SCANNED) {
812
+ truncated = true;
813
+ break;
814
+ }
779
815
  if (cursor) await sleep(100);
780
- } while (cursor && allUsers.length < 500);
816
+ } while (cursor);
781
817
 
782
818
  return asMcpJson({
783
819
  query: args.query,
784
820
  count: Math.min(allUsers.length, limit),
785
821
  total_matches: allUsers.length,
822
+ truncated,
786
823
  users: allUsers.slice(0, limit)
787
824
  });
788
825
  }
@@ -840,8 +877,8 @@ const HOSTED_UPGRADE_PAYLOAD = {
840
877
  message: "This tool needs hosted mode (Vectorize + Workers AI). Get free monthly credits at mcp.revasserlabs.com — no card required.",
841
878
  signup_url: "https://mcp.revasserlabs.com/signup",
842
879
  upgrade_url: "https://mcp.revasserlabs.com/pricing",
843
- 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).",
880
+ free_tier_quota: "2,000 requests/mo + 25 AI tool calls/mo (no card)",
881
+ pro_value_prop: "Pro $19/mo unlocks unlimited requests and AI tool calls, permanent OAuth (no token rotation).",
845
882
  };
846
883
 
847
884
  export async function handleSmartSearch(args) {
@@ -873,3 +910,35 @@ export async function handleTriage(args) {
873
910
  true
874
911
  );
875
912
  }
913
+
914
+ // ============ Shared Tool Dispatch Map ============
915
+ // Single source of truth mapping every advertised tool name (lib/tools.js)
916
+ // to its handler. Both transports — stdio (src/server.js) and HTTP
917
+ // (src/server-http.js) — dispatch through this map so the advertised tool
918
+ // list and the dispatch surface cannot drift apart.
919
+
920
+ export const TOOL_HANDLERS = Object.freeze({
921
+ slack_token_status: handleTokenStatus,
922
+ slack_health_check: handleHealthCheck,
923
+ slack_refresh_tokens: handleRefreshTokens,
924
+ slack_list_conversations: handleListConversations,
925
+ slack_conversations_history: handleConversationsHistory,
926
+ slack_get_full_conversation: handleGetFullConversation,
927
+ slack_search_messages: handleSearchMessages,
928
+ slack_users_info: handleUsersInfo,
929
+ slack_send_message: handleSendMessage,
930
+ slack_get_thread: handleGetThread,
931
+ slack_list_users: handleListUsers,
932
+ slack_add_reaction: handleAddReaction,
933
+ slack_remove_reaction: handleRemoveReaction,
934
+ slack_conversations_mark: handleConversationsMark,
935
+ slack_conversations_unreads: handleConversationsUnreads,
936
+ slack_users_search: handleUsersSearch,
937
+ // Workflow profile primitives (OSS local JSON store)
938
+ slack_workflow_save: handleWorkflowSave,
939
+ slack_workflows: handleWorkflows,
940
+ // Hosted-only AI tools (OSS = upgrade stubs)
941
+ slack_smart_search: handleSmartSearch,
942
+ slack_catch_me_up: handleCatchMeUp,
943
+ slack_triage: handleTriage,
944
+ });
@@ -0,0 +1,171 @@
1
+ /**
2
+ * OAuth Lifeboat
3
+ *
4
+ * Detects Slack session-token death at the moment of pain and returns a
5
+ * genuinely helpful recovery message instead of a raw Slack API error.
6
+ *
7
+ * Session tokens (xoxc/xoxd) are extracted from the browser and Slack rotates
8
+ * them roughly every 1-2 weeks. When they die, every tool call fails with an
9
+ * auth error. This module classifies that failure and hands the user the
10
+ * self-fix first (re-extract) and the permanent fix second (hosted OAuth).
11
+ *
12
+ * Design rules:
13
+ * - Self-fix is always shown; the hosted option is shown second and is
14
+ * suppressed entirely by SLACK_MCP_NO_UPSELL=1.
15
+ * - The long-form message is emitted at most once per process per hour;
16
+ * subsequent failures inside that window get a one-line version so agents
17
+ * in retry loops do not spam.
18
+ * - Never reads or logs token values — it only inspects error codes.
19
+ */
20
+
21
+ // Slack API error codes (and the HTTP 401 case) that mean the session token
22
+ // is dead and cannot be auto-healed — the point where the Lifeboat fires.
23
+ export const AUTH_DEATH_SLACK_CODES = new Set([
24
+ "invalid_auth",
25
+ "not_authed",
26
+ "token_expired",
27
+ "token_revoked",
28
+ "account_inactive",
29
+ ]);
30
+
31
+ const THROTTLE_WINDOW_MS = 60 * 60 * 1000; // one long-form message per process per hour
32
+
33
+ const SETUP_CMD = "npx -y @jtalk22/slack-mcp --setup";
34
+ const README_ANCHOR_URL =
35
+ "https://github.com/jtalk22/slack-mcp-server#token-expired--oauth-lifeboat";
36
+ const HOSTED_SETUP_URL =
37
+ "https://mcp.revasserlabs.com/setup?utm_source=lifeboat&utm_medium=npm&utm_campaign=token_death";
38
+
39
+ // Module-level throttle state (shared across every transport in one process).
40
+ let lastLongFormAt = 0;
41
+
42
+ /**
43
+ * Reset the throttle. Intended for tests.
44
+ */
45
+ export function resetLifeboatThrottle() {
46
+ lastLongFormAt = 0;
47
+ }
48
+
49
+ function upsellEnabled() {
50
+ return process.env.SLACK_MCP_NO_UPSELL !== "1";
51
+ }
52
+
53
+ /**
54
+ * Pull the specific Slack auth-death code out of an error, if present.
55
+ * Returns the code string (e.g. "token_revoked") or null.
56
+ */
57
+ export function authDeathCode(err) {
58
+ if (!err) return null;
59
+
60
+ if (typeof err === "string") {
61
+ const s = err.trim().toLowerCase();
62
+ return AUTH_DEATH_SLACK_CODES.has(s) ? s : null;
63
+ }
64
+
65
+ const slackCode =
66
+ typeof err.slack_error === "string" ? err.slack_error.trim().toLowerCase() : null;
67
+ if (slackCode && AUTH_DEATH_SLACK_CODES.has(slackCode)) return slackCode;
68
+
69
+ const msg = typeof err.message === "string" ? err.message.trim().toLowerCase() : "";
70
+ if (AUTH_DEATH_SLACK_CODES.has(msg)) return msg;
71
+ for (const code of AUTH_DEATH_SLACK_CODES) {
72
+ if (msg.includes(code)) return code;
73
+ }
74
+
75
+ return null;
76
+ }
77
+
78
+ /**
79
+ * Classify an error as AUTH-DEATH.
80
+ * Accepts a thrown Error (optionally carrying `.slack_error`, `.code`,
81
+ * `.status`), a raw Slack error-code string, or an HTTP-401 signal.
82
+ */
83
+ export function isAuthDeath(err) {
84
+ if (!err) return false;
85
+
86
+ // The wrapped auth-failure lib/slack-client.js throws after a failed auto-heal.
87
+ if (typeof err === "object" && err.code === "token_auth_failed") return true;
88
+
89
+ if (authDeathCode(err)) return true;
90
+
91
+ // HTTP 401 (defensive — Slack normally returns HTTP 200 with ok:false).
92
+ if (typeof err === "string") {
93
+ const s = err.trim().toLowerCase();
94
+ return s === "401" || s.includes("http 401") || s.includes("status 401");
95
+ }
96
+ const status = err.status ?? err.statusCode ?? err.httpStatus ?? null;
97
+ if (status === 401) return true;
98
+ const msg = typeof err.message === "string" ? err.message.toLowerCase() : "";
99
+ return msg.includes("http 401") || msg.includes("status 401");
100
+ }
101
+
102
+ /**
103
+ * Build the structured recovery payload for an auth-death error.
104
+ * First call within the throttle window returns the long form; later calls
105
+ * return the one-line form. Honors SLACK_MCP_NO_UPSELL.
106
+ */
107
+ export function buildLifeboatPayload(err, options = {}) {
108
+ const now = options.now ?? Date.now();
109
+ const slackError = authDeathCode(err);
110
+ const upsell = upsellEnabled();
111
+
112
+ const throttled = lastLongFormAt > 0 && now - lastLongFormAt < THROTTLE_WINDOW_MS;
113
+
114
+ if (throttled) {
115
+ const message = upsell
116
+ ? `Slack session token is still expired or revoked. Re-extract with \`${SETUP_CMD}\`. Permanent fix (hosted OAuth, free tier): ${HOSTED_SETUP_URL}`
117
+ : `Slack session token is still expired or revoked. Re-extract with \`${SETUP_CMD}\`.`;
118
+ return {
119
+ status: "error",
120
+ code: "slack_auth_expired",
121
+ slack_error: slackError,
122
+ throttled: true,
123
+ message,
124
+ next_action: `Run \`${SETUP_CMD}\`.`,
125
+ };
126
+ }
127
+
128
+ lastLongFormAt = now;
129
+
130
+ const payload = {
131
+ status: "error",
132
+ code: "slack_auth_expired",
133
+ slack_error: slackError,
134
+ throttled: false,
135
+ message:
136
+ "Your Slack session token has expired or been revoked. Slack rotates browser session tokens (xoxc/xoxd) roughly every 1-2 weeks, so this is expected — the server can no longer authenticate until you supply fresh tokens.",
137
+ self_fix:
138
+ `Re-extract fresh tokens: run \`${SETUP_CMD}\`. On macOS with Slack open in Chrome you can instead call the slack_refresh_tokens tool. Full steps: ${README_ANCHOR_URL}`,
139
+ };
140
+
141
+ // Preserve the auto-heal diagnostic when the wrapped error carried one.
142
+ if (err && typeof err === "object" && err.extraction_error) {
143
+ payload.extraction_error = err.extraction_error;
144
+ }
145
+
146
+ if (upsell) {
147
+ payload.hosted_option =
148
+ `To stop re-extracting every 1-2 weeks, switch to hosted OAuth (it never rotates): ${HOSTED_SETUP_URL} — free tier available, no credit card.`;
149
+ payload.next_action = `Run \`${SETUP_CMD}\` to re-extract tokens, or switch to permanent hosted OAuth (see hosted_option).`;
150
+ } else {
151
+ payload.next_action = `Run \`${SETUP_CMD}\` to re-extract tokens (macOS: call the slack_refresh_tokens tool).`;
152
+ }
153
+
154
+ return payload;
155
+ }
156
+
157
+ /**
158
+ * Wrap the recovery payload as an MCP tool-error response.
159
+ * Used by both transports (stdio + HTTP) so the recovery surface is identical.
160
+ */
161
+ export function lifeboatResponse(err) {
162
+ return {
163
+ content: [
164
+ {
165
+ type: "text",
166
+ text: JSON.stringify(buildLifeboatPayload(err), null, 2),
167
+ },
168
+ ],
169
+ isError: true,
170
+ };
171
+ }
@@ -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 $19/mo unlocks unlimited AI tools and permanent OAuth.`;
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 $19/mo unlocks unlimited AI tools and permanent OAuth.`;
79
79
  }
80
80
 
81
81
  function demoFooterLinks() {
@@ -113,7 +113,7 @@ function commonTokens() {
113
113
  SELF_HOSTED_TOOL_COUNT: String(PUBLIC_METADATA.selfHostedToolCount),
114
114
  CLOUD_MANAGED_TOOL_COUNT: "15",
115
115
  TEAM_AI_WORKFLOW_COUNT: "3",
116
- CLOUD_SOLO_PRICE: "$9/mo",
116
+ CLOUD_SOLO_PRICE: "$19/mo",
117
117
  CLOUD_TEAM_PRICE: "$49/mo",
118
118
  CLOUD_TURNKEY_LAUNCH_PRICE: "contact us",
119
119
  CLOUD_MANAGED_RELIABILITY_PRICE: "contact us",
@@ -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
@@ -504,7 +504,7 @@ export const TOOLS = [
504
504
  // pointing at mcp.revasserlabs.com — the hosted worker actually runs them.
505
505
  {
506
506
  name: "slack_smart_search",
507
- description: "Semantic + lexical hybrid search across your indexed Slack history. Returns ranked results with relevance scores, channel context, thread context, and matched terms. Hosted-only (requires Vectorize + Workers AI). Free tier ships 10 calls/month; upgrade to Pro $9/mo for unlimited at mcp.revasserlabs.com/pricing.",
507
+ description: "Semantic + lexical hybrid search across your indexed Slack history. Returns ranked results with relevance scores, channel context, thread context, and matched terms. Hosted-only (requires Vectorize + Workers AI). Free tier ships 25 AI tool calls/month (shared across the hosted AI tools); upgrade to Pro $19/mo for unlimited at mcp.revasserlabs.com/pricing.",
508
508
  inputSchema: {
509
509
  type: "object",
510
510
  properties: {
@@ -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 25 AI tool calls/month (shared across the hosted AI tools); Pro $19/mo unlocks unlimited.",
541
541
  inputSchema: {
542
542
  type: "object",
543
543
  properties: {
@@ -561,7 +561,7 @@ export const TOOLS = [
561
561
  },
562
562
  {
563
563
  name: "slack_triage",
564
- description: "Classify and route Slack threads against a workflow profile. Returns triage decisions per thread: priority (low|medium|high|urgent), suggested owner, escalation flag, time-sensitivity, and a routing recommendation. Hosted-only. Free tier ships 5 triage runs per day; Pro $9/mo unlocks unlimited.",
564
+ description: "Classify and route Slack threads against a workflow profile. Returns triage decisions per thread: priority (low|medium|high|urgent), suggested owner, escalation flag, time-sensitivity, and a routing recommendation. Hosted-only. Free tier ships 25 AI tool calls/month (shared across the hosted AI tools); Pro $19/mo unlocks unlimited.",
565
565
  inputSchema: {
566
566
  type: "object",
567
567
  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.1",
5
- "description": "Slack MCP without OAuth \u2014 21 tools, session-based, local-first. Free OSS + hosted tier from $9/mo.",
4
+ "version": "4.4.3",
5
+ "description": "Slack MCP without OAuth 21 tools, session-based, local-first. Free OSS + hosted tier from $19/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 $19/mo unlocks unlimited AI tools and permanent OAuth.</p>
128
128
  </main>
129
129
  </body>
130
130
  </html>
@@ -43,7 +43,7 @@ function printUsage() {
43
43
  console.log(" slack-mcp --apply-template support-triage --channels C012345,C067890");
44
44
  console.log("");
45
45
  console.log("Templates write to ~/.slack-mcp-workflows.json. The hosted AI brain at");
46
- console.log("mcp.revasserlabs.com (free tier or Pro $9/mo) reads these profiles and");
46
+ console.log("mcp.revasserlabs.com (free tier or Pro $19/mo) reads these profiles and");
47
47
  console.log("returns structured JSON per the workflow_kind. The OSS package ships the");
48
48
  console.log("profile primitives + 3 discoverable upgrade stubs (slack_smart_search,");
49
49
  console.log("slack_catch_me_up, slack_triage). The brain is hosted-only.");
@@ -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: 25 AI tool calls/month. Pro $19/mo unlocks unlimited.)`);
117
117
  }
@@ -474,7 +474,7 @@ async function showHelp() {
474
474
  print(" https://github.com/jtalk22/slack-mcp-server");
475
475
  print();
476
476
  print(`${colors.bold}Hosted tier:${colors.reset}`);
477
- print(" https://mcp.revasserlabs.com — $9/mo Pro, permanent OAuth,");
477
+ print(" https://mcp.revasserlabs.com — $19/mo Pro, permanent OAuth,");
478
478
  print(" semantic search, workflow continuity across channels.");
479
479
  }
480
480
 
@@ -548,7 +548,7 @@ async function main() {
548
548
  print(" • Or add to Claude Desktop config");
549
549
  print();
550
550
  print(`${colors.dim}Want permanent tokens, semantic search, and workflow continuity?${colors.reset}`);
551
- print(`${colors.dim}Hosted tier: https://mcp.revasserlabs.com — $9/mo Pro, 10 free paid calls.${colors.reset}`);
551
+ print(`${colors.dim}Hosted tier: https://mcp.revasserlabs.com — $19/mo Pro, 25 free AI calls/mo.${colors.reset}`);
552
552
  } else {
553
553
  print(`${colors.red}Setup failed.${colors.reset} See errors above.`);
554
554
  process.exit(1);
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 \u2014 21 tools, session-based, local-first. Free OSS + hosted tier from $9/mo.",
5
+ "description": "Slack MCP without OAuth \u2014 21 tools, session-based, local-first. Free OSS + hosted tier from $19/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.1",
20
+ "version": "4.4.3",
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.1",
31
+ "version": "4.4.3",
32
32
  "transport": {
33
33
  "type": "stdio"
34
34
  },
@@ -15,25 +15,9 @@ 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";
20
+ import { isAuthDeath, lifeboatResponse } from "../lib/lifeboat.js";
37
21
 
38
22
  const SERVER_NAME = "slack-mcp-server";
39
23
  const SERVER_VERSION = RELEASE_VERSION;
@@ -98,54 +82,29 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
98
82
  const { name, arguments: args } = request.params;
99
83
 
100
84
  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
- };
85
+ // Shared dispatch map keeps this transport in lockstep with the stdio
86
+ // server and the advertised TOOLS list.
87
+ const handler = TOOL_HANDLERS[name];
88
+ if (!handler) {
89
+ return {
90
+ content: [{
91
+ type: "text",
92
+ text: JSON.stringify({
93
+ status: "error",
94
+ code: "unknown_tool",
95
+ message: `Unknown tool: ${name}`,
96
+ next_action: "Call tools/list to inspect available tool names."
97
+ }, null, 2)
98
+ }],
99
+ isError: true
100
+ };
147
101
  }
102
+ return await handler(args);
148
103
  } catch (error) {
104
+ // OAuth Lifeboat: dead session token → recovery guidance, not a raw error.
105
+ if (isAuthDeath(error)) {
106
+ return lifeboatResponse(error);
107
+ }
149
108
  return {
150
109
  content: [{
151
110
  type: "text",
package/src/server.js CHANGED
@@ -30,28 +30,11 @@ 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";
37
+ import { isAuthDeath, lifeboatResponse } from "../lib/lifeboat.js";
55
38
 
56
39
  // Background refresh interval (4 hours)
57
40
  const BACKGROUND_REFRESH_INTERVAL = 4 * 60 * 60 * 1000;
@@ -231,103 +214,29 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
231
214
  const { name, arguments: args } = request.params;
232
215
 
233
216
  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
- };
313
- }
314
- } catch (error) {
315
- if (error?.code === "token_auth_failed") {
217
+ // Shared dispatch map (lib/handlers.js) keeps this transport in lockstep
218
+ // with the HTTP server and the advertised TOOLS list.
219
+ const handler = TOOL_HANDLERS[name];
220
+ if (!handler) {
316
221
  return {
317
222
  content: [{
318
223
  type: "text",
319
224
  text: JSON.stringify({
320
225
  status: "error",
321
- code: "token_auth_failed",
322
- message: String(error?.message || error),
323
- slack_error: error.slack_error || null,
324
- extraction_error: error.extraction_error || null,
325
- next_action: error.next_action || "Open http://localhost:3000 and click Refresh, OR run `npm run tokens:auto` with Slack open in Chrome, OR check Chrome > View > Developer > Allow JavaScript from Apple Events."
226
+ code: "unknown_tool",
227
+ message: `Unknown tool: ${name}`,
228
+ next_action: "Use tools/list to discover available tools."
326
229
  }, null, 2)
327
230
  }],
328
231
  isError: true
329
232
  };
330
233
  }
234
+ return await handler(args);
235
+ } catch (error) {
236
+ // OAuth Lifeboat: dead session token → recovery guidance, not a raw error.
237
+ if (isAuthDeath(error)) {
238
+ return lifeboatResponse(error);
239
+ }
331
240
  return {
332
241
  content: [{
333
242
  type: "text",