@jtalk22/slack-mcp 4.1.0 → 4.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -7
- package/docs/DEPLOYMENT-MODES.md +56 -0
- package/docs/TROUBLESHOOTING.md +3 -1
- package/lib/handlers.js +92 -0
- package/lib/public-metadata.js +1 -1
- package/lib/public-pages.js +4 -4
- package/lib/slack-client.js +70 -17
- package/lib/token-store.js +279 -93
- package/lib/tools.js +160 -0
- package/lib/workflow-store.js +188 -0
- package/package.json +6 -3
- package/public/index.html +5 -4
- package/public/share.html +5 -5
- package/scripts/apply-template.js +117 -0
- package/scripts/setup-wizard.js +19 -0
- package/server.json +3 -3
- package/smithery.yaml +2 -0
- package/src/cli.js +3 -0
- package/src/server.js +58 -2
- package/templates/workflow-profiles/customer-feedback.json +10 -0
- package/templates/workflow-profiles/exec-monday.json +10 -0
- package/templates/workflow-profiles/incident-room.json +10 -0
- package/templates/workflow-profiles/oncall-handoff.json +10 -0
- package/templates/workflow-profiles/sprint-tracker.json +10 -0
- package/templates/workflow-profiles/support-triage.json +10 -0
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 } from "fs";
|
|
11
|
+
import { readFileSync, writeFileSync, existsSync, renameSync, unlinkSync, 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";
|
|
@@ -20,6 +20,12 @@ const KEYCHAIN_SERVICE = "slack-mcp-server";
|
|
|
20
20
|
// Platform detection
|
|
21
21
|
const IS_MACOS = platform() === 'darwin';
|
|
22
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
|
+
|
|
23
29
|
// Refresh lock to prevent concurrent extraction attempts
|
|
24
30
|
let refreshInProgress = null;
|
|
25
31
|
let lastExtractionError = null;
|
|
@@ -65,13 +71,45 @@ export function getFromFile() {
|
|
|
65
71
|
return {
|
|
66
72
|
token: data.SLACK_TOKEN,
|
|
67
73
|
cookie: data.SLACK_COOKIE,
|
|
68
|
-
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
|
|
69
78
|
};
|
|
70
79
|
} catch (e) {
|
|
71
80
|
return null;
|
|
72
81
|
}
|
|
73
82
|
}
|
|
74
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
|
+
|
|
75
113
|
/**
|
|
76
114
|
* Atomic write to prevent file corruption from concurrent writes
|
|
77
115
|
*/
|
|
@@ -109,7 +147,65 @@ const SLACK_TOKEN_PATHS = [
|
|
|
109
147
|
`window.boot_data?.api_token`,
|
|
110
148
|
];
|
|
111
149
|
|
|
112
|
-
//
|
|
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)
|
|
113
209
|
const CHROME_PROFILES = ['Default', 'Profile 1', 'Profile 2', 'Profile 3'];
|
|
114
210
|
|
|
115
211
|
function normalizeExtractionError(error) {
|
|
@@ -155,76 +251,118 @@ function normalizeExtractionError(error) {
|
|
|
155
251
|
}
|
|
156
252
|
|
|
157
253
|
/**
|
|
158
|
-
* Extract the Slack
|
|
159
|
-
*
|
|
160
|
-
*
|
|
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.
|
|
161
259
|
*/
|
|
162
|
-
function
|
|
163
|
-
const
|
|
260
|
+
function extractCookieForProfile(profileDir) {
|
|
261
|
+
const cookiesPath = join(profileDir, 'Cookies');
|
|
262
|
+
if (!existsSync(cookiesPath)) return null;
|
|
164
263
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
264
|
+
const tmpDir = mkdtempSync(join(tmpdir(), 'slack-mcp-'));
|
|
265
|
+
const tmpDb = join(tmpDir, 'Cookies');
|
|
266
|
+
try {
|
|
267
|
+
copyFileSync(cookiesPath, tmpDb);
|
|
169
268
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
copyFileSync(cookiesPath, tmpDb);
|
|
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();
|
|
175
273
|
|
|
176
|
-
|
|
177
|
-
const queryResult = execFileSync('sqlite3', [
|
|
178
|
-
tmpDb,
|
|
179
|
-
"SELECT hex(encrypted_value) FROM cookies WHERE host_key LIKE '%.slack.com%' AND name = 'd' LIMIT 1;"
|
|
180
|
-
], { encoding: 'utf-8', timeout: 5000 }).trim();
|
|
274
|
+
try { unlinkSync(tmpDb); unlinkSync(tmpDir); } catch {}
|
|
181
275
|
|
|
182
|
-
|
|
183
|
-
try { unlinkSync(tmpDb); unlinkSync(tmpDir); } catch {}
|
|
276
|
+
if (!queryResult) return null;
|
|
184
277
|
|
|
185
|
-
|
|
278
|
+
const encrypted = Buffer.from(queryResult, 'hex');
|
|
279
|
+
if (encrypted.length < 4) return null;
|
|
186
280
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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();
|
|
190
285
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
], { encoding: 'utf-8', timeout: 5000 }).trim();
|
|
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;
|
|
195
289
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
290
|
+
const ciphertext = encrypted.subarray(3);
|
|
291
|
+
const key = pbkdf2Sync(safeStoragePassword, 'saltysalt', 1003, 16, 'sha1');
|
|
292
|
+
const iv = Buffer.alloc(16, ' ');
|
|
199
293
|
|
|
200
|
-
|
|
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
|
+
}
|
|
201
301
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
+
}
|
|
205
311
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
+
}
|
|
213
324
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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;
|
|
218
339
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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 {
|
|
224
363
|
continue;
|
|
225
364
|
}
|
|
226
365
|
}
|
|
227
|
-
|
|
228
366
|
return null;
|
|
229
367
|
}
|
|
230
368
|
|
|
@@ -272,11 +410,27 @@ end tell`;
|
|
|
272
410
|
/**
|
|
273
411
|
* Extract tokens from Chrome (macOS only).
|
|
274
412
|
*
|
|
275
|
-
*
|
|
276
|
-
*
|
|
277
|
-
*
|
|
278
|
-
*
|
|
279
|
-
*
|
|
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
|
|
280
434
|
*/
|
|
281
435
|
function extractFromChromeInternal() {
|
|
282
436
|
lastExtractionError = null;
|
|
@@ -285,56 +439,85 @@ function extractFromChromeInternal() {
|
|
|
285
439
|
lastExtractionError = {
|
|
286
440
|
code: "unsupported_platform",
|
|
287
441
|
message: "Chrome auto-extraction is only available on macOS.",
|
|
288
|
-
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."
|
|
289
443
|
};
|
|
290
444
|
return null;
|
|
291
445
|
}
|
|
292
446
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
cookie = extractCookieFromChromeDB();
|
|
297
|
-
} catch (e) {
|
|
298
|
-
lastExtractionError = normalizeExtractionError(e);
|
|
299
|
-
return null;
|
|
300
|
-
}
|
|
447
|
+
const mode = getExtractionMode();
|
|
448
|
+
const base = getChromeBase();
|
|
449
|
+
const profiles = enumerateChromeProfiles();
|
|
301
450
|
|
|
302
|
-
if (
|
|
451
|
+
if (profiles.length === 0) {
|
|
303
452
|
lastExtractionError = {
|
|
304
|
-
code: "
|
|
305
|
-
message: "
|
|
306
|
-
detail:
|
|
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.`
|
|
307
456
|
};
|
|
308
457
|
return null;
|
|
309
458
|
}
|
|
310
459
|
|
|
311
|
-
//
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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') {
|
|
472
|
+
lastExtractionError = {
|
|
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.`
|
|
476
|
+
};
|
|
477
|
+
return null;
|
|
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
|
+
}
|
|
502
|
+
|
|
503
|
+
if (cookieSeen && !lastExtractionError) {
|
|
319
504
|
lastExtractionError = {
|
|
320
505
|
code: "apple_events_javascript_disabled",
|
|
321
|
-
message: "Cookie extracted, but
|
|
322
|
-
detail: "
|
|
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."
|
|
323
508
|
};
|
|
509
|
+
return null;
|
|
324
510
|
}
|
|
325
|
-
return null;
|
|
326
511
|
}
|
|
327
512
|
|
|
328
|
-
if (!
|
|
513
|
+
if (!lastExtractionError) {
|
|
329
514
|
lastExtractionError = {
|
|
330
|
-
code: "
|
|
331
|
-
message: "Could not extract Slack
|
|
332
|
-
detail:
|
|
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.`
|
|
333
518
|
};
|
|
334
|
-
return null;
|
|
335
519
|
}
|
|
336
|
-
|
|
337
|
-
return { token, cookie };
|
|
520
|
+
return null;
|
|
338
521
|
}
|
|
339
522
|
|
|
340
523
|
/**
|
|
@@ -384,7 +567,10 @@ function getStoredTokens() {
|
|
|
384
567
|
token: fileTokens.token,
|
|
385
568
|
cookie: fileTokens.cookie,
|
|
386
569
|
source: "file",
|
|
387
|
-
updatedAt: fileTokens.updatedAt
|
|
570
|
+
updatedAt: fileTokens.updatedAt,
|
|
571
|
+
lastAutoHealAttempt: fileTokens.lastAutoHealAttempt,
|
|
572
|
+
lastAutoHealError: fileTokens.lastAutoHealError,
|
|
573
|
+
stuckSince: fileTokens.stuckSince
|
|
388
574
|
};
|
|
389
575
|
}
|
|
390
576
|
|
package/lib/tools.js
CHANGED
|
@@ -399,5 +399,165 @@ export const TOOLS = [
|
|
|
399
399
|
idempotentHint: true,
|
|
400
400
|
openWorldHint: true
|
|
401
401
|
}
|
|
402
|
+
},
|
|
403
|
+
// ============ Workflow Profile Primitives (OSS) ============
|
|
404
|
+
// Local JSON storage at ~/.slack-mcp-workflows.json. Defines the
|
|
405
|
+
// structural primitive that the hosted AI brain (smart_search,
|
|
406
|
+
// catch_me_up, triage) consumes to return structured JSON.
|
|
407
|
+
{
|
|
408
|
+
name: "slack_workflow_save",
|
|
409
|
+
description: "Save or update a workflow profile that binds a workflow_kind (support_inbox | incident_room | exec_brief | product_launch_watch | custom) to channels, priority people, retention mode, and summary cadence. Stored locally at ~/.slack-mcp-workflows.json. Hosted brain reads these to return structured JSON per the workflow_kind.",
|
|
410
|
+
inputSchema: {
|
|
411
|
+
type: "object",
|
|
412
|
+
properties: {
|
|
413
|
+
profile_name: {
|
|
414
|
+
type: "string",
|
|
415
|
+
description: "Unique name for this workflow profile (e.g. 'morning-exec-brief', 'on-call-rotation')"
|
|
416
|
+
},
|
|
417
|
+
workflow_kind: {
|
|
418
|
+
type: "string",
|
|
419
|
+
enum: ["support_inbox", "incident_room", "exec_brief", "product_launch_watch", "custom"],
|
|
420
|
+
description: "Workflow kind. Determines structured JSON output shape from the hosted AI brain."
|
|
421
|
+
},
|
|
422
|
+
channels: {
|
|
423
|
+
type: "array",
|
|
424
|
+
items: { type: "string" },
|
|
425
|
+
description: "Slack channel IDs to read (e.g. ['C012345', 'C067890'])"
|
|
426
|
+
},
|
|
427
|
+
priority_people: {
|
|
428
|
+
type: "array",
|
|
429
|
+
items: { type: "string" },
|
|
430
|
+
description: "Slack user IDs whose messages get extra weight in summaries"
|
|
431
|
+
},
|
|
432
|
+
retention_mode: {
|
|
433
|
+
type: "string",
|
|
434
|
+
enum: ["ephemeral", "persistent"],
|
|
435
|
+
description: "Token retention mode for hosted execution. Default ephemeral."
|
|
436
|
+
},
|
|
437
|
+
summary_cadence: {
|
|
438
|
+
type: "string",
|
|
439
|
+
enum: ["on_demand", "daily_8am", "weekly_monday"],
|
|
440
|
+
description: "When the hosted brain auto-runs slack_catch_me_up against this profile. on_demand only on hosted free; daily_8am and weekly_monday require Pro or Team."
|
|
441
|
+
}
|
|
442
|
+
},
|
|
443
|
+
required: ["profile_name", "workflow_kind"]
|
|
444
|
+
},
|
|
445
|
+
annotations: {
|
|
446
|
+
title: "Save Workflow Profile",
|
|
447
|
+
readOnlyHint: false,
|
|
448
|
+
destructiveHint: false,
|
|
449
|
+
idempotentHint: true,
|
|
450
|
+
openWorldHint: false
|
|
451
|
+
}
|
|
452
|
+
},
|
|
453
|
+
{
|
|
454
|
+
name: "slack_workflows",
|
|
455
|
+
description: "List all saved workflow profiles from ~/.slack-mcp-workflows.json. Optionally filter by workflow_kind. Returns profile_name, channels, priority_people, retention_mode, summary_cadence, structured_keys, created_at, updated_at.",
|
|
456
|
+
inputSchema: {
|
|
457
|
+
type: "object",
|
|
458
|
+
properties: {
|
|
459
|
+
workflow_kind: {
|
|
460
|
+
type: "string",
|
|
461
|
+
enum: ["support_inbox", "incident_room", "exec_brief", "product_launch_watch", "custom"],
|
|
462
|
+
description: "Optional filter — return only profiles of this workflow_kind"
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
},
|
|
466
|
+
annotations: {
|
|
467
|
+
title: "List Workflow Profiles",
|
|
468
|
+
readOnlyHint: true,
|
|
469
|
+
idempotentHint: true,
|
|
470
|
+
openWorldHint: false
|
|
471
|
+
}
|
|
472
|
+
},
|
|
473
|
+
// ============ Hosted-Only AI Tools (OSS = upgrade stubs) ============
|
|
474
|
+
// These tool definitions appear in every MCP client's tool list so users
|
|
475
|
+
// discover them. The OSS handlers return a structured upgrade message
|
|
476
|
+
// pointing at mcp.revasserlabs.com — the hosted worker actually runs them.
|
|
477
|
+
{
|
|
478
|
+
name: "slack_smart_search",
|
|
479
|
+
description: "Semantic + lexical hybrid search across your indexed Slack history. Returns ranked results with relevance scores, channel context, thread context, and matched terms. Hosted-only (requires Vectorize + Workers AI). Free tier ships 10 calls/month; upgrade to Pro $9/mo for unlimited at mcp.revasserlabs.com/pricing.",
|
|
480
|
+
inputSchema: {
|
|
481
|
+
type: "object",
|
|
482
|
+
properties: {
|
|
483
|
+
query: {
|
|
484
|
+
type: "string",
|
|
485
|
+
description: "Natural language or keyword query (semantic + lexical hybrid)"
|
|
486
|
+
},
|
|
487
|
+
channel_ids: {
|
|
488
|
+
type: "array",
|
|
489
|
+
items: { type: "string" },
|
|
490
|
+
description: "Optional — restrict search to these channel IDs"
|
|
491
|
+
},
|
|
492
|
+
days_back: {
|
|
493
|
+
type: "number",
|
|
494
|
+
description: "Optional — restrict search to the last N days (max 90 on Pro+, 7 on Free)"
|
|
495
|
+
},
|
|
496
|
+
limit: {
|
|
497
|
+
type: "number",
|
|
498
|
+
description: "Maximum results to return (default 10, max 50)"
|
|
499
|
+
}
|
|
500
|
+
},
|
|
501
|
+
required: ["query"]
|
|
502
|
+
},
|
|
503
|
+
annotations: {
|
|
504
|
+
title: "Smart Search (hosted)",
|
|
505
|
+
readOnlyHint: true,
|
|
506
|
+
idempotentHint: true,
|
|
507
|
+
openWorldHint: true
|
|
508
|
+
}
|
|
509
|
+
},
|
|
510
|
+
{
|
|
511
|
+
name: "slack_catch_me_up",
|
|
512
|
+
description: "Run a structured catch-up against a saved workflow profile. Returns structured JSON per the profile's workflow_kind: support_inbox returns {open_threads, ack_lag, owner_gaps, escalations, next_actions}; incident_room returns {incident_summary, timeline, open_risks, owner_gaps, next_actions}; exec_brief returns {summary, decisions, risks, asks, action_items}; product_launch_watch returns {launch_signals, feedback_themes, blockers, metrics, next_actions}; custom returns {summary, highlights, open_questions, next_actions}. Hosted-only. Free tier ships 3 calls/month; Pro $9/mo unlocks unlimited (scheduled morning DM at 8am workspace tz rolling out Q2 2026).",
|
|
513
|
+
inputSchema: {
|
|
514
|
+
type: "object",
|
|
515
|
+
properties: {
|
|
516
|
+
profile_name: {
|
|
517
|
+
type: "string",
|
|
518
|
+
description: "Name of a workflow profile saved via slack_workflow_save (or use --apply-template at install time to seed one)"
|
|
519
|
+
},
|
|
520
|
+
since: {
|
|
521
|
+
type: "string",
|
|
522
|
+
description: "Optional ISO8601 timestamp — only consider Slack messages newer than this. Default: 24 hours ago for daily-cadence profiles, 7 days for weekly."
|
|
523
|
+
}
|
|
524
|
+
},
|
|
525
|
+
required: ["profile_name"]
|
|
526
|
+
},
|
|
527
|
+
annotations: {
|
|
528
|
+
title: "Catch Me Up (hosted)",
|
|
529
|
+
readOnlyHint: true,
|
|
530
|
+
idempotentHint: false,
|
|
531
|
+
openWorldHint: true
|
|
532
|
+
}
|
|
533
|
+
},
|
|
534
|
+
{
|
|
535
|
+
name: "slack_triage",
|
|
536
|
+
description: "Classify and route Slack threads against a workflow profile. Returns triage decisions per thread: priority (low|medium|high|urgent), suggested owner, escalation flag, time-sensitivity, and a routing recommendation. Hosted-only. Free tier ships 5 triage runs per day; Pro $9/mo unlocks unlimited.",
|
|
537
|
+
inputSchema: {
|
|
538
|
+
type: "object",
|
|
539
|
+
properties: {
|
|
540
|
+
profile_name: {
|
|
541
|
+
type: "string",
|
|
542
|
+
description: "Name of a workflow profile saved via slack_workflow_save"
|
|
543
|
+
},
|
|
544
|
+
channel_ids: {
|
|
545
|
+
type: "array",
|
|
546
|
+
items: { type: "string" },
|
|
547
|
+
description: "Optional — restrict triage to these channels (defaults to profile's channels)"
|
|
548
|
+
},
|
|
549
|
+
thread_ts: {
|
|
550
|
+
type: "string",
|
|
551
|
+
description: "Optional — triage a specific thread instead of the full inbox"
|
|
552
|
+
}
|
|
553
|
+
},
|
|
554
|
+
required: ["profile_name"]
|
|
555
|
+
},
|
|
556
|
+
annotations: {
|
|
557
|
+
title: "Triage (hosted)",
|
|
558
|
+
readOnlyHint: true,
|
|
559
|
+
idempotentHint: false,
|
|
560
|
+
openWorldHint: true
|
|
561
|
+
}
|
|
402
562
|
}
|
|
403
563
|
];
|