@jtalk22/slack-mcp 1.2.2 → 1.2.4

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.
@@ -12,11 +12,19 @@
12
12
 
13
13
  import { platform } from "os";
14
14
  import * as readline from "readline";
15
- import { loadTokens, saveTokens, extractFromChrome, isAutoRefreshAvailable, TOKEN_FILE } from "../lib/token-store.js";
16
- import { slackAPI } from "../lib/slack-client.js";
15
+ import {
16
+ saveTokens,
17
+ extractFromChrome,
18
+ getLastExtractionError,
19
+ isAutoRefreshAvailable,
20
+ TOKEN_FILE,
21
+ getFromFile,
22
+ getFromKeychain,
23
+ } from "../lib/token-store.js";
17
24
 
18
25
  const IS_MACOS = platform() === 'darwin';
19
- const VERSION = "1.2.2";
26
+ const VERSION = "1.2.4";
27
+ const MIN_NODE_MAJOR = 20;
20
28
 
21
29
  // ANSI colors
22
30
  const colors = {
@@ -69,27 +77,25 @@ async function pressEnterToContinue(rl) {
69
77
  }
70
78
 
71
79
  async function validateTokens(token, cookie) {
72
- const hadOldToken = Object.prototype.hasOwnProperty.call(process.env, "SLACK_TOKEN");
73
- const hadOldCookie = Object.prototype.hasOwnProperty.call(process.env, "SLACK_COOKIE");
74
- const oldToken = process.env.SLACK_TOKEN;
75
- const oldCookie = process.env.SLACK_COOKIE;
76
-
77
- // Temporarily set env vars for validation
78
- process.env.SLACK_TOKEN = token;
79
- process.env.SLACK_COOKIE = cookie;
80
-
81
80
  try {
82
- const result = await slackAPI("auth.test", {});
83
- return { valid: true, user: result.user, team: result.team };
81
+ const response = await fetch("https://slack.com/api/auth.test", {
82
+ method: "POST",
83
+ headers: {
84
+ "Authorization": `Bearer ${token}`,
85
+ "Cookie": `d=${cookie}`,
86
+ "Content-Type": "application/json; charset=utf-8",
87
+ },
88
+ body: JSON.stringify({}),
89
+ });
90
+
91
+ const result = await response.json();
92
+ if (!result.ok) {
93
+ return { valid: false, error: result.error || "auth.test_failed" };
94
+ }
95
+
96
+ return { valid: true, user: result.user, team: result.team, userId: result.user_id };
84
97
  } catch (e) {
85
98
  return { valid: false, error: e.message };
86
- } finally {
87
- // Always restore prior process env state
88
- if (hadOldToken) process.env.SLACK_TOKEN = oldToken;
89
- else delete process.env.SLACK_TOKEN;
90
-
91
- if (hadOldCookie) process.env.SLACK_COOKIE = oldCookie;
92
- else delete process.env.SLACK_COOKIE;
93
99
  }
94
100
  }
95
101
 
@@ -112,13 +118,27 @@ async function runMacOSSetup(rl) {
112
118
  const tokens = extractFromChrome();
113
119
 
114
120
  if (!tokens) {
121
+ const extractionError = getLastExtractionError();
115
122
  print();
116
123
  error("Could not extract tokens from Chrome.");
124
+ if (extractionError) {
125
+ print(`Reason: ${extractionError.message}`);
126
+ if (extractionError.detail) {
127
+ print(`Detail: ${extractionError.detail}`);
128
+ }
129
+ }
117
130
  print();
118
- print("Make sure:");
119
- print(" 1. Chrome is running");
120
- print(" 2. You have a Slack tab open (app.slack.com)");
121
- print(" 3. You're logged into that workspace");
131
+ if (extractionError?.code === "apple_events_javascript_disabled") {
132
+ print("Fix and retry:");
133
+ print(" 1. In Chrome menu: View > Developer > Allow JavaScript from Apple Events");
134
+ print(" 2. Keep Slack open in a Chrome tab (app.slack.com)");
135
+ print(" 3. Re-run: npx -y @jtalk22/slack-mcp --setup");
136
+ } else {
137
+ print("Make sure:");
138
+ print(" 1. Chrome is running");
139
+ print(" 2. You have a Slack tab open (app.slack.com)");
140
+ print(" 3. You're logged into that workspace");
141
+ }
122
142
  print();
123
143
 
124
144
  const retry = await question(rl, "Try manual entry instead? (y/n): ");
@@ -227,7 +247,7 @@ async function runManualSetup(rl) {
227
247
  }
228
248
 
229
249
  async function showStatus() {
230
- const creds = loadTokens();
250
+ const creds = getDoctorCredentials();
231
251
 
232
252
  if (!creds) {
233
253
  error("No tokens found");
@@ -237,29 +257,126 @@ async function showStatus() {
237
257
  }
238
258
 
239
259
  print(`Token source: ${creds.source}`);
240
- print(`Token file: ${TOKEN_FILE}`);
260
+ if (creds.path) {
261
+ print(`Token file: ${creds.path}`);
262
+ }
241
263
  if (creds.updatedAt) {
242
264
  print(`Last updated: ${creds.updatedAt}`);
243
265
  }
244
266
  print();
245
267
 
246
- try {
247
- // Need to set env vars for slackAPI to work
248
- process.env.SLACK_TOKEN = creds.token;
249
- process.env.SLACK_COOKIE = creds.cookie;
250
-
251
- const result = await slackAPI("auth.test", {});
252
- success("Status: VALID");
253
- print(`User: ${result.user}`);
254
- print(`Team: ${result.team}`);
255
- print(`User ID: ${result.user_id}`);
256
- } catch (e) {
268
+ const result = await validateTokens(creds.token, creds.cookie);
269
+ if (!result.valid) {
257
270
  error("Status: INVALID");
258
- print(`Error: ${e.message}`);
271
+ print(`Error: ${result.error}`);
259
272
  print();
260
273
  print("Run setup wizard to refresh: npx -y @jtalk22/slack-mcp --setup");
261
274
  process.exit(1);
262
275
  }
276
+
277
+ success("Status: VALID");
278
+ print(`User: ${result.user}`);
279
+ print(`Team: ${result.team}`);
280
+ print(`User ID: ${result.userId}`);
281
+ }
282
+
283
+ function getDoctorCredentials() {
284
+ if (process.env.SLACK_TOKEN && process.env.SLACK_COOKIE) {
285
+ return { token: process.env.SLACK_TOKEN, cookie: process.env.SLACK_COOKIE, source: "environment" };
286
+ }
287
+
288
+ const fileTokens = getFromFile();
289
+ if (fileTokens?.token && fileTokens?.cookie) {
290
+ return {
291
+ token: fileTokens.token,
292
+ cookie: fileTokens.cookie,
293
+ source: "file",
294
+ path: TOKEN_FILE,
295
+ updatedAt: fileTokens.updatedAt,
296
+ };
297
+ }
298
+
299
+ if (IS_MACOS) {
300
+ const keychainToken = getFromKeychain("token");
301
+ const keychainCookie = getFromKeychain("cookie");
302
+ if (keychainToken && keychainCookie) {
303
+ return { token: keychainToken, cookie: keychainCookie, source: "keychain" };
304
+ }
305
+ }
306
+
307
+ return null;
308
+ }
309
+
310
+ function classifyAuthError(rawError) {
311
+ const msg = String(rawError || "").toLowerCase();
312
+ if (
313
+ msg.includes("invalid_auth") ||
314
+ msg.includes("token_expired") ||
315
+ msg.includes("not_authed") ||
316
+ msg.includes("account_inactive")
317
+ ) {
318
+ return 2;
319
+ }
320
+ return 3;
321
+ }
322
+
323
+ function parseNodeMajor() {
324
+ return Number.parseInt(process.versions.node.split(".")[0], 10);
325
+ }
326
+
327
+ async function runDoctor() {
328
+ print(`${colors.bold}slack-mcp-server doctor${colors.reset}`);
329
+ print();
330
+
331
+ const nodeMajor = parseNodeMajor();
332
+ if (Number.isNaN(nodeMajor) || nodeMajor < MIN_NODE_MAJOR) {
333
+ error(`Node.js ${process.versions.node} detected (requires Node ${MIN_NODE_MAJOR}+)`);
334
+ print();
335
+ print("Next action:");
336
+ print(` npx -y @jtalk22/slack-mcp --doctor # rerun after upgrading Node ${MIN_NODE_MAJOR}+`);
337
+ process.exit(3);
338
+ }
339
+ success(`Node.js ${process.versions.node} (supported)`);
340
+
341
+ const creds = getDoctorCredentials();
342
+ if (!creds) {
343
+ error("Credentials: not found");
344
+ print();
345
+ print("Next action:");
346
+ print(" npx -y @jtalk22/slack-mcp --setup");
347
+ process.exit(1);
348
+ }
349
+
350
+ success(`Credentials loaded from: ${creds.source}`);
351
+ if (creds.path) {
352
+ print(`Path: ${creds.path}`);
353
+ }
354
+ if (creds.updatedAt) {
355
+ print(`Last updated: ${creds.updatedAt}`);
356
+ }
357
+
358
+ print();
359
+ print("Validating Slack auth...");
360
+ const validation = await validateTokens(creds.token, creds.cookie);
361
+ if (!validation.valid) {
362
+ const exitCode = classifyAuthError(validation.error);
363
+ error(`Slack auth failed: ${validation.error}`);
364
+ print();
365
+ print("Next action:");
366
+ if (exitCode === 2) {
367
+ print(" npx -y @jtalk22/slack-mcp --setup");
368
+ } else {
369
+ print(" Check network connectivity and retry:");
370
+ print(" npx -y @jtalk22/slack-mcp --doctor");
371
+ }
372
+ process.exit(exitCode);
373
+ }
374
+
375
+ success(`Slack auth valid for ${validation.user} @ ${validation.team}`);
376
+ print();
377
+ print("Ready. Next command:");
378
+ print(" npx -y @jtalk22/slack-mcp");
379
+ process.exit(0);
263
380
  }
264
381
 
265
382
  async function showHelp() {
@@ -271,6 +388,7 @@ async function showHelp() {
271
388
  print(" npx -y @jtalk22/slack-mcp Start MCP server (stdio)");
272
389
  print(" npx -y @jtalk22/slack-mcp --setup Interactive token setup wizard");
273
390
  print(" npx -y @jtalk22/slack-mcp --status Check token health");
391
+ print(" npx -y @jtalk22/slack-mcp --doctor Run runtime and auth diagnostics");
274
392
  print(" npx -y @jtalk22/slack-mcp --version Print version");
275
393
  print(" npx -y @jtalk22/slack-mcp --help Show this help");
276
394
  print();
@@ -296,6 +414,10 @@ async function main() {
296
414
  case 'status':
297
415
  await showStatus();
298
416
  return;
417
+ case '--doctor':
418
+ case 'doctor':
419
+ await runDoctor();
420
+ return;
299
421
  case '--version':
300
422
  case '-v':
301
423
  print(`slack-mcp-server v${VERSION}`);
@@ -3,7 +3,7 @@
3
3
  * Token CLI - Manage Slack tokens
4
4
  */
5
5
 
6
- import { loadTokens, saveTokens, extractFromChrome, getFromFile, TOKEN_FILE, KEYCHAIN_SERVICE } from "../lib/token-store.js";
6
+ import { loadTokensReadOnly, saveTokens, extractFromChrome, getFromFile, TOKEN_FILE, KEYCHAIN_SERVICE } from "../lib/token-store.js";
7
7
  import { slackAPI } from "../lib/slack-client.js";
8
8
  import * as readline from "readline";
9
9
 
@@ -35,7 +35,7 @@ async function main() {
35
35
  }
36
36
 
37
37
  async function showStatus() {
38
- const creds = loadTokens();
38
+ const creds = loadTokensReadOnly();
39
39
  if (!creds) {
40
40
  console.log("No tokens found");
41
41
  console.log("");
@@ -46,11 +46,13 @@ async function showStatus() {
46
46
  }
47
47
 
48
48
  console.log("Token source:", creds.source);
49
- console.log("Token file:", TOKEN_FILE);
49
+ if (creds.source === "file") {
50
+ console.log("Token file:", TOKEN_FILE);
51
+ }
50
52
  console.log("");
51
53
 
52
54
  try {
53
- const result = await slackAPI("auth.test", {});
55
+ const result = await slackAPI("auth.test", {}, { retryOnAuthFail: false });
54
56
  console.log("Status: VALID");
55
57
  console.log("User:", result.user);
56
58
  console.log("Team:", result.team);
@@ -0,0 +1,157 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawnSync } from "node:child_process";
4
+ import { mkdtempSync, rmSync } from "node:fs";
5
+ import { tmpdir } from "node:os";
6
+ import { dirname, join } from "node:path";
7
+ import { fileURLToPath } from "node:url";
8
+
9
+ const PKG = "@jtalk22/slack-mcp";
10
+ const repoRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
11
+
12
+ function runNpx(args, options = {}) {
13
+ const cmdArgs = ["-y", PKG, ...args];
14
+ const result = spawnSync("npx", cmdArgs, {
15
+ cwd: options.cwd,
16
+ env: options.env,
17
+ encoding: "utf8",
18
+ timeout: 120000,
19
+ });
20
+
21
+ return {
22
+ args: cmdArgs.join(" "),
23
+ status: result.status,
24
+ stdout: (result.stdout || "").trim(),
25
+ stderr: (result.stderr || "").trim(),
26
+ error: result.error,
27
+ };
28
+ }
29
+
30
+ function assert(condition, message, details = "") {
31
+ if (!condition) {
32
+ const suffix = details ? `\n${details}` : "";
33
+ throw new Error(`${message}${suffix}`);
34
+ }
35
+ }
36
+
37
+ function printResult(label, result) {
38
+ console.log(`\n[${label}] npx ${result.args}`);
39
+ console.log(`exit=${result.status}`);
40
+ if (result.stdout) {
41
+ console.log("stdout:");
42
+ console.log(result.stdout);
43
+ }
44
+ if (result.stderr) {
45
+ console.log("stderr:");
46
+ console.log(result.stderr);
47
+ }
48
+ }
49
+
50
+ function runLocalSetupStatus(options = {}) {
51
+ const result = spawnSync("node", [join(repoRoot, "scripts/setup-wizard.js"), "--status"], {
52
+ cwd: repoRoot,
53
+ env: options.env,
54
+ encoding: "utf8",
55
+ timeout: 120000,
56
+ });
57
+
58
+ return {
59
+ args: "node scripts/setup-wizard.js --status",
60
+ status: result.status,
61
+ stdout: (result.stdout || "").trim(),
62
+ stderr: (result.stderr || "").trim(),
63
+ error: result.error,
64
+ };
65
+ }
66
+
67
+ function runLocalDoctor(options = {}) {
68
+ const result = spawnSync("node", [join(repoRoot, "scripts/setup-wizard.js"), "--doctor"], {
69
+ cwd: repoRoot,
70
+ env: options.env,
71
+ encoding: "utf8",
72
+ timeout: 120000,
73
+ });
74
+
75
+ return {
76
+ args: "node scripts/setup-wizard.js --doctor",
77
+ status: result.status,
78
+ stdout: (result.stdout || "").trim(),
79
+ stderr: (result.stderr || "").trim(),
80
+ error: result.error,
81
+ };
82
+ }
83
+
84
+ function main() {
85
+ const testHome = mkdtempSync(join(tmpdir(), "slack-mcp-install-check-"));
86
+
87
+ // Force a clean environment so --status reflects missing credentials.
88
+ const env = { ...process.env, HOME: testHome, USERPROFILE: testHome };
89
+ delete env.SLACK_TOKEN;
90
+ delete env.SLACK_COOKIE;
91
+
92
+ try {
93
+ const versionResult = runNpx(["--version"], { cwd: testHome, env });
94
+ printResult("version", versionResult);
95
+ assert(
96
+ versionResult.status === 0,
97
+ "Expected --version to exit 0",
98
+ versionResult.stderr || versionResult.stdout,
99
+ );
100
+
101
+ const helpResult = runNpx(["--help"], { cwd: testHome, env });
102
+ printResult("help", helpResult);
103
+ assert(
104
+ helpResult.status === 0,
105
+ "Expected --help to exit 0",
106
+ helpResult.stderr || helpResult.stdout,
107
+ );
108
+
109
+ const statusResult = runNpx(["--status"], { cwd: testHome, env });
110
+ printResult("status", statusResult);
111
+ assert(
112
+ statusResult.status !== 0,
113
+ "Expected --status to exit non-zero when credentials are missing",
114
+ statusResult.stderr || statusResult.stdout,
115
+ );
116
+
117
+ const localStatusResult = runLocalSetupStatus({ env });
118
+ printResult("local-status", localStatusResult);
119
+ assert(
120
+ localStatusResult.status !== 0,
121
+ "Expected local --status to exit non-zero when credentials are missing",
122
+ localStatusResult.stderr || localStatusResult.stdout,
123
+ );
124
+ assert(
125
+ !localStatusResult.stderr.includes("Attempting Chrome auto-extraction"),
126
+ "Expected local --status to be read-only without auto-extraction side effects",
127
+ localStatusResult.stderr,
128
+ );
129
+
130
+ const localDoctorMissingResult = runLocalDoctor({ env });
131
+ printResult("local-doctor-missing", localDoctorMissingResult);
132
+ assert(
133
+ localDoctorMissingResult.status === 1,
134
+ "Expected local --doctor to exit 1 when credentials are missing",
135
+ localDoctorMissingResult.stderr || localDoctorMissingResult.stdout,
136
+ );
137
+
138
+ const invalidEnv = {
139
+ ...env,
140
+ SLACK_TOKEN: "xoxc-invalid-token",
141
+ SLACK_COOKIE: "xoxd-invalid-cookie",
142
+ };
143
+ const localDoctorInvalidResult = runLocalDoctor({ env: invalidEnv });
144
+ printResult("local-doctor-invalid", localDoctorInvalidResult);
145
+ assert(
146
+ localDoctorInvalidResult.status === 2,
147
+ "Expected local --doctor to exit 2 when credentials are invalid",
148
+ localDoctorInvalidResult.stderr || localDoctorInvalidResult.stdout,
149
+ );
150
+
151
+ console.log("\nInstall flow verification passed.");
152
+ } finally {
153
+ rmSync(testHome, { recursive: true, force: true });
154
+ }
155
+ }
156
+
157
+ main();
package/src/cli.js CHANGED
@@ -20,6 +20,7 @@ const firstArg = args[0];
20
20
  const WIZARD_ARGS = new Set([
21
21
  "--setup", "setup",
22
22
  "--status", "status",
23
+ "--doctor", "doctor",
23
24
  "--version", "-v",
24
25
  "--help", "-h", "help",
25
26
  ]);
@@ -30,7 +30,7 @@ import {
30
30
  } from "../lib/handlers.js";
31
31
 
32
32
  const SERVER_NAME = "slack-mcp-server";
33
- const SERVER_VERSION = "1.2.2";
33
+ const SERVER_VERSION = "1.2.4";
34
34
  const PORT = process.env.PORT || 3000;
35
35
 
36
36
  // Create MCP server
package/src/server.js CHANGED
@@ -11,7 +11,7 @@
11
11
  * - Network error retry with exponential backoff
12
12
  * - Background token health monitoring
13
13
  *
14
- * @version 1.2.2
14
+ * @version 1.2.4
15
15
  */
16
16
 
17
17
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
@@ -25,7 +25,7 @@ import {
25
25
  ReadResourceRequestSchema,
26
26
  } from "@modelcontextprotocol/sdk/types.js";
27
27
 
28
- import { loadTokens } from "../lib/token-store.js";
28
+ import { loadTokensReadOnly } from "../lib/token-store.js";
29
29
  import { checkTokenHealth } from "../lib/slack-client.js";
30
30
  import { TOOLS } from "../lib/tools.js";
31
31
  import {
@@ -47,7 +47,7 @@ const BACKGROUND_REFRESH_INTERVAL = 4 * 60 * 60 * 1000;
47
47
 
48
48
  // Package info
49
49
  const SERVER_NAME = "slack-mcp-server";
50
- const SERVER_VERSION = "1.2.2";
50
+ const SERVER_VERSION = "1.2.4";
51
51
 
52
52
  // MCP Prompts - predefined prompt templates for common Slack operations
53
53
  const PROMPTS = [
@@ -271,10 +271,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
271
271
  // Main entry point
272
272
  async function main() {
273
273
  // Check for credentials at startup
274
- const credentials = loadTokens();
274
+ const credentials = loadTokensReadOnly();
275
275
  if (!credentials) {
276
276
  console.error("WARNING: No Slack credentials found at startup");
277
- console.error("Will attempt Chrome auto-extraction on first API call");
277
+ console.error("Use npx -y @jtalk22/slack-mcp --setup to configure credentials");
278
278
  } else {
279
279
  console.error(`Credentials loaded from: ${credentials.source}`);
280
280
 
package/src/web-server.js CHANGED
@@ -5,7 +5,7 @@
5
5
  * Exposes Slack MCP tools as REST endpoints for browser access.
6
6
  * Run alongside or instead of the MCP server for web-based access.
7
7
  *
8
- * @version 1.2.2
8
+ * @version 1.2.4
9
9
  */
10
10
 
11
11
  import express from "express";
@@ -15,7 +15,7 @@ import { dirname, join } from "path";
15
15
  import { existsSync, readFileSync, writeFileSync } from "fs";
16
16
  import { execSync } from "child_process";
17
17
  import { homedir } from "os";
18
- import { loadTokens } from "../lib/token-store.js";
18
+ import { loadTokensReadOnly } from "../lib/token-store.js";
19
19
 
20
20
  const __dirname = dirname(fileURLToPath(import.meta.url));
21
21
  import {
@@ -116,7 +116,7 @@ function extractContent(result) {
116
116
  app.get("/", (req, res) => {
117
117
  res.json({
118
118
  name: "Slack Web API Server",
119
- version: "1.2.2",
119
+ version: "1.2.4",
120
120
  status: "running",
121
121
  endpoints: [
122
122
  "GET /health",
@@ -283,10 +283,10 @@ app.get("/users/:id", authenticate, async (req, res) => {
283
283
  // Start server
284
284
  async function main() {
285
285
  // Check for credentials
286
- const credentials = loadTokens();
286
+ const credentials = loadTokensReadOnly();
287
287
  if (!credentials) {
288
288
  console.error("WARNING: No Slack credentials found");
289
- console.error("Run: npm run tokens:auto");
289
+ console.error("Run: npx -y @jtalk22/slack-mcp --setup");
290
290
  } else {
291
291
  console.log(`Credentials loaded from: ${credentials.source}`);
292
292
  }
@@ -295,7 +295,7 @@ async function main() {
295
295
  app.listen(PORT, '127.0.0.1', () => {
296
296
  // Print to stderr to keep logs clean (stdout reserved for JSON in some setups)
297
297
  console.error(`\n${"═".repeat(60)}`);
298
- console.error(` Slack Web API Server v1.2.2`);
298
+ console.error(` Slack Web API Server v1.2.4`);
299
299
  console.error(`${"═".repeat(60)}`);
300
300
  console.error(`\n Dashboard: http://localhost:${PORT}/?key=${API_KEY}`);
301
301
  console.error(`\n API Key: ${API_KEY}`);