@jtalk22/slack-mcp 4.0.0 → 4.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +106 -11
- package/docs/SETUP.md +2 -34
- package/docs/TROUBLESHOOTING.md +2 -24
- package/lib/handlers.js +1 -1
- package/lib/public-pages.js +11 -11
- package/lib/slack-client.js +70 -17
- package/lib/token-store.js +365 -68
- package/package.json +11 -3
- package/public/index.html +2 -2
- package/public/share.html +9 -9
- package/scripts/setup-wizard.js +74 -25
- package/server.json +3 -3
- package/src/server.js +36 -2
package/README.md
CHANGED
|
@@ -10,11 +10,13 @@ Give your AI agent full Slack access. No app registration, no admin approval, no
|
|
|
10
10
|
npx -y @jtalk22/slack-mcp --setup
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
](https://jtalk22.github.io/slack-mcp-server/public/demo-video.html)
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
**[▶ Watch the demo](https://jtalk22.github.io/slack-mcp-server/public/demo-video.html)** — 7 scenarios, from 47 unreads to inbox zero, without opening Slack.
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
> **Ask your AI to catch you up on #engineering from the last 24 hours.** Search for that deployment thread from last week. Find the printer admin PIN nobody can remember. Send a reply. All from your editor.
|
|
18
|
+
|
|
19
|
+
[Interactive demo](https://jtalk22.github.io/slack-mcp-server/public/demo-slack-mcp.html) · [Latest release](https://github.com/jtalk22/slack-mcp-server/releases/latest)
|
|
18
20
|
|
|
19
21
|
## Why This Exists
|
|
20
22
|
|
|
@@ -22,7 +24,90 @@ Slack's official MCP server requires a registered app, admin approval, and [does
|
|
|
22
24
|
|
|
23
25
|
This server uses your browser's session tokens instead. If you can see it in Slack, your AI agent can see it too. No app install, no scopes, no admin.
|
|
24
26
|
|
|
25
|
-
|
|
27
|
+
**Session-token transport:** No bot user appears in the workspace admin panel, no app install shows up, no audit trail entry is created. Your AI agent operates with the same workspace footprint as your browser tab — nothing more, nothing less.
|
|
28
|
+
|
|
29
|
+

|
|
30
|
+
|
|
31
|
+
| | Slack Official MCP | This Server |
|
|
32
|
+
|---|---|---|
|
|
33
|
+
| OAuth app required | Yes | **No** |
|
|
34
|
+
| Admin approval | Yes | **No** |
|
|
35
|
+
| Works with Claude Code | No (DCR incompatible) | **Yes** |
|
|
36
|
+
| Works with Cursor | No | **Yes** |
|
|
37
|
+
| Works with Copilot | No | **Yes** |
|
|
38
|
+
| Works with Windsurf | No | **Yes** |
|
|
39
|
+
| Works with Gemini CLI | No | **Yes** |
|
|
40
|
+
| Works with Codex CLI | No | **Yes** |
|
|
41
|
+
| Setup time | ~30 min | **~2 min** |
|
|
42
|
+
| Tools | Limited | **16** |
|
|
43
|
+
| Visible to admins | Yes | **No — session-token transport** |
|
|
44
|
+
|
|
45
|
+
## Quick Start per Client
|
|
46
|
+
|
|
47
|
+
<details>
|
|
48
|
+
<summary><strong>Claude Desktop / Claude Code</strong></summary>
|
|
49
|
+
|
|
50
|
+
Add to `~/.claude.json` or Claude Desktop settings:
|
|
51
|
+
```json
|
|
52
|
+
{
|
|
53
|
+
"mcpServers": {
|
|
54
|
+
"slack": { "command": "npx", "args": ["-y", "@jtalk22/slack-mcp"] }
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
</details>
|
|
59
|
+
|
|
60
|
+
<details>
|
|
61
|
+
<summary><strong>Cursor</strong></summary>
|
|
62
|
+
|
|
63
|
+
Add to `.cursor/mcp.json`:
|
|
64
|
+
```json
|
|
65
|
+
{
|
|
66
|
+
"mcpServers": {
|
|
67
|
+
"slack": { "command": "npx", "args": ["-y", "@jtalk22/slack-mcp"] }
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
</details>
|
|
72
|
+
|
|
73
|
+
<details>
|
|
74
|
+
<summary><strong>Windsurf</strong></summary>
|
|
75
|
+
|
|
76
|
+
Add to `~/.codeium/windsurf/mcp_config.json`:
|
|
77
|
+
```json
|
|
78
|
+
{
|
|
79
|
+
"mcpServers": {
|
|
80
|
+
"slack": { "command": "npx", "args": ["-y", "@jtalk22/slack-mcp"] }
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
</details>
|
|
85
|
+
|
|
86
|
+
<details>
|
|
87
|
+
<summary><strong>Gemini CLI</strong></summary>
|
|
88
|
+
|
|
89
|
+
Add to `~/.gemini/settings.json`:
|
|
90
|
+
```json
|
|
91
|
+
{
|
|
92
|
+
"mcpServers": {
|
|
93
|
+
"slack": { "command": "npx", "args": ["-y", "@jtalk22/slack-mcp"] }
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
</details>
|
|
98
|
+
|
|
99
|
+
<details>
|
|
100
|
+
<summary><strong>Codex CLI</strong></summary>
|
|
101
|
+
|
|
102
|
+
Add to `~/.codex/config.toml`:
|
|
103
|
+
```toml
|
|
104
|
+
[mcp_servers.slack]
|
|
105
|
+
command = "npx"
|
|
106
|
+
args = ["-y", "@jtalk22/slack-mcp"]
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Or via CLI: `codex mcp add slack -- npx -y @jtalk22/slack-mcp`
|
|
110
|
+
</details>
|
|
26
111
|
|
|
27
112
|
## Tools
|
|
28
113
|
|
|
@@ -145,13 +230,15 @@ On macOS, tokens are auto-extracted from Chrome — `env` block is optional.
|
|
|
145
230
|
<details>
|
|
146
231
|
<summary><strong>Claude Web / Remote MCP</strong></summary>
|
|
147
232
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
```
|
|
151
|
-
https://mcp.revasserlabs.com/oauth/mcp
|
|
152
|
-
```
|
|
233
|
+
Hosted tiers at [mcp.revasserlabs.com](https://mcp.revasserlabs.com):
|
|
153
234
|
|
|
154
|
-
|
|
235
|
+
| Tier | Price | What it owns |
|
|
236
|
+
|------|-------|-------------|
|
|
237
|
+
| Self-host | Free | Local stdio, all 16 tools, MIT licensed |
|
|
238
|
+
| Solo | $19/mo | Managed endpoint + OAuth 2.1 bridge (required for Claude.ai web) + encrypted storage |
|
|
239
|
+
| Team | $49/mo | Solo features + multi-seat routing |
|
|
240
|
+
| Turnkey Team Launch | from $2,500+ | Dedicated instance + 30-day setup support |
|
|
241
|
+
| Managed Reliability | from $800/mo+ | SLA-backed instance + incident response |
|
|
155
242
|
|
|
156
243
|
</details>
|
|
157
244
|
|
|
@@ -191,6 +278,14 @@ Session tokens (`xoxc-` + `xoxd-`) from your browser. If you can see it in Slack
|
|
|
191
278
|
|
|
192
279
|
Tokens expire. The server notices before you do — proactive health monitoring, automatic refresh on macOS, warnings when tokens age out. File writes are atomic (temp file → chmod → rename) to prevent corruption. Concurrent refresh attempts are mutex-locked.
|
|
193
280
|
|
|
281
|
+
## What's New in 4.1.2
|
|
282
|
+
|
|
283
|
+
- **LevelDB extraction** — reads tokens directly from Chrome's LevelDB store. No live Slack tab required, no AppleScript flag dependency.
|
|
284
|
+
- **Multi-profile enumeration** — automatically picks the freshest Chrome profile. Override with `SLACK_MCP_CHROME_USER_DATA_DIR`, `SLACK_MCP_CHROME_PROFILE`, or `SLACK_MCP_EXTRACTION_MODE`.
|
|
285
|
+
- **Explicit shutdown handlers** — SIGTERM/SIGINT/SIGHUP/stdin EOF/stdin error all exit cleanly. Zero zombie processes.
|
|
286
|
+
|
|
287
|
+
Full release notes in [docs/INDEX.md](docs/INDEX.md) and on [GitHub releases/latest](https://github.com/jtalk22/slack-mcp-server/releases/latest).
|
|
288
|
+
|
|
194
289
|
## Hosted HTTP Mode
|
|
195
290
|
|
|
196
291
|
For remote MCP endpoints (Cloudflare Worker, VPS, etc.):
|
|
@@ -247,4 +342,4 @@ Not affiliated with Slack Technologies, Inc. Uses browser session credentials
|
|
|
247
342
|
|
|
248
343
|
---
|
|
249
344
|
|
|
250
|
-
|
|
345
|
+
Hosted tiers live at [mcp.revasserlabs.com](https://mcp.revasserlabs.com): Solo $19/mo, Team $49/mo, Turnkey Team Launch from $2,500+, Managed Reliability from $800/mo+. Hosted owns the managed MCP endpoint, the OAuth 2.1 bridge into Claude.ai, encrypted credential storage, and the structural absence of the zombie-process class. It does not replace Chrome — the user still pastes `xoxc-`/`xoxd-` at setup.
|
package/docs/SETUP.md
CHANGED
|
@@ -1,40 +1,8 @@
|
|
|
1
1
|
# Setup Guide
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## Hosted (Coming Soon)
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
1. Go to [Slack MCP Cloud](https://mcp.revasserlabs.com) and purchase a plan ($19/mo Solo, $49/mo Team)
|
|
8
|
-
2. After checkout, you'll receive an API key and ready-to-paste config for Claude Desktop / Claude Code
|
|
9
|
-
3. Use the hosted endpoint and API key — one URL, 15 managed tools. Team adds 3 AI workflows.
|
|
10
|
-
|
|
11
|
-
**Claude Desktop config (Cloud):**
|
|
12
|
-
```json
|
|
13
|
-
{
|
|
14
|
-
"mcpServers": {
|
|
15
|
-
"slack": {
|
|
16
|
-
"url": "https://mcp.revasserlabs.com/oauth/mcp"
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
**Claude Code config (Cloud):**
|
|
23
|
-
```json
|
|
24
|
-
{
|
|
25
|
-
"mcpServers": {
|
|
26
|
-
"slack": {
|
|
27
|
-
"type": "sse",
|
|
28
|
-
"url": "https://mcp.revasserlabs.com/mcp",
|
|
29
|
-
"headers": {
|
|
30
|
-
"Authorization": "Bearer YOUR_API_KEY"
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
If you prefer self-hosting, continue below.
|
|
5
|
+
A hosted version with permanent OAuth tokens, semantic search, and AI summaries is in development at [mcp.revasserlabs.com](https://mcp.revasserlabs.com). The current release is self-hosted only — continue below.
|
|
38
6
|
|
|
39
7
|
---
|
|
40
8
|
|
package/docs/TROUBLESHOOTING.md
CHANGED
|
@@ -29,31 +29,9 @@ If `--version` fails here, the issue is install/runtime path, not Slack credenti
|
|
|
29
29
|
|
|
30
30
|
---
|
|
31
31
|
|
|
32
|
-
##
|
|
32
|
+
## Hosted Version
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
**Symptom:** `401 Unauthorized` or `403 Forbidden` when using Cloud endpoint.
|
|
37
|
-
|
|
38
|
-
**Solutions:**
|
|
39
|
-
1. Verify your API key starts with `stmh_` (team) or `smsh_` (solo)
|
|
40
|
-
2. Check the key hasn't been revoked — contact support@revasserlabs.com for key issues
|
|
41
|
-
3. Ensure you're using the correct endpoint: `https://mcp.revasserlabs.com/mcp`
|
|
42
|
-
|
|
43
|
-
### Cloud Tools Not Available
|
|
44
|
-
|
|
45
|
-
**Symptom:** Only seeing fewer tools than expected.
|
|
46
|
-
|
|
47
|
-
**Cause:** AI compound tools (`slack_channel_summary`, `slack_extract_action_items`, `slack_find_decisions`) are Team plan only ($49/mo).
|
|
48
|
-
|
|
49
|
-
**Solution:** Upgrade to Team plan for AI compound tools, or use the 15 standard managed tools available on all Cloud plans.
|
|
50
|
-
|
|
51
|
-
### Cloud Endpoint Health Check
|
|
52
|
-
|
|
53
|
-
```bash
|
|
54
|
-
curl -s https://mcp.revasserlabs.com/health | jq .
|
|
55
|
-
# Expected: {"status":"healthy","server":"slack-mcp-hosted","version":"0.5.0"}
|
|
56
|
-
```
|
|
34
|
+
A hosted version with permanent OAuth tokens, semantic search, and AI summaries is coming soon at [mcp.revasserlabs.com](https://mcp.revasserlabs.com). The current release is self-hosted only.
|
|
57
35
|
|
|
58
36
|
---
|
|
59
37
|
|
package/lib/handlers.js
CHANGED
|
@@ -216,7 +216,7 @@ export async function handleRefreshTokens() {
|
|
|
216
216
|
code: extractionError.code,
|
|
217
217
|
message: extractionError.message,
|
|
218
218
|
detail: extractionError.detail,
|
|
219
|
-
next_action: "In Chrome: View > Developer > Allow JavaScript from Apple Events, then retry."
|
|
219
|
+
next_action: "In Chrome: View > Developer > Allow JavaScript from Apple Events, then retry. (Only needed for token — cookie is extracted from Chrome's database automatically.)"
|
|
220
220
|
}, true);
|
|
221
221
|
}
|
|
222
222
|
|
package/lib/public-pages.js
CHANGED
|
@@ -15,7 +15,7 @@ const ICON_URL = `${GITHUB_PAGES_ROOT}/docs/assets/icon-512.png`;
|
|
|
15
15
|
const NPM_URL = "https://www.npmjs.com/package/@jtalk22/slack-mcp";
|
|
16
16
|
const RELEASES_URL = `${PUBLIC_METADATA.canonicalRepoUrl}/releases/latest`;
|
|
17
17
|
const SETUP_URL = `${PUBLIC_METADATA.canonicalRepoUrl}/blob/main/docs/SETUP.md`;
|
|
18
|
-
const DEMO_VIDEO_URL = `${GITHUB_PAGES_ROOT}/docs/videos/demo-
|
|
18
|
+
const DEMO_VIDEO_URL = `${GITHUB_PAGES_ROOT}/docs/videos/demo-slack-mcp-mobile-20s.mp4`;
|
|
19
19
|
|
|
20
20
|
function template(name) {
|
|
21
21
|
return readFileSync(resolve(TEMPLATE_DIR, name), "utf8");
|
|
@@ -58,28 +58,28 @@ function shareLinks() {
|
|
|
58
58
|
<a href="${GITHUB_PAGES_ROOT}/" rel="noopener">Autoplay Demo Landing</a>
|
|
59
59
|
<a href="${DEMO_VIDEO_URL}" rel="noopener">20s Mobile Clip</a>
|
|
60
60
|
<a href="${NPM_URL}" rel="noopener">npm Package</a>
|
|
61
|
-
<a href="${PUBLIC_METADATA.canonicalSiteUrl}" rel="noopener" style="background:rgba(240,194,70,0.18);border-color:rgba(240,194,70,0.45);color:#f0c246">
|
|
61
|
+
<a href="${PUBLIC_METADATA.canonicalSiteUrl}" rel="noopener" style="background:rgba(240,194,70,0.18);border-color:rgba(240,194,70,0.45);color:#f0c246">Hosted</a>
|
|
62
62
|
`.trim();
|
|
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.
|
|
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 version coming soon at <a href="${PUBLIC_METADATA.canonicalSiteUrl}">mcp.revasserlabs.com</a>.`;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
function demoLinks() {
|
|
70
70
|
return `
|
|
71
|
-
<a href="${PUBLIC_METADATA.canonicalSiteUrl}" target="_blank" rel="noopener noreferrer" style="background:rgba(240,194,70,0.18);border-color:rgba(240,194,70,0.45);color:#f0c246">
|
|
71
|
+
<a href="${PUBLIC_METADATA.canonicalSiteUrl}" target="_blank" rel="noopener noreferrer" style="background:rgba(240,194,70,0.18);border-color:rgba(240,194,70,0.45);color:#f0c246">Hosted</a>
|
|
72
72
|
<a href="${NPM_URL}" target="_blank" rel="noopener noreferrer">npm Install</a>
|
|
73
73
|
<a href="${SETUP_URL}" target="_blank" rel="noopener noreferrer">Setup Guide</a>
|
|
74
74
|
`.trim();
|
|
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.
|
|
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 version coming soon at <a href="${PUBLIC_METADATA.canonicalSiteUrl}" target="_blank" rel="noopener noreferrer">mcp.revasserlabs.com</a>.`;
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
function demoFooterLinks() {
|
|
82
|
-
return `<a href="${PUBLIC_METADATA.canonicalRepoUrl}">GitHub</a> · <a href="${NPM_URL}" style="color:#94a3b8;text-decoration:none;font-size:0.875rem">npm</a> · <a href="${PUBLIC_METADATA.canonicalSiteUrl}" style="color:#f0c246;text-decoration:none;font-size:0.875rem">
|
|
82
|
+
return `<a href="${PUBLIC_METADATA.canonicalRepoUrl}">GitHub</a> · <a href="${NPM_URL}" style="color:#94a3b8;text-decoration:none;font-size:0.875rem">npm</a> · <a href="${PUBLIC_METADATA.canonicalSiteUrl}" style="color:#f0c246;text-decoration:none;font-size:0.875rem">Hosted</a>`;
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
function commonTokens() {
|
|
@@ -113,10 +113,10 @@ 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: "
|
|
117
|
-
CLOUD_TEAM_PRICE: "
|
|
118
|
-
CLOUD_TURNKEY_LAUNCH_PRICE: "
|
|
119
|
-
CLOUD_MANAGED_RELIABILITY_PRICE: "
|
|
116
|
+
CLOUD_SOLO_PRICE: "coming soon",
|
|
117
|
+
CLOUD_TEAM_PRICE: "coming soon",
|
|
118
|
+
CLOUD_TURNKEY_LAUNCH_PRICE: "contact us",
|
|
119
|
+
CLOUD_MANAGED_RELIABILITY_PRICE: "contact us",
|
|
120
120
|
SUPPORT_EMAIL: PUBLIC_METADATA.supportEmail,
|
|
121
121
|
ROOT_DECISION_PANEL: rootDecisionPanel(),
|
|
122
122
|
SHARE_LINKS: shareLinks(),
|
|
@@ -134,6 +134,6 @@ export function buildPublicPages() {
|
|
|
134
134
|
"public/share.html": replaceTokens(template("share.html.tpl"), tokens),
|
|
135
135
|
"public/demo.html": replaceTokens(template("demo.html.tpl"), tokens),
|
|
136
136
|
"public/demo-video.html": replaceTokens(template("demo-video.html.tpl"), tokens),
|
|
137
|
-
"public/demo-
|
|
137
|
+
"public/demo-slack-mcp.html": replaceTokens(template("demo-slack-mcp.html.tpl"), tokens),
|
|
138
138
|
};
|
|
139
139
|
}
|
package/lib/slack-client.js
CHANGED
|
@@ -9,12 +9,19 @@
|
|
|
9
9
|
* - Proactive token health checking
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
loadTokens,
|
|
14
|
+
saveTokens,
|
|
15
|
+
extractFromChrome,
|
|
16
|
+
getLastExtractionError,
|
|
17
|
+
saveAutoHealTelemetry,
|
|
18
|
+
} from "./token-store.js";
|
|
13
19
|
|
|
14
20
|
// ============ Configuration ============
|
|
15
21
|
|
|
16
22
|
const TOKEN_WARNING_AGE = 10 * 24 * 60 * 60 * 1000; // 10 days
|
|
17
23
|
const TOKEN_CRITICAL_AGE = 13 * 24 * 60 * 60 * 1000; // 13 days
|
|
24
|
+
const STUCK_THRESHOLD_MS = 24 * 60 * 60 * 1000; // Escalate to 'stuck' after 24h of repeated auto-heal failures
|
|
18
25
|
const REFRESH_COOLDOWN = 60 * 60 * 1000; // 1 hour between refresh attempts
|
|
19
26
|
const USER_CACHE_MAX_SIZE = 500;
|
|
20
27
|
const USER_CACHE_TTL = 60 * 60 * 1000; // 1 hour
|
|
@@ -113,14 +120,21 @@ export async function checkTokenHealth(logger = console) {
|
|
|
113
120
|
? Math.round(tokenAge / (60 * 60 * 1000) * 10) / 10
|
|
114
121
|
: null;
|
|
115
122
|
|
|
123
|
+
// Read auto-heal telemetry (only populated when source is "file")
|
|
124
|
+
let lastAutoHealAttempt = creds.lastAutoHealAttempt || null;
|
|
125
|
+
let lastAutoHealError = creds.lastAutoHealError || null;
|
|
126
|
+
let stuckSince = creds.stuckSince || null;
|
|
127
|
+
|
|
116
128
|
// Attempt proactive refresh if token is getting old
|
|
117
129
|
if (hasKnownAge && tokenAge > TOKEN_WARNING_AGE && Date.now() - lastRefreshAttempt > REFRESH_COOLDOWN) {
|
|
118
130
|
lastRefreshAttempt = Date.now();
|
|
131
|
+
const attemptAt = new Date().toISOString();
|
|
119
132
|
logger.error?.(`Token is ${ageHours}h old, attempting proactive refresh...`);
|
|
120
133
|
|
|
121
134
|
const newTokens = extractFromChrome();
|
|
122
135
|
if (newTokens) {
|
|
123
136
|
saveTokens(newTokens.token, newTokens.cookie);
|
|
137
|
+
saveAutoHealTelemetry({ attemptAt, error: null });
|
|
124
138
|
logger.error?.('Proactively refreshed tokens from Chrome');
|
|
125
139
|
return {
|
|
126
140
|
healthy: true,
|
|
@@ -129,35 +143,59 @@ export async function checkTokenHealth(logger = console) {
|
|
|
129
143
|
age_known: true,
|
|
130
144
|
age_state: 'fresh',
|
|
131
145
|
source: 'chrome-auto',
|
|
146
|
+
last_auto_heal_attempt: attemptAt,
|
|
147
|
+
last_auto_heal_error: null,
|
|
148
|
+
stuck_since: null,
|
|
132
149
|
message: 'Tokens refreshed successfully'
|
|
133
150
|
};
|
|
134
151
|
} else {
|
|
135
|
-
|
|
152
|
+
const extractionError = getLastExtractionError();
|
|
153
|
+
const errorCode = extractionError?.code || 'chrome_extraction_failed';
|
|
154
|
+
saveAutoHealTelemetry({ attemptAt, error: errorCode });
|
|
155
|
+
lastAutoHealAttempt = attemptAt;
|
|
156
|
+
if (lastAutoHealError !== errorCode) {
|
|
157
|
+
stuckSince = attemptAt;
|
|
158
|
+
}
|
|
159
|
+
lastAutoHealError = errorCode;
|
|
160
|
+
logger.error?.(`Could not refresh from Chrome: ${extractionError?.message || 'unknown error'}`);
|
|
136
161
|
}
|
|
137
162
|
}
|
|
138
163
|
|
|
164
|
+
const stuckSinceMs = stuckSince ? new Date(stuckSince).getTime() : Number.NaN;
|
|
165
|
+
const isStuck = Number.isFinite(stuckSinceMs)
|
|
166
|
+
&& (Date.now() - stuckSinceMs) > STUCK_THRESHOLD_MS
|
|
167
|
+
&& !!lastAutoHealError;
|
|
168
|
+
|
|
139
169
|
return {
|
|
140
170
|
healthy: !hasKnownAge || tokenAge < TOKEN_CRITICAL_AGE,
|
|
141
171
|
age_hours: ageHours,
|
|
142
172
|
age_known: hasKnownAge,
|
|
143
|
-
age_state:
|
|
144
|
-
? '
|
|
145
|
-
:
|
|
146
|
-
? '
|
|
147
|
-
: tokenAge >
|
|
148
|
-
? '
|
|
149
|
-
:
|
|
173
|
+
age_state: isStuck
|
|
174
|
+
? 'stuck'
|
|
175
|
+
: !hasKnownAge
|
|
176
|
+
? 'unknown'
|
|
177
|
+
: tokenAge > TOKEN_CRITICAL_AGE
|
|
178
|
+
? 'critical'
|
|
179
|
+
: tokenAge > TOKEN_WARNING_AGE
|
|
180
|
+
? 'warning'
|
|
181
|
+
: 'healthy',
|
|
150
182
|
warning: hasKnownAge && tokenAge > TOKEN_WARNING_AGE,
|
|
151
183
|
critical: hasKnownAge && tokenAge > TOKEN_CRITICAL_AGE,
|
|
184
|
+
stuck: isStuck,
|
|
152
185
|
source: creds.source,
|
|
153
186
|
updated_at: creds.updatedAt,
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
187
|
+
last_auto_heal_attempt: lastAutoHealAttempt,
|
|
188
|
+
last_auto_heal_error: lastAutoHealError,
|
|
189
|
+
stuck_since: stuckSince,
|
|
190
|
+
message: isStuck
|
|
191
|
+
? `Auto-heal has been failing since ${stuckSince} (last error: ${lastAutoHealError}). Open Chrome > View > Developer > Allow JavaScript from Apple Events, then run npm run tokens:auto.`
|
|
192
|
+
: !hasKnownAge
|
|
193
|
+
? 'Token age unknown (missing timestamp) - auth can still be valid'
|
|
194
|
+
: tokenAge > TOKEN_CRITICAL_AGE
|
|
195
|
+
? 'Token may expire soon - open Slack in Chrome'
|
|
196
|
+
: tokenAge > TOKEN_WARNING_AGE
|
|
197
|
+
? 'Token is getting old - will auto-refresh if Slack tab is open'
|
|
198
|
+
: 'Token is healthy'
|
|
161
199
|
};
|
|
162
200
|
}
|
|
163
201
|
|
|
@@ -250,13 +288,28 @@ export async function slackAPI(method, params = {}, options = {}) {
|
|
|
250
288
|
// Handle auth errors with auto-retry
|
|
251
289
|
if ((data.error === "invalid_auth" || data.error === "token_expired") && retryOnAuthFail) {
|
|
252
290
|
logger.error?.("Token expired, attempting Chrome auto-extraction...");
|
|
291
|
+
const attemptAt = new Date().toISOString();
|
|
253
292
|
const chromeTokens = extractFromChrome();
|
|
254
293
|
if (chromeTokens) {
|
|
255
294
|
saveTokens(chromeTokens.token, chromeTokens.cookie);
|
|
295
|
+
saveAutoHealTelemetry({ attemptAt, error: null });
|
|
256
296
|
// Retry the request
|
|
257
297
|
return slackAPI(method, params, { ...options, retryOnAuthFail: false });
|
|
258
298
|
}
|
|
259
|
-
|
|
299
|
+
const extractionError = getLastExtractionError() || {
|
|
300
|
+
code: 'chrome_extraction_failed',
|
|
301
|
+
message: 'Auto-heal attempted but no structured error surfaced.',
|
|
302
|
+
detail: null
|
|
303
|
+
};
|
|
304
|
+
saveAutoHealTelemetry({ attemptAt, error: extractionError.code });
|
|
305
|
+
const err = new Error(
|
|
306
|
+
`Slack auth failed (${data.error}) and auto-heal could not refresh tokens: ${extractionError.message}`
|
|
307
|
+
);
|
|
308
|
+
err.code = 'token_auth_failed';
|
|
309
|
+
err.slack_error = data.error;
|
|
310
|
+
err.extraction_error = extractionError;
|
|
311
|
+
err.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.';
|
|
312
|
+
throw err;
|
|
260
313
|
}
|
|
261
314
|
throw new Error(data.error || "Slack API error");
|
|
262
315
|
}
|
package/lib/token-store.js
CHANGED
|
@@ -8,10 +8,11 @@
|
|
|
8
8
|
* 4. Chrome auto-extraction (fallback)
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { readFileSync, writeFileSync, existsSync, renameSync, unlinkSync, chmodSync } from "fs";
|
|
12
|
-
import { homedir, platform } from "os";
|
|
11
|
+
import { readFileSync, writeFileSync, existsSync, renameSync, unlinkSync, chmodSync, copyFileSync, mkdtempSync, statSync, readdirSync } from "fs";
|
|
12
|
+
import { homedir, platform, tmpdir } from "os";
|
|
13
13
|
import { join } from "path";
|
|
14
|
-
import {
|
|
14
|
+
import { execFileSync } from "child_process";
|
|
15
|
+
import { pbkdf2Sync, createDecipheriv } from "crypto";
|
|
15
16
|
|
|
16
17
|
const TOKEN_FILE = join(homedir(), ".slack-mcp-tokens.json");
|
|
17
18
|
const KEYCHAIN_SERVICE = "slack-mcp-server";
|
|
@@ -19,6 +20,12 @@ const KEYCHAIN_SERVICE = "slack-mcp-server";
|
|
|
19
20
|
// Platform detection
|
|
20
21
|
const IS_MACOS = platform() === 'darwin';
|
|
21
22
|
|
|
23
|
+
// Default Chrome user-data dir on macOS
|
|
24
|
+
const DEFAULT_CHROME_BASE = join(homedir(), 'Library', 'Application Support', 'Google', 'Chrome');
|
|
25
|
+
|
|
26
|
+
// Slack xoxc- token regex: 3 numeric segments then a hex signature
|
|
27
|
+
const XOXC_TOKEN_RE = /xoxc-[0-9]+-[0-9]+-[0-9]+-[a-f0-9]{20,}/g;
|
|
28
|
+
|
|
22
29
|
// Refresh lock to prevent concurrent extraction attempts
|
|
23
30
|
let refreshInProgress = null;
|
|
24
31
|
let lastExtractionError = null;
|
|
@@ -64,13 +71,45 @@ export function getFromFile() {
|
|
|
64
71
|
return {
|
|
65
72
|
token: data.SLACK_TOKEN,
|
|
66
73
|
cookie: data.SLACK_COOKIE,
|
|
67
|
-
updatedAt: data.updated_at || data.UPDATED_AT || null
|
|
74
|
+
updatedAt: data.updated_at || data.UPDATED_AT || null,
|
|
75
|
+
lastAutoHealAttempt: data.last_auto_heal_attempt || null,
|
|
76
|
+
lastAutoHealError: data.last_auto_heal_error || null,
|
|
77
|
+
stuckSince: data.stuck_since || null
|
|
68
78
|
};
|
|
69
79
|
} catch (e) {
|
|
70
80
|
return null;
|
|
71
81
|
}
|
|
72
82
|
}
|
|
73
83
|
|
|
84
|
+
/**
|
|
85
|
+
* Persist auto-heal telemetry into the token file.
|
|
86
|
+
* Best-effort: silent on failure (tokens are more important than metadata).
|
|
87
|
+
* error === null indicates a successful auto-heal; any non-null string is an
|
|
88
|
+
* error code (e.g. "apple_events_javascript_disabled"). When the error code
|
|
89
|
+
* changes, stuck_since is reset; when it stays the same across attempts,
|
|
90
|
+
* stuck_since is preserved so downstream consumers can detect a long-running
|
|
91
|
+
* stuck state.
|
|
92
|
+
*/
|
|
93
|
+
export function saveAutoHealTelemetry({ attemptAt, error }) {
|
|
94
|
+
if (!existsSync(TOKEN_FILE)) return;
|
|
95
|
+
try {
|
|
96
|
+
const data = JSON.parse(readFileSync(TOKEN_FILE, "utf-8"));
|
|
97
|
+
data.last_auto_heal_attempt = attemptAt;
|
|
98
|
+
if (error) {
|
|
99
|
+
if (data.last_auto_heal_error !== error) {
|
|
100
|
+
data.stuck_since = attemptAt;
|
|
101
|
+
}
|
|
102
|
+
data.last_auto_heal_error = error;
|
|
103
|
+
} else {
|
|
104
|
+
data.last_auto_heal_error = null;
|
|
105
|
+
data.stuck_since = null;
|
|
106
|
+
}
|
|
107
|
+
atomicWriteSync(TOKEN_FILE, JSON.stringify(data, null, 2));
|
|
108
|
+
} catch (e) {
|
|
109
|
+
// Silent: telemetry must never break the auto-heal hot path.
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
74
113
|
/**
|
|
75
114
|
* Atomic write to prevent file corruption from concurrent writes
|
|
76
115
|
*/
|
|
@@ -102,32 +141,89 @@ export function saveToFile(token, cookie) {
|
|
|
102
141
|
|
|
103
142
|
// Multiple localStorage paths Slack might use (for robustness)
|
|
104
143
|
const SLACK_TOKEN_PATHS = [
|
|
105
|
-
// Current known path
|
|
106
144
|
`JSON.parse(localStorage.localConfig_v2).teams[Object.keys(JSON.parse(localStorage.localConfig_v2).teams)[0]].token`,
|
|
107
|
-
// Potential future paths
|
|
108
145
|
`JSON.parse(localStorage.localConfig_v3).teams[Object.keys(JSON.parse(localStorage.localConfig_v3).teams)[0]].token`,
|
|
109
|
-
// Redux store path (older Slack)
|
|
110
146
|
`JSON.parse(localStorage.getItem('reduxPersist:localConfig'))?.teams?.[Object.keys(JSON.parse(localStorage.getItem('reduxPersist:localConfig'))?.teams || {})[0]]?.token`,
|
|
111
|
-
// Direct boot data
|
|
112
147
|
`window.boot_data?.api_token`,
|
|
113
148
|
];
|
|
114
149
|
|
|
150
|
+
// Fallback profile list used when Local State JSON can't be read
|
|
151
|
+
const FALLBACK_CHROME_PROFILES = ['Default', 'Profile 1', 'Profile 2', 'Profile 3', 'Profile 4', 'Profile 5'];
|
|
152
|
+
|
|
153
|
+
// ============ Chrome profile discovery ============
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Resolve the Chrome user-data directory.
|
|
157
|
+
* Override with SLACK_MCP_CHROME_USER_DATA_DIR for non-standard installations
|
|
158
|
+
* (e.g. a portable Chrome, a test profile, or a Chrome Canary layout).
|
|
159
|
+
*/
|
|
160
|
+
function getChromeBase() {
|
|
161
|
+
return process.env.SLACK_MCP_CHROME_USER_DATA_DIR || DEFAULT_CHROME_BASE;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Extraction mode config:
|
|
166
|
+
* "auto" - LevelDB first, AppleScript fallback (default)
|
|
167
|
+
* "leveldb" - On-disk only, never touch AppleScript (CI-safe, headless-safe)
|
|
168
|
+
* "applescript"- Legacy AppleScript-only path
|
|
169
|
+
*/
|
|
170
|
+
function getExtractionMode() {
|
|
171
|
+
const mode = (process.env.SLACK_MCP_EXTRACTION_MODE || 'auto').toLowerCase();
|
|
172
|
+
return ['auto', 'leveldb', 'applescript'].includes(mode) ? mode : 'auto';
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Enumerate all Chrome profiles present on this machine, newest cookie DB first.
|
|
177
|
+
* SLACK_MCP_CHROME_PROFILE can pin a single profile (exact directory name).
|
|
178
|
+
* Falls back to the legacy hardcoded list if Local State is unreadable.
|
|
179
|
+
*/
|
|
180
|
+
function enumerateChromeProfiles() {
|
|
181
|
+
const envProfile = process.env.SLACK_MCP_CHROME_PROFILE;
|
|
182
|
+
if (envProfile) return [envProfile];
|
|
183
|
+
|
|
184
|
+
const base = getChromeBase();
|
|
185
|
+
const localStatePath = join(base, 'Local State');
|
|
186
|
+
|
|
187
|
+
let profiles = [];
|
|
188
|
+
try {
|
|
189
|
+
const localState = JSON.parse(readFileSync(localStatePath, 'utf-8'));
|
|
190
|
+
profiles = Object.keys(localState.profile?.info_cache || {});
|
|
191
|
+
} catch {
|
|
192
|
+
profiles = [...FALLBACK_CHROME_PROFILES];
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (profiles.length === 0) profiles = [...FALLBACK_CHROME_PROFILES];
|
|
196
|
+
|
|
197
|
+
// Rank profiles by cookie-db mtime descending so the freshest Slack session wins.
|
|
198
|
+
const ranked = profiles.map(p => {
|
|
199
|
+
const cookiePath = join(base, p, 'Cookies');
|
|
200
|
+
let mtime = 0;
|
|
201
|
+
try { mtime = statSync(cookiePath).mtimeMs; } catch {}
|
|
202
|
+
return { name: p, mtime };
|
|
203
|
+
});
|
|
204
|
+
ranked.sort((a, b) => b.mtime - a.mtime);
|
|
205
|
+
return ranked.map(x => x.name);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Chrome profile directories to search (legacy helper retained for back-compat)
|
|
209
|
+
const CHROME_PROFILES = ['Default', 'Profile 1', 'Profile 2', 'Profile 3'];
|
|
210
|
+
|
|
115
211
|
function normalizeExtractionError(error) {
|
|
116
212
|
const raw = String(error?.message || error || "");
|
|
117
213
|
|
|
118
214
|
if (raw.includes("Executing JavaScript through AppleScript is turned off")) {
|
|
119
215
|
return {
|
|
120
216
|
code: "apple_events_javascript_disabled",
|
|
121
|
-
message: "Chrome
|
|
122
|
-
detail: "
|
|
217
|
+
message: "Chrome needs one setting enabled for token extraction.",
|
|
218
|
+
detail: "In Chrome: View > Developer > Allow JavaScript from Apple Events. Cookie extraction works without this — only the token needs it."
|
|
123
219
|
};
|
|
124
220
|
}
|
|
125
221
|
|
|
126
222
|
if (raw.includes("Application isn't running") || raw.includes("Google Chrome got an error")) {
|
|
127
223
|
return {
|
|
128
224
|
code: "chrome_not_ready",
|
|
129
|
-
message: "Chrome is not
|
|
130
|
-
detail: "Open Google Chrome with
|
|
225
|
+
message: "Chrome is not running or has no windows open.",
|
|
226
|
+
detail: "Open Google Chrome with a Slack tab at app.slack.com."
|
|
131
227
|
};
|
|
132
228
|
}
|
|
133
229
|
|
|
@@ -139,6 +235,14 @@ function normalizeExtractionError(error) {
|
|
|
139
235
|
};
|
|
140
236
|
}
|
|
141
237
|
|
|
238
|
+
if (raw.includes("Chrome Safe Storage")) {
|
|
239
|
+
return {
|
|
240
|
+
code: "keychain_access_denied",
|
|
241
|
+
message: "Could not access Chrome's encryption key in Keychain.",
|
|
242
|
+
detail: "You may need to allow terminal access in System Settings > Privacy > Full Disk Access."
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
142
246
|
return {
|
|
143
247
|
code: "chrome_extraction_failed",
|
|
144
248
|
message: "Chrome token extraction failed.",
|
|
@@ -147,83 +251,273 @@ function normalizeExtractionError(error) {
|
|
|
147
251
|
}
|
|
148
252
|
|
|
149
253
|
/**
|
|
150
|
-
* Extract
|
|
151
|
-
* Returns
|
|
254
|
+
* Extract the Slack `d` cookie from a specific Chrome profile's cookie DB.
|
|
255
|
+
* Returns the decrypted xoxd- cookie string or null if this profile has no
|
|
256
|
+
* Slack session or decryption fails.
|
|
257
|
+
*
|
|
258
|
+
* Chrome holds a WAL lock on the live DB; we copy-then-query for safety.
|
|
259
|
+
*/
|
|
260
|
+
function extractCookieForProfile(profileDir) {
|
|
261
|
+
const cookiesPath = join(profileDir, 'Cookies');
|
|
262
|
+
if (!existsSync(cookiesPath)) return null;
|
|
263
|
+
|
|
264
|
+
const tmpDir = mkdtempSync(join(tmpdir(), 'slack-mcp-'));
|
|
265
|
+
const tmpDb = join(tmpDir, 'Cookies');
|
|
266
|
+
try {
|
|
267
|
+
copyFileSync(cookiesPath, tmpDb);
|
|
268
|
+
|
|
269
|
+
const queryResult = execFileSync('sqlite3', [
|
|
270
|
+
tmpDb,
|
|
271
|
+
"SELECT hex(encrypted_value) FROM cookies WHERE host_key LIKE '%.slack.com%' AND name = 'd' LIMIT 1;"
|
|
272
|
+
], { encoding: 'utf-8', timeout: 5000 }).trim();
|
|
273
|
+
|
|
274
|
+
try { unlinkSync(tmpDb); unlinkSync(tmpDir); } catch {}
|
|
275
|
+
|
|
276
|
+
if (!queryResult) return null;
|
|
277
|
+
|
|
278
|
+
const encrypted = Buffer.from(queryResult, 'hex');
|
|
279
|
+
if (encrypted.length < 4) return null;
|
|
280
|
+
|
|
281
|
+
// Chrome Safe Storage password (per-machine, stored in macOS Keychain)
|
|
282
|
+
const safeStoragePassword = execFileSync('security', [
|
|
283
|
+
'find-generic-password', '-s', 'Chrome Safe Storage', '-w'
|
|
284
|
+
], { encoding: 'utf-8', timeout: 5000 }).trim();
|
|
285
|
+
|
|
286
|
+
// macOS Chrome cookies: v10 prefix + AES-128-CBC
|
|
287
|
+
const prefix = encrypted.subarray(0, 3).toString('utf-8');
|
|
288
|
+
if (prefix !== 'v10') return null;
|
|
289
|
+
|
|
290
|
+
const ciphertext = encrypted.subarray(3);
|
|
291
|
+
const key = pbkdf2Sync(safeStoragePassword, 'saltysalt', 1003, 16, 'sha1');
|
|
292
|
+
const iv = Buffer.alloc(16, ' ');
|
|
293
|
+
|
|
294
|
+
const decipher = createDecipheriv('aes-128-cbc', key, iv);
|
|
295
|
+
let decrypted;
|
|
296
|
+
try {
|
|
297
|
+
decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
298
|
+
} catch {
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const text = decrypted.toString('utf-8');
|
|
303
|
+
const xoxdIndex = text.indexOf('xoxd-');
|
|
304
|
+
if (xoxdIndex < 0) return null;
|
|
305
|
+
return text.substring(xoxdIndex);
|
|
306
|
+
} catch {
|
|
307
|
+
try { unlinkSync(tmpDb); unlinkSync(tmpDir); } catch {}
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Legacy helper: walk CHROME_PROFILES and return the first cookie found.
|
|
314
|
+
* Retained so existing callers that only want a cookie string keep working.
|
|
315
|
+
*/
|
|
316
|
+
function extractCookieFromChromeDB() {
|
|
317
|
+
const base = getChromeBase();
|
|
318
|
+
for (const profile of enumerateChromeProfiles()) {
|
|
319
|
+
const cookie = extractCookieForProfile(join(base, profile));
|
|
320
|
+
if (cookie) return cookie;
|
|
321
|
+
}
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Extract a Slack xoxc- token by reading the on-disk LevelDB for a profile.
|
|
327
|
+
* This is the preferred path:
|
|
328
|
+
* - No AppleScript required
|
|
329
|
+
* - No "Allow JavaScript from Apple Events" Chrome dev flag required
|
|
330
|
+
* - No live Slack tab required — the token just has to have been cached
|
|
331
|
+
* at some point during normal use
|
|
332
|
+
* - Works headlessly, works in CI, works when Chrome is closed
|
|
333
|
+
*
|
|
334
|
+
* We scan .ldb and .log files newest-first so the freshest cached token wins.
|
|
335
|
+
*/
|
|
336
|
+
function extractTokenFromLevelDB(profileDir) {
|
|
337
|
+
const ldbDir = join(profileDir, 'Local Storage', 'leveldb');
|
|
338
|
+
if (!existsSync(ldbDir)) return null;
|
|
339
|
+
|
|
340
|
+
let files;
|
|
341
|
+
try {
|
|
342
|
+
files = readdirSync(ldbDir)
|
|
343
|
+
.filter(f => /\.(ldb|log)$/.test(f))
|
|
344
|
+
.map(f => {
|
|
345
|
+
const p = join(ldbDir, f);
|
|
346
|
+
let mtime = 0;
|
|
347
|
+
try { mtime = statSync(p).mtimeMs; } catch {}
|
|
348
|
+
return { path: p, mtime };
|
|
349
|
+
})
|
|
350
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
351
|
+
} catch {
|
|
352
|
+
return null;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
for (const f of files) {
|
|
356
|
+
try {
|
|
357
|
+
// Binary encoding avoids UTF-8 re-interpretation of snappy-compressed blocks
|
|
358
|
+
const txt = readFileSync(f.path).toString('binary');
|
|
359
|
+
XOXC_TOKEN_RE.lastIndex = 0;
|
|
360
|
+
const matches = txt.match(XOXC_TOKEN_RE);
|
|
361
|
+
if (matches && matches.length) return matches[0];
|
|
362
|
+
} catch {
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Extract Slack token from Chrome via AppleScript (reads localStorage).
|
|
371
|
+
* Uses strict URL matching to avoid hitting non-Slack tabs.
|
|
372
|
+
*/
|
|
373
|
+
function extractTokenFromChrome() {
|
|
374
|
+
// Prefer /client URLs (active workspace), fall back to any app.slack.com
|
|
375
|
+
const urlChecks = [
|
|
376
|
+
'URL of t starts with "https://app.slack.com/client"',
|
|
377
|
+
'URL of t starts with "https://app.slack.com"',
|
|
378
|
+
];
|
|
379
|
+
|
|
380
|
+
const tokenPathsJS = SLACK_TOKEN_PATHS.map((path, i) =>
|
|
381
|
+
`try { var t${i} = ${path}; if (t${i} && t${i}.startsWith('xoxc-')) return t${i}; } catch(e) {}`
|
|
382
|
+
).join(' ');
|
|
383
|
+
|
|
384
|
+
for (const urlCheck of urlChecks) {
|
|
385
|
+
try {
|
|
386
|
+
const script = `tell application "Google Chrome"
|
|
387
|
+
repeat with w in windows
|
|
388
|
+
repeat with t in tabs of w
|
|
389
|
+
if ${urlCheck} then
|
|
390
|
+
return execute t javascript "(function() { ${tokenPathsJS} return ''; })()"
|
|
391
|
+
end if
|
|
392
|
+
end repeat
|
|
393
|
+
end repeat
|
|
394
|
+
return ""
|
|
395
|
+
end tell`;
|
|
396
|
+
|
|
397
|
+
const token = execFileSync('osascript', ['-e', script], {
|
|
398
|
+
encoding: 'utf-8', timeout: 8000
|
|
399
|
+
}).trim();
|
|
400
|
+
|
|
401
|
+
if (token && token.startsWith('xoxc-')) return token;
|
|
402
|
+
} catch {
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return null;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Extract tokens from Chrome (macOS only).
|
|
412
|
+
*
|
|
413
|
+
* Two extraction paths:
|
|
414
|
+
*
|
|
415
|
+
* 1. LevelDB (preferred, default "auto" mode tries this first):
|
|
416
|
+
* Cookie: Reads the encrypted SQLite cookie DB and decrypts with the
|
|
417
|
+
* Chrome Safe Storage key from macOS Keychain.
|
|
418
|
+
* Token: Reads the on-disk LevelDB under Local Storage and regex-matches
|
|
419
|
+
* any cached xoxc- token. Works without a live Slack tab, without
|
|
420
|
+
* the AppleScript dev flag, and works when Chrome is closed.
|
|
421
|
+
*
|
|
422
|
+
* 2. AppleScript (legacy fallback, or forced with SLACK_MCP_EXTRACTION_MODE=applescript):
|
|
423
|
+
* Cookie: Same SQLite-backed path.
|
|
424
|
+
* Token: Drives Chrome via AppleScript to run JS against localStorage.
|
|
425
|
+
* Requires Chrome > View > Developer > "Allow JavaScript from
|
|
426
|
+
* Apple Events" AND a live app.slack.com tab. Kept because it
|
|
427
|
+
* grabs the token from whichever workspace is actually active
|
|
428
|
+
* right now, which can differ from what's cached on disk.
|
|
429
|
+
*
|
|
430
|
+
* Environment overrides:
|
|
431
|
+
* SLACK_MCP_CHROME_USER_DATA_DIR - base Chrome dir (default ~/Library/Application Support/Google/Chrome)
|
|
432
|
+
* SLACK_MCP_CHROME_PROFILE - pin a single profile directory name
|
|
433
|
+
* SLACK_MCP_EXTRACTION_MODE - auto | leveldb | applescript
|
|
152
434
|
*/
|
|
153
435
|
function extractFromChromeInternal() {
|
|
154
436
|
lastExtractionError = null;
|
|
437
|
+
|
|
155
438
|
if (!IS_MACOS) {
|
|
156
|
-
// AppleScript/osascript is macOS-only
|
|
157
439
|
lastExtractionError = {
|
|
158
440
|
code: "unsupported_platform",
|
|
159
441
|
message: "Chrome auto-extraction is only available on macOS.",
|
|
160
|
-
detail: "Use manual token setup on this platform."
|
|
442
|
+
detail: "Use manual token setup on this platform, or set SLACK_TOKEN and SLACK_COOKIE env vars."
|
|
161
443
|
};
|
|
162
444
|
return null;
|
|
163
445
|
}
|
|
164
446
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
447
|
+
const mode = getExtractionMode();
|
|
448
|
+
const base = getChromeBase();
|
|
449
|
+
const profiles = enumerateChromeProfiles();
|
|
450
|
+
|
|
451
|
+
if (profiles.length === 0) {
|
|
452
|
+
lastExtractionError = {
|
|
453
|
+
code: "no_chrome_profiles",
|
|
454
|
+
message: "No Chrome profiles found.",
|
|
455
|
+
detail: `Looked under ${base}. Set SLACK_MCP_CHROME_USER_DATA_DIR if Chrome is installed elsewhere.`
|
|
456
|
+
};
|
|
457
|
+
return null;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// --- Path 1: LevelDB (no AppleScript, no live tab needed) ---
|
|
461
|
+
if (mode === 'leveldb' || mode === 'auto') {
|
|
462
|
+
for (const profileName of profiles) {
|
|
463
|
+
const profileDir = join(base, profileName);
|
|
464
|
+
const cookie = extractCookieForProfile(profileDir);
|
|
465
|
+
if (!cookie) continue;
|
|
466
|
+
const token = extractTokenFromLevelDB(profileDir);
|
|
467
|
+
if (!token) continue;
|
|
468
|
+
return { token, cookie, profile: profileName, extraction_mode: 'leveldb' };
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (mode === 'leveldb') {
|
|
184
472
|
lastExtractionError = {
|
|
185
|
-
code: "
|
|
186
|
-
message: "
|
|
187
|
-
detail:
|
|
473
|
+
code: "leveldb_no_matching_profile",
|
|
474
|
+
message: "No Chrome profile had both a Slack cookie and a cached xoxc- token on disk.",
|
|
475
|
+
detail: `Profiles checked: ${profiles.join(', ')}. Open Slack in Chrome and sign in once, then retry. SLACK_MCP_CHROME_PROFILE can pin a specific profile.`
|
|
188
476
|
};
|
|
189
477
|
return null;
|
|
190
478
|
}
|
|
479
|
+
// Fall through to AppleScript
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// --- Path 2: AppleScript + SQLite (legacy, requires live tab + dev flag) ---
|
|
483
|
+
if (mode === 'applescript' || mode === 'auto') {
|
|
484
|
+
let cookieSeen = null;
|
|
485
|
+
for (const profileName of profiles) {
|
|
486
|
+
const profileDir = join(base, profileName);
|
|
487
|
+
const cookie = extractCookieForProfile(profileDir);
|
|
488
|
+
if (!cookie) continue;
|
|
489
|
+
cookieSeen = cookie;
|
|
490
|
+
|
|
491
|
+
let token;
|
|
492
|
+
try {
|
|
493
|
+
token = extractTokenFromChrome();
|
|
494
|
+
} catch (e) {
|
|
495
|
+
lastExtractionError = normalizeExtractionError(e);
|
|
496
|
+
continue;
|
|
497
|
+
}
|
|
498
|
+
if (token) {
|
|
499
|
+
return { token, cookie, profile: profileName, extraction_mode: 'applescript' };
|
|
500
|
+
}
|
|
501
|
+
}
|
|
191
502
|
|
|
192
|
-
|
|
193
|
-
const tokenPathsJS = SLACK_TOKEN_PATHS.map((path, i) =>
|
|
194
|
-
`try { var t${i} = ${path}; if (t${i}?.startsWith('xoxc-')) return t${i}; } catch(e) {}`
|
|
195
|
-
).join(' ');
|
|
196
|
-
|
|
197
|
-
const tokenScript = `
|
|
198
|
-
tell application "Google Chrome"
|
|
199
|
-
repeat with w in windows
|
|
200
|
-
repeat with t in tabs of w
|
|
201
|
-
if URL of t contains "slack.com" then
|
|
202
|
-
return execute t javascript "(function() { ${tokenPathsJS} return ''; })()"
|
|
203
|
-
end if
|
|
204
|
-
end repeat
|
|
205
|
-
end repeat
|
|
206
|
-
return ""
|
|
207
|
-
end tell
|
|
208
|
-
`;
|
|
209
|
-
const token = execSync(`osascript -e '${tokenScript.replace(/'/g, "'\"'\"'")}'`, {
|
|
210
|
-
encoding: 'utf-8', timeout: 5000
|
|
211
|
-
}).trim();
|
|
212
|
-
|
|
213
|
-
if (!token || !token.startsWith('xoxc-')) {
|
|
503
|
+
if (cookieSeen && !lastExtractionError) {
|
|
214
504
|
lastExtractionError = {
|
|
215
|
-
code: "
|
|
216
|
-
message: "
|
|
217
|
-
detail: "
|
|
505
|
+
code: "apple_events_javascript_disabled",
|
|
506
|
+
message: "Cookie extracted, but AppleScript could not read the Slack token from localStorage.",
|
|
507
|
+
detail: "Enable Chrome > View > Developer > Allow JavaScript from Apple Events, then retry. Or set SLACK_MCP_EXTRACTION_MODE=leveldb to skip AppleScript entirely."
|
|
218
508
|
};
|
|
219
509
|
return null;
|
|
220
510
|
}
|
|
511
|
+
}
|
|
221
512
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
513
|
+
if (!lastExtractionError) {
|
|
514
|
+
lastExtractionError = {
|
|
515
|
+
code: "extraction_failed_all_paths",
|
|
516
|
+
message: "Could not extract Slack credentials via LevelDB or AppleScript.",
|
|
517
|
+
detail: `Profiles checked: ${profiles.join(', ')}. Ensure you are logged into Slack at app.slack.com in Chrome at least once.`
|
|
518
|
+
};
|
|
226
519
|
}
|
|
520
|
+
return null;
|
|
227
521
|
}
|
|
228
522
|
|
|
229
523
|
/**
|
|
@@ -273,7 +567,10 @@ function getStoredTokens() {
|
|
|
273
567
|
token: fileTokens.token,
|
|
274
568
|
cookie: fileTokens.cookie,
|
|
275
569
|
source: "file",
|
|
276
|
-
updatedAt: fileTokens.updatedAt
|
|
570
|
+
updatedAt: fileTokens.updatedAt,
|
|
571
|
+
lastAutoHealAttempt: fileTokens.lastAutoHealAttempt,
|
|
572
|
+
lastAutoHealError: fileTokens.lastAutoHealError,
|
|
573
|
+
stuckSince: fileTokens.stuckSince
|
|
277
574
|
};
|
|
278
575
|
}
|
|
279
576
|
|
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.
|
|
5
|
-
"description": "Slack MCP
|
|
4
|
+
"version": "4.1.2",
|
|
5
|
+
"description": "Slack MCP without OAuth — no app registration, no admin approval. Works with Claude Code, Cursor, Copilot (where the official server doesn't). 16 tools, one command.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "src/server.js",
|
|
8
8
|
"bin": {
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"build:demo-mobile:gif": "node scripts/build-mobile-demo.js --gif",
|
|
31
31
|
"record-demo": "node scripts/record-demo.js",
|
|
32
32
|
"social-preview:update": "node scripts/update-github-social-preview.js --headed",
|
|
33
|
+
"bookmarklet": "node scripts/generate-bookmarklet.js",
|
|
33
34
|
"cf:browser": "node scripts/cloudflare-browser-tool.js",
|
|
34
35
|
"verify:attribution-guardrail": "node scripts/verify-attribution-guardrail.js",
|
|
35
36
|
"verify:public-pages": "node scripts/verify-generated-public-pages.js",
|
|
@@ -61,7 +62,14 @@
|
|
|
61
62
|
"slack-integration",
|
|
62
63
|
"ai-agents",
|
|
63
64
|
"automation",
|
|
64
|
-
"productivity"
|
|
65
|
+
"productivity",
|
|
66
|
+
"oauth-free",
|
|
67
|
+
"no-admin-approval",
|
|
68
|
+
"cursor",
|
|
69
|
+
"copilot",
|
|
70
|
+
"windsurf",
|
|
71
|
+
"codex-cli",
|
|
72
|
+
"stealth-mode"
|
|
65
73
|
],
|
|
66
74
|
"author": {
|
|
67
75
|
"name": "Revasser",
|
package/public/index.html
CHANGED
|
@@ -255,8 +255,8 @@
|
|
|
255
255
|
<div class="container">
|
|
256
256
|
<h1>Slack Web API <span id="status" class="status"></span></h1>
|
|
257
257
|
<div style="background:rgba(240,194,70,0.08);border:1px solid rgba(240,194,70,0.2);border-radius:8px;padding:8px 14px;margin-bottom:16px;display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:8px;font-size:13px;color:#d4c48a">
|
|
258
|
-
<span>
|
|
259
|
-
<a href="https://mcp.revasserlabs.com" style="color:#f0c246;font-weight:600;text-decoration:none;white-space:nowrap" target="_blank">
|
|
258
|
+
<span>Hosted tiers live — <strong style="color:#f0c246">managed MCP endpoint, OAuth bridge for Claude.ai, encrypted storage</strong></span>
|
|
259
|
+
<a href="https://mcp.revasserlabs.com" style="color:#f0c246;font-weight:600;text-decoration:none;white-space:nowrap" target="_blank">See tiers →</a>
|
|
260
260
|
</div>
|
|
261
261
|
<div class="grid">
|
|
262
262
|
<div class="sidebar">
|
package/public/share.html
CHANGED
|
@@ -4,17 +4,17 @@
|
|
|
4
4
|
<meta charset="utf-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
6
|
<title>Slack MCP Server</title>
|
|
7
|
-
<meta name="description" content="
|
|
7
|
+
<meta name="description" content="No OAuth. No admin. 16 Slack tools for Claude, Cursor, Copilot, Gemini, and any MCP client. One command: npx -y @jtalk22/slack-mcp --setup">
|
|
8
8
|
<meta property="og:type" content="website">
|
|
9
|
-
<meta property="og:title" content="Slack MCP Server">
|
|
10
|
-
<meta property="og:description" content="
|
|
9
|
+
<meta property="og:title" content="Slack MCP Server — No OAuth, no admin, just your browser session">
|
|
10
|
+
<meta property="og:description" content="Slack's official MCP needs OAuth + admin. This one uses your browser session. 16 tools, works with Claude, Cursor, Copilot, Gemini.">
|
|
11
11
|
<meta property="og:url" content="https://jtalk22.github.io/slack-mcp-server/public/share.html">
|
|
12
12
|
<meta property="og:image" content="https://jtalk22.github.io/slack-mcp-server/docs/images/social-preview-v3.png">
|
|
13
13
|
<meta property="og:image:width" content="1280">
|
|
14
14
|
<meta property="og:image:height" content="640">
|
|
15
15
|
<meta name="twitter:card" content="summary_large_image">
|
|
16
|
-
<meta name="twitter:title" content="Slack MCP Server">
|
|
17
|
-
<meta name="twitter:description" content="
|
|
16
|
+
<meta name="twitter:title" content="Slack MCP Server — No OAuth, no admin, just your browser session">
|
|
17
|
+
<meta name="twitter:description" content="16 tools for Claude, Cursor, Copilot, Gemini. npx -y @jtalk22/slack-mcp --setup">
|
|
18
18
|
<meta name="twitter:image" content="https://jtalk22.github.io/slack-mcp-server/docs/images/social-preview-v3.png">
|
|
19
19
|
<link rel="icon" href="https://jtalk22.github.io/slack-mcp-server/docs/assets/icon-512.png" type="image/png">
|
|
20
20
|
<style>
|
|
@@ -107,7 +107,7 @@
|
|
|
107
107
|
<body>
|
|
108
108
|
<main class="wrap">
|
|
109
109
|
<h1>Slack MCP Server</h1>
|
|
110
|
-
<p class="sub">Give Claude full access to your Slack. Self-host 16 tools for free
|
|
110
|
+
<p class="sub">Give Claude full access to your Slack. Self-host 16 tools for free. Hosted version with semantic search, AI summaries, and permanent OAuth coming soon.</p>
|
|
111
111
|
|
|
112
112
|
<a class="preview" href="https://github.com/jtalk22/slack-mcp-server" rel="noopener">
|
|
113
113
|
<img src="https://jtalk22.github.io/slack-mcp-server/docs/images/social-preview-v3.png" alt="Slack MCP Server social preview card">
|
|
@@ -118,12 +118,12 @@
|
|
|
118
118
|
<a href="https://github.com/jtalk22/slack-mcp-server/blob/main/docs/SETUP.md" rel="noopener">Verify (`--version/--doctor/--status`)</a>
|
|
119
119
|
<a href="https://github.com/jtalk22/slack-mcp-server/releases/latest" rel="noopener">Latest Release</a>
|
|
120
120
|
<a href="https://jtalk22.github.io/slack-mcp-server/" rel="noopener">Autoplay Demo Landing</a>
|
|
121
|
-
<a href="https://jtalk22.github.io/slack-mcp-server/docs/videos/demo-
|
|
121
|
+
<a href="https://jtalk22.github.io/slack-mcp-server/docs/videos/demo-slack-mcp-mobile-20s.mp4" rel="noopener">20s Mobile Clip</a>
|
|
122
122
|
<a href="https://www.npmjs.com/package/@jtalk22/slack-mcp" rel="noopener">npm Package</a>
|
|
123
|
-
<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">
|
|
123
|
+
<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>
|
|
124
124
|
</div>
|
|
125
125
|
|
|
126
|
-
<p class="note"><strong>Verify in 30 seconds:</strong> <code>--version</code>, <code>--doctor</code>, <code>--status</code>. Self-host gives 16 tools with session-based auth. Works with any MCP client — Claude, ChatGPT, Cursor, Copilot, Gemini, Windsurf.
|
|
126
|
+
<p class="note"><strong>Verify in 30 seconds:</strong> <code>--version</code>, <code>--doctor</code>, <code>--status</code>. Self-host gives 16 tools with session-based auth. Works with any MCP client — Claude, ChatGPT, Cursor, Copilot, Gemini, Windsurf. Hosted version coming soon at <a href="https://mcp.revasserlabs.com">mcp.revasserlabs.com</a>.</p>
|
|
127
127
|
</main>
|
|
128
128
|
</body>
|
|
129
129
|
</html>
|
package/scripts/setup-wizard.js
CHANGED
|
@@ -131,10 +131,18 @@ async function runMacOSSetup(rl) {
|
|
|
131
131
|
}
|
|
132
132
|
print();
|
|
133
133
|
if (extractionError?.code === "apple_events_javascript_disabled") {
|
|
134
|
-
print(
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
134
|
+
print();
|
|
135
|
+
printBox([
|
|
136
|
+
"Chrome needs one setting enabled (one-time only):",
|
|
137
|
+
"",
|
|
138
|
+
"1. Open Chrome",
|
|
139
|
+
"2. Menu bar: View → Developer → Allow JavaScript",
|
|
140
|
+
" from Apple Events ✓",
|
|
141
|
+
"3. Run this command again",
|
|
142
|
+
], 55);
|
|
143
|
+
print();
|
|
144
|
+
print("Once enabled, --setup extracts tokens automatically.");
|
|
145
|
+
print("No DevTools, no copy-paste, just one command.");
|
|
138
146
|
} else {
|
|
139
147
|
print("Make sure:");
|
|
140
148
|
print(" 1. Chrome is running");
|
|
@@ -180,42 +188,83 @@ async function runManualSetup(rl) {
|
|
|
180
188
|
print();
|
|
181
189
|
if (IS_MACOS) {
|
|
182
190
|
info("Switching to manual token entry...");
|
|
191
|
+
info("Note: On macOS, the session cookie can be extracted automatically.");
|
|
183
192
|
} else {
|
|
184
193
|
info(`Detected platform: ${platform()}`);
|
|
185
194
|
warn("Auto-extraction not available on this platform.");
|
|
186
195
|
}
|
|
196
|
+
|
|
197
|
+
const consoleHotkey = IS_MACOS ? "Cmd+Option+J" : "Ctrl+Shift+J";
|
|
198
|
+
// Token-only one-liner (cookie is HttpOnly and cannot be read via document.cookie)
|
|
199
|
+
const oneLiner = `copy(JSON.parse(localStorage.localConfig_v2).teams[Object.keys(JSON.parse(localStorage.localConfig_v2).teams)[0]].token)`;
|
|
200
|
+
|
|
187
201
|
print();
|
|
188
|
-
print(
|
|
189
|
-
print();
|
|
190
|
-
print(`${colors.bold}Step 1:${colors.reset} Open Chrome and navigate to your Slack workspace`);
|
|
191
|
-
print(" https://app.slack.com");
|
|
202
|
+
print(`${colors.bold}Quick extract (recommended):${colors.reset}`);
|
|
192
203
|
print();
|
|
193
|
-
print(
|
|
204
|
+
print(` 1. Open Chrome → ${colors.cyan}app.slack.com${colors.reset} (must be logged in)`);
|
|
205
|
+
print(` 2. Press ${colors.cyan}${consoleHotkey}${colors.reset} to open the Console`);
|
|
206
|
+
print(` 3. Paste this one-liner and press Enter:`);
|
|
194
207
|
print();
|
|
195
|
-
|
|
208
|
+
printBox([oneLiner], oneLiner.length + 4);
|
|
196
209
|
print();
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
], 55);
|
|
210
|
+
print(` 4. Your token is now on the clipboard. Paste below.`);
|
|
211
|
+
if (IS_MACOS) {
|
|
212
|
+
print(` ${colors.dim}(Cookie will be extracted automatically from Chrome)${colors.reset}`);
|
|
213
|
+
}
|
|
202
214
|
print();
|
|
203
|
-
print(
|
|
215
|
+
print(`${colors.dim}(Or paste a JSON object with token+cookie, or a raw xoxc- token)${colors.reset}`);
|
|
204
216
|
print();
|
|
205
217
|
|
|
206
|
-
const
|
|
218
|
+
const input = await question(rl, `${colors.bold}Paste token:${colors.reset} `);
|
|
219
|
+
const trimmed = input.trim();
|
|
207
220
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
221
|
+
let token, cookie;
|
|
222
|
+
|
|
223
|
+
// Try JSON parse first (legacy one-liner output or manual JSON)
|
|
224
|
+
if (trimmed.startsWith('{')) {
|
|
225
|
+
try {
|
|
226
|
+
const parsed = JSON.parse(trimmed);
|
|
227
|
+
if (parsed.token) token = parsed.token;
|
|
228
|
+
if (parsed.cookie) cookie = parsed.cookie;
|
|
229
|
+
} catch (_) {
|
|
230
|
+
// Not valid JSON — fall through to raw token
|
|
231
|
+
}
|
|
211
232
|
}
|
|
212
233
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
234
|
+
// Treat as raw token
|
|
235
|
+
if (!token) {
|
|
236
|
+
token = trimmed;
|
|
237
|
+
if (!token.startsWith('xoxc-')) {
|
|
238
|
+
error("Invalid input. Expected a token starting with 'xoxc-'");
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// On macOS, try to extract cookie from Chrome's cookie database automatically
|
|
244
|
+
if (!cookie && IS_MACOS) {
|
|
245
|
+
print();
|
|
246
|
+
print("Extracting session cookie from Chrome...");
|
|
247
|
+
const chromeTokens = extractFromChrome();
|
|
248
|
+
if (chromeTokens?.cookie) {
|
|
249
|
+
cookie = chromeTokens.cookie;
|
|
250
|
+
success("Cookie extracted from Chrome automatically");
|
|
251
|
+
} else {
|
|
252
|
+
warn("Could not extract cookie automatically.");
|
|
253
|
+
print(` Paste the cookie manually. In Chrome: ${colors.cyan}Application${colors.reset} tab → ${colors.cyan}Cookies${colors.reset} → find '${colors.cyan}d${colors.reset}'`);
|
|
254
|
+
print();
|
|
255
|
+
const cookieInput = await question(rl, `${colors.bold}Paste cookie (xoxd-...):${colors.reset} `);
|
|
256
|
+
cookie = cookieInput.trim();
|
|
257
|
+
}
|
|
258
|
+
}
|
|
217
259
|
|
|
218
|
-
|
|
260
|
+
// Non-macOS: always ask for cookie
|
|
261
|
+
if (!cookie) {
|
|
262
|
+
print();
|
|
263
|
+
print(`Paste the cookie. In Chrome: ${colors.cyan}Application${colors.reset} tab → ${colors.cyan}Cookies${colors.reset} → find '${colors.cyan}d${colors.reset}'`);
|
|
264
|
+
print();
|
|
265
|
+
const cookieInput = await question(rl, `${colors.bold}Paste cookie (xoxd-...):${colors.reset} `);
|
|
266
|
+
cookie = cookieInput.trim();
|
|
267
|
+
}
|
|
219
268
|
|
|
220
269
|
if (!cookie.startsWith('xoxd-')) {
|
|
221
270
|
error("Invalid cookie. Cookie should start with 'xoxd-'");
|
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
|
|
5
|
+
"description": "Slack MCP without OAuth — no app registration, no admin approval. Works with Claude Code, Cursor, Copilot (where the official server doesn't). 16 tools, one command.",
|
|
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.
|
|
20
|
+
"version": "4.1.2",
|
|
21
21
|
"remotes": [
|
|
22
22
|
{
|
|
23
23
|
"type": "streamable-http",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
{
|
|
29
29
|
"registryType": "npm",
|
|
30
30
|
"identifier": "@jtalk22/slack-mcp",
|
|
31
|
-
"version": "4.
|
|
31
|
+
"version": "4.1.2",
|
|
32
32
|
"transport": {
|
|
33
33
|
"type": "stdio"
|
|
34
34
|
},
|
package/src/server.js
CHANGED
|
@@ -290,6 +290,22 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
290
290
|
};
|
|
291
291
|
}
|
|
292
292
|
} catch (error) {
|
|
293
|
+
if (error?.code === "token_auth_failed") {
|
|
294
|
+
return {
|
|
295
|
+
content: [{
|
|
296
|
+
type: "text",
|
|
297
|
+
text: JSON.stringify({
|
|
298
|
+
status: "error",
|
|
299
|
+
code: "token_auth_failed",
|
|
300
|
+
message: String(error?.message || error),
|
|
301
|
+
slack_error: error.slack_error || null,
|
|
302
|
+
extraction_error: error.extraction_error || null,
|
|
303
|
+
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."
|
|
304
|
+
}, null, 2)
|
|
305
|
+
}],
|
|
306
|
+
isError: true
|
|
307
|
+
};
|
|
308
|
+
}
|
|
293
309
|
return {
|
|
294
310
|
content: [{
|
|
295
311
|
type: "text",
|
|
@@ -323,8 +339,9 @@ async function main() {
|
|
|
323
339
|
}
|
|
324
340
|
|
|
325
341
|
// Background token health check (every 4 hours)
|
|
326
|
-
//
|
|
327
|
-
//
|
|
342
|
+
// unref() alone doesn't prevent StdioServerTransport from keeping the event
|
|
343
|
+
// loop alive after the MCP client disconnects — we add explicit shutdown
|
|
344
|
+
// handlers below to kill zombie processes on stdin EOF and signals.
|
|
328
345
|
const backgroundTimer = setInterval(async () => {
|
|
329
346
|
try {
|
|
330
347
|
const health = await checkTokenHealth(console);
|
|
@@ -339,6 +356,23 @@ async function main() {
|
|
|
339
356
|
}, BACKGROUND_REFRESH_INTERVAL);
|
|
340
357
|
backgroundTimer.unref();
|
|
341
358
|
|
|
359
|
+
// Explicit shutdown path prevents the zombie-process pileup we were seeing
|
|
360
|
+
// when Claude Code or another MCP client disconnected without signalling.
|
|
361
|
+
// StdioServerTransport doesn't exit the event loop on its own when stdin EOFs.
|
|
362
|
+
let shuttingDown = false;
|
|
363
|
+
const shutdown = (reason) => {
|
|
364
|
+
if (shuttingDown) return;
|
|
365
|
+
shuttingDown = true;
|
|
366
|
+
try { clearInterval(backgroundTimer); } catch {}
|
|
367
|
+
console.error(`slack-mcp-server exiting: ${reason}`);
|
|
368
|
+
process.exit(0);
|
|
369
|
+
};
|
|
370
|
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
371
|
+
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
372
|
+
process.on("SIGHUP", () => shutdown("SIGHUP"));
|
|
373
|
+
process.stdin.on("end", () => shutdown("stdin end (MCP client disconnected)"));
|
|
374
|
+
process.stdin.on("error", (err) => shutdown(`stdin error: ${err?.message || err}`));
|
|
375
|
+
|
|
342
376
|
// Start server
|
|
343
377
|
const transport = new StdioServerTransport();
|
|
344
378
|
await server.connect(transport);
|