@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 +22 -6
- package/docs/DEPLOYMENT-MODES.md +1 -1
- package/docs/TROUBLESHOOTING.md +1 -1
- package/lib/handlers.js +80 -11
- package/lib/lifeboat.js +171 -0
- package/lib/public-pages.js +3 -3
- package/lib/token-store.js +16 -4
- package/lib/tools.js +3 -3
- package/lib/workflow-store.js +27 -12
- package/package.json +2 -2
- package/public/share.html +1 -1
- package/scripts/apply-template.js +2 -2
- package/scripts/setup-wizard.js +2 -2
- package/server.json +3 -3
- package/src/server-http.js +23 -64
- package/src/server.js +15 -106
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 `$
|
|
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,
|
|
272
|
-
| Pro | $
|
|
273
|
-
| Team | $49/mo flat | Pro + shared workflow profiles
|
|
274
|
-
|
|
|
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), $
|
|
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.
|
package/docs/DEPLOYMENT-MODES.md
CHANGED
|
@@ -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 $
|
|
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
|
|
package/docs/TROUBLESHOOTING.md
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
//
|
|
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
|
|
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: "
|
|
844
|
-
pro_value_prop: "Pro $
|
|
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
|
+
});
|
package/lib/lifeboat.js
ADDED
|
@@ -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
|
+
}
|
package/lib/public-pages.js
CHANGED
|
@@ -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 $
|
|
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 $
|
|
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: "$
|
|
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",
|
package/lib/token-store.js
CHANGED
|
@@ -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 {
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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: {
|
package/lib/workflow-store.js
CHANGED
|
@@ -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
|
-
|
|
73
|
+
quarantineCorruptStore("shape-invalid");
|
|
74
74
|
return emptyStore();
|
|
75
75
|
}
|
|
76
76
|
if (data.version !== STORE_VERSION) {
|
|
77
|
-
// Future: migration logic. For now,
|
|
78
|
-
// before falling back to empty so the old data is recoverable.
|
|
79
|
-
|
|
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
|
-
|
|
84
|
+
quarantineCorruptStore("json-parse-error");
|
|
85
85
|
return emptyStore();
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
|
|
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
|
-
|
|
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(
|
|
102
|
+
try { chmodSync(quarantinePath, 0o600); } catch {}
|
|
96
103
|
}
|
|
97
|
-
|
|
98
|
-
|
|
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.
|
|
5
|
-
"description": "Slack MCP without OAuth
|
|
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 $
|
|
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 $
|
|
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:
|
|
116
|
+
console.log(`(Free tier: 25 AI tool calls/month. Pro $19/mo unlocks unlimited.)`);
|
|
117
117
|
}
|
package/scripts/setup-wizard.js
CHANGED
|
@@ -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 — $
|
|
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 — $
|
|
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 $
|
|
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.
|
|
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.
|
|
31
|
+
"version": "4.4.3",
|
|
32
32
|
"transport": {
|
|
33
33
|
"type": "stdio"
|
|
34
34
|
},
|
package/src/server-http.js
CHANGED
|
@@ -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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
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
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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: "
|
|
322
|
-
message:
|
|
323
|
-
|
|
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",
|