@jtalk22/slack-mcp 1.2.3 → 2.0.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.
Files changed (39) hide show
  1. package/README.md +23 -7
  2. package/docs/API.md +11 -4
  3. package/docs/COMMUNICATION-STYLE.md +1 -0
  4. package/docs/COMPATIBILITY.md +19 -0
  5. package/docs/HN-LAUNCH.md +32 -32
  6. package/docs/INDEX.md +10 -1
  7. package/docs/INSTALL-PROOF.md +18 -0
  8. package/docs/LAUNCH-COPY-v2.0.0.md +59 -0
  9. package/docs/LAUNCH-MATRIX.md +20 -0
  10. package/docs/LAUNCH-OPS.md +70 -0
  11. package/docs/RELEASE-HEALTH.md +81 -0
  12. package/docs/SETUP.md +2 -0
  13. package/docs/TROUBLESHOOTING.md +6 -4
  14. package/docs/WEB-API.md +12 -1
  15. package/docs/release-health/2026-02-25.md +33 -0
  16. package/docs/release-health/2026-02-26.md +33 -0
  17. package/docs/release-health/24h-delta.md +21 -0
  18. package/docs/release-health/24h-end.md +33 -0
  19. package/docs/release-health/24h-start.md +33 -0
  20. package/docs/release-health/latest.md +33 -0
  21. package/docs/release-health/launch-log-template.md +21 -0
  22. package/docs/release-health/version-parity.md +21 -0
  23. package/lib/handlers.js +121 -85
  24. package/lib/slack-client.js +37 -17
  25. package/lib/token-store.js +103 -30
  26. package/package.json +26 -39
  27. package/public/demo-claude.html +4 -4
  28. package/public/demo-video.html +2 -2
  29. package/public/demo.html +2 -2
  30. package/scripts/build-release-health-delta.js +201 -0
  31. package/scripts/check-public-language.sh +25 -0
  32. package/scripts/check-version-parity.js +162 -0
  33. package/scripts/collect-release-health.js +150 -0
  34. package/scripts/setup-wizard.js +35 -9
  35. package/scripts/token-cli.js +6 -4
  36. package/scripts/verify-install-flow.js +107 -2
  37. package/src/server-http.js +26 -4
  38. package/src/server.js +23 -7
  39. package/src/web-server.js +61 -23
@@ -0,0 +1,162 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
4
+ import { dirname, join } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ const __dirname = dirname(fileURLToPath(import.meta.url));
8
+ const repoRoot = join(__dirname, "..");
9
+
10
+ const pkg = JSON.parse(readFileSync(join(repoRoot, "package.json"), "utf8"));
11
+ const serverMeta = JSON.parse(readFileSync(join(repoRoot, "server.json"), "utf8"));
12
+
13
+ const outputArg = process.argv.includes("--out")
14
+ ? process.argv[process.argv.indexOf("--out") + 1]
15
+ : "docs/release-health/version-parity.md";
16
+ const allowPropagation = process.argv.includes("--allow-propagation");
17
+
18
+ const mcpServerName = serverMeta.name;
19
+ const smitheryEndpoint = "https://server.smithery.ai/jtalk22/slack-mcp-server";
20
+
21
+ async function fetchJson(url) {
22
+ const res = await fetch(url);
23
+ if (!res.ok) {
24
+ throw new Error(`HTTP ${res.status} for ${url}`);
25
+ }
26
+ return res.json();
27
+ }
28
+
29
+ async function fetchText(url) {
30
+ const res = await fetch(url);
31
+ if (!res.ok) {
32
+ throw new Error(`HTTP ${res.status} for ${url}`);
33
+ }
34
+ return res.text();
35
+ }
36
+
37
+ function row(surface, version, status, note = "") {
38
+ return `| ${surface} | ${version || "n/a"} | ${status} | ${note} |`;
39
+ }
40
+
41
+ async function main() {
42
+ const localVersion = pkg.version;
43
+ const localServerVersion = serverMeta.version;
44
+ const localServerPkgVersion = serverMeta.packages?.[0]?.version || null;
45
+
46
+ let npmVersion = null;
47
+ let mcpRegistryVersion = null;
48
+ let smitheryReachable = null;
49
+ let npmError = null;
50
+ let mcpError = null;
51
+ let smitheryError = null;
52
+
53
+ try {
54
+ const npmMeta = await fetchJson(`https://registry.npmjs.org/${encodeURIComponent(pkg.name)}`);
55
+ npmVersion = npmMeta?.["dist-tags"]?.latest || null;
56
+ } catch (error) {
57
+ npmError = String(error?.message || error);
58
+ }
59
+
60
+ try {
61
+ const registry = await fetchJson(
62
+ `https://registry.modelcontextprotocol.io/v0/servers/${encodeURIComponent(mcpServerName)}/versions/latest`
63
+ );
64
+ mcpRegistryVersion = registry?.server?.version || null;
65
+ } catch (error) {
66
+ mcpError = String(error?.message || error);
67
+ }
68
+
69
+ try {
70
+ const html = await fetchText(smitheryEndpoint);
71
+ smitheryReachable = html.length > 0;
72
+ } catch (error) {
73
+ smitheryError = String(error?.message || error);
74
+ }
75
+
76
+ const parityChecks = [
77
+ { name: "package.json vs server.json", ok: localVersion === localServerVersion },
78
+ { name: "package.json vs server.json package", ok: localVersion === localServerPkgVersion },
79
+ { name: "npm latest", ok: npmVersion === localVersion },
80
+ { name: "MCP registry latest", ok: mcpRegistryVersion === localVersion },
81
+ ];
82
+
83
+ const externalMismatches = parityChecks
84
+ .filter((check) => !check.ok && (check.name === "npm latest" || check.name === "MCP registry latest"));
85
+ const hardFailures = parityChecks
86
+ .filter((check) => !check.ok && check.name !== "npm latest" && check.name !== "MCP registry latest");
87
+
88
+ const now = new Date().toISOString();
89
+ const lines = [
90
+ "# Version Parity Report",
91
+ "",
92
+ `- Generated: ${now}`,
93
+ `- Local target version: ${localVersion}`,
94
+ "",
95
+ "## Surface Matrix",
96
+ "",
97
+ "| Surface | Version | Status | Notes |",
98
+ "|---|---|---|---|",
99
+ row(
100
+ "package.json",
101
+ localVersion,
102
+ "ok"
103
+ ),
104
+ row(
105
+ "server.json (root)",
106
+ localServerVersion,
107
+ localServerVersion === localVersion ? "ok" : "mismatch"
108
+ ),
109
+ row(
110
+ "server.json (package entry)",
111
+ localServerPkgVersion,
112
+ localServerPkgVersion === localVersion ? "ok" : "mismatch"
113
+ ),
114
+ row(
115
+ "npm dist-tag latest",
116
+ npmVersion,
117
+ npmVersion === localVersion ? "ok" : "mismatch",
118
+ npmError ? `fetch_error: ${npmError}` : ""
119
+ ),
120
+ row(
121
+ "MCP Registry latest",
122
+ mcpRegistryVersion,
123
+ mcpRegistryVersion === localVersion ? "ok" : "mismatch",
124
+ mcpError ? `fetch_error: ${mcpError}` : ""
125
+ ),
126
+ row(
127
+ "Smithery endpoint",
128
+ "n/a",
129
+ smitheryReachable ? "reachable" : "unreachable",
130
+ smitheryError ? `check_error: ${smitheryError}` : "Version check is manual."
131
+ ),
132
+ "",
133
+ "## Interpretation",
134
+ "",
135
+ hardFailures.length === 0
136
+ ? "- Local metadata parity: pass."
137
+ : `- Local metadata parity: fail (${hardFailures.map((f) => f.name).join(", ")}).`,
138
+ externalMismatches.length === 0
139
+ ? "- External parity: pass."
140
+ : `- External parity mismatch: ${externalMismatches.map((f) => f.name).join(", ")}.`,
141
+ allowPropagation && externalMismatches.length > 0
142
+ ? "- Propagation mode enabled: external mismatch accepted temporarily."
143
+ : "- Propagation mode disabled: external mismatch is a release gate failure.",
144
+ ];
145
+
146
+ const outPath = join(repoRoot, outputArg);
147
+ mkdirSync(dirname(outPath), { recursive: true });
148
+ writeFileSync(outPath, `${lines.join("\n")}\n`, "utf8");
149
+ console.log(`Wrote ${outputArg}`);
150
+
151
+ if (hardFailures.length > 0) {
152
+ process.exit(1);
153
+ }
154
+ if (!allowPropagation && externalMismatches.length > 0) {
155
+ process.exit(1);
156
+ }
157
+ }
158
+
159
+ main().catch((error) => {
160
+ console.error(error);
161
+ process.exit(1);
162
+ });
@@ -0,0 +1,150 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { execSync } from "node:child_process";
4
+ import { mkdirSync, writeFileSync } from "node:fs";
5
+ import { join, resolve } from "node:path";
6
+
7
+ const REPO =
8
+ process.env.RELEASE_HEALTH_REPO ||
9
+ process.env.GROWTH_REPO ||
10
+ "jtalk22/slack-mcp-server";
11
+ const NPM_PACKAGE =
12
+ process.env.RELEASE_HEALTH_NPM_PACKAGE ||
13
+ process.env.GROWTH_NPM_PACKAGE ||
14
+ "@jtalk22/slack-mcp";
15
+
16
+ function safeGhApi(path) {
17
+ try {
18
+ const out = execSync(`gh api ${path}`, { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
19
+ return JSON.parse(out);
20
+ } catch {
21
+ return null;
22
+ }
23
+ }
24
+
25
+ async function fetchJson(url) {
26
+ const response = await fetch(url);
27
+ if (!response.ok) {
28
+ throw new Error(`Request failed: ${url} (${response.status})`);
29
+ }
30
+ return response.json();
31
+ }
32
+
33
+ function toDateSlug(date) {
34
+ const y = date.getFullYear();
35
+ const m = String(date.getMonth() + 1).padStart(2, "0");
36
+ const d = String(date.getDate()).padStart(2, "0");
37
+ return `${y}-${m}-${d}`;
38
+ }
39
+
40
+ function countNonPrIssues(items) {
41
+ if (!Array.isArray(items)) return 0;
42
+ return items.filter((item) => item && !item.pull_request).length;
43
+ }
44
+
45
+ function buildMarkdown(data) {
46
+ const lines = [];
47
+ lines.push("# Release Health Snapshot");
48
+ lines.push("");
49
+ lines.push(`- Generated: ${data.generatedAt}`);
50
+ lines.push(`- Repo: \`${REPO}\``);
51
+ lines.push(`- Package: \`${NPM_PACKAGE}\``);
52
+ lines.push("");
53
+
54
+ lines.push("## Install Signals");
55
+ lines.push("");
56
+ lines.push(`- npm downloads (last week): ${data.npm.lastWeek ?? "n/a"}`);
57
+ lines.push(`- npm downloads (last month): ${data.npm.lastMonth ?? "n/a"}`);
58
+ lines.push(`- npm latest version: ${data.npm.latestVersion ?? "n/a"}`);
59
+ lines.push("");
60
+
61
+ lines.push("## GitHub Reach");
62
+ lines.push("");
63
+ lines.push(`- stars: ${data.github.stars ?? "n/a"}`);
64
+ lines.push(`- forks: ${data.github.forks ?? "n/a"}`);
65
+ lines.push(`- open issues: ${data.github.openIssues ?? "n/a"}`);
66
+ lines.push(`- 14d views: ${data.github.viewsCount ?? "n/a"}`);
67
+ lines.push(`- 14d unique visitors: ${data.github.viewsUniques ?? "n/a"}`);
68
+ lines.push(`- 14d clones: ${data.github.clonesCount ?? "n/a"}`);
69
+ lines.push(`- 14d unique cloners: ${data.github.clonesUniques ?? "n/a"}`);
70
+ lines.push(`- deployment-intake submissions (all-time): ${data.github.deploymentIntakeCount ?? "n/a"}`);
71
+ lines.push("");
72
+
73
+ lines.push("## 14-Day Reliability Targets (v2.0.0 Cycle)");
74
+ lines.push("");
75
+ lines.push("- weekly downloads: >= 180");
76
+ lines.push("- qualified deployment-intake submissions: >= 2");
77
+ lines.push("- maintainer support load: <= 2 hours/week");
78
+ lines.push("");
79
+
80
+ lines.push("## Notes");
81
+ lines.push("");
82
+ lines.push("- Update this snapshot daily during active release windows, then weekly.");
83
+ lines.push("- Track deployment-intake quality and support load manually in issue notes.");
84
+
85
+ return `${lines.join("\n")}\n`;
86
+ }
87
+
88
+ async function main() {
89
+ const now = new Date();
90
+ const generatedAt = now.toISOString();
91
+ const dateSlug = toDateSlug(now);
92
+
93
+ let npmWeek = null;
94
+ let npmMonth = null;
95
+ let npmMeta = null;
96
+
97
+ try {
98
+ npmWeek = await fetchJson(`https://api.npmjs.org/downloads/point/last-week/${encodeURIComponent(NPM_PACKAGE)}`);
99
+ } catch {}
100
+
101
+ try {
102
+ npmMonth = await fetchJson(`https://api.npmjs.org/downloads/point/last-month/${encodeURIComponent(NPM_PACKAGE)}`);
103
+ } catch {}
104
+
105
+ try {
106
+ npmMeta = await fetchJson(`https://registry.npmjs.org/${encodeURIComponent(NPM_PACKAGE)}`);
107
+ } catch {}
108
+
109
+ const repoInfo = safeGhApi(`repos/${REPO}`) || {};
110
+ const views = safeGhApi(`repos/${REPO}/traffic/views`) || {};
111
+ const clones = safeGhApi(`repos/${REPO}/traffic/clones`) || {};
112
+ const intakeIssues = safeGhApi(`repos/${REPO}/issues?state=all&labels=deployment-intake&per_page=100`) || [];
113
+
114
+ const data = {
115
+ generatedAt,
116
+ npm: {
117
+ lastWeek: npmWeek?.downloads ?? null,
118
+ lastMonth: npmMonth?.downloads ?? null,
119
+ latestVersion: npmMeta?.["dist-tags"]?.latest ?? null,
120
+ },
121
+ github: {
122
+ stars: repoInfo.stargazers_count ?? null,
123
+ forks: repoInfo.forks_count ?? null,
124
+ openIssues: repoInfo.open_issues_count ?? null,
125
+ viewsCount: views.count ?? null,
126
+ viewsUniques: views.uniques ?? null,
127
+ clonesCount: clones.count ?? null,
128
+ clonesUniques: clones.uniques ?? null,
129
+ deploymentIntakeCount: countNonPrIssues(intakeIssues),
130
+ },
131
+ };
132
+
133
+ const markdown = buildMarkdown(data);
134
+
135
+ const metricsDir = resolve("docs", "release-health");
136
+ const datedPath = join(metricsDir, `${dateSlug}.md`);
137
+ const latestPath = join(metricsDir, "latest.md");
138
+
139
+ mkdirSync(metricsDir, { recursive: true });
140
+ writeFileSync(datedPath, markdown);
141
+ writeFileSync(latestPath, markdown);
142
+
143
+ console.log(`Wrote ${datedPath}`);
144
+ console.log(`Wrote ${latestPath}`);
145
+ }
146
+
147
+ main().catch((error) => {
148
+ console.error(error.message);
149
+ process.exit(1);
150
+ });
@@ -13,9 +13,9 @@
13
13
  import { platform } from "os";
14
14
  import * as readline from "readline";
15
15
  import {
16
- loadTokens,
17
16
  saveTokens,
18
17
  extractFromChrome,
18
+ getLastExtractionError,
19
19
  isAutoRefreshAvailable,
20
20
  TOKEN_FILE,
21
21
  getFromFile,
@@ -23,8 +23,9 @@ import {
23
23
  } from "../lib/token-store.js";
24
24
 
25
25
  const IS_MACOS = platform() === 'darwin';
26
- const VERSION = "1.2.3";
26
+ const VERSION = "2.0.0";
27
27
  const MIN_NODE_MAJOR = 20;
28
+ const AUTH_TEST_URL = process.env.SLACK_MCP_AUTH_TEST_URL || "https://slack.com/api/auth.test";
28
29
 
29
30
  // ANSI colors
30
31
  const colors = {
@@ -78,7 +79,7 @@ async function pressEnterToContinue(rl) {
78
79
 
79
80
  async function validateTokens(token, cookie) {
80
81
  try {
81
- const response = await fetch("https://slack.com/api/auth.test", {
82
+ const response = await fetch(AUTH_TEST_URL, {
82
83
  method: "POST",
83
84
  headers: {
84
85
  "Authorization": `Bearer ${token}`,
@@ -118,13 +119,27 @@ async function runMacOSSetup(rl) {
118
119
  const tokens = extractFromChrome();
119
120
 
120
121
  if (!tokens) {
122
+ const extractionError = getLastExtractionError();
121
123
  print();
122
124
  error("Could not extract tokens from Chrome.");
125
+ if (extractionError) {
126
+ print(`Reason: ${extractionError.message}`);
127
+ if (extractionError.detail) {
128
+ print(`Detail: ${extractionError.detail}`);
129
+ }
130
+ }
123
131
  print();
124
- print("Make sure:");
125
- print(" 1. Chrome is running");
126
- print(" 2. You have a Slack tab open (app.slack.com)");
127
- print(" 3. You're logged into that workspace");
132
+ if (extractionError?.code === "apple_events_javascript_disabled") {
133
+ print("Fix and retry:");
134
+ print(" 1. In Chrome menu: View > Developer > Allow JavaScript from Apple Events");
135
+ print(" 2. Keep Slack open in a Chrome tab (app.slack.com)");
136
+ print(" 3. Re-run: npx -y @jtalk22/slack-mcp --setup");
137
+ } else {
138
+ print("Make sure:");
139
+ print(" 1. Chrome is running");
140
+ print(" 2. You have a Slack tab open (app.slack.com)");
141
+ print(" 3. You're logged into that workspace");
142
+ }
128
143
  print();
129
144
 
130
145
  const retry = await question(rl, "Try manual entry instead? (y/n): ");
@@ -233,17 +248,21 @@ async function runManualSetup(rl) {
233
248
  }
234
249
 
235
250
  async function showStatus() {
236
- const creds = loadTokens();
251
+ const creds = getDoctorCredentials();
237
252
 
238
253
  if (!creds) {
239
254
  error("No tokens found");
255
+ print("Code: missing_credentials");
256
+ print("Message: No credentials available from environment, file, or keychain.");
240
257
  print();
241
258
  print("Run setup wizard: npx -y @jtalk22/slack-mcp --setup");
242
259
  process.exit(1);
243
260
  }
244
261
 
245
262
  print(`Token source: ${creds.source}`);
246
- print(`Token file: ${TOKEN_FILE}`);
263
+ if (creds.path) {
264
+ print(`Token file: ${creds.path}`);
265
+ }
247
266
  if (creds.updatedAt) {
248
267
  print(`Last updated: ${creds.updatedAt}`);
249
268
  }
@@ -252,6 +271,7 @@ async function showStatus() {
252
271
  const result = await validateTokens(creds.token, creds.cookie);
253
272
  if (!result.valid) {
254
273
  error("Status: INVALID");
274
+ print("Code: auth_failed");
255
275
  print(`Error: ${result.error}`);
256
276
  print();
257
277
  print("Run setup wizard to refresh: npx -y @jtalk22/slack-mcp --setup");
@@ -259,6 +279,8 @@ async function showStatus() {
259
279
  }
260
280
 
261
281
  success("Status: VALID");
282
+ print("Code: ok");
283
+ print("Message: Slack auth valid.");
262
284
  print(`User: ${result.user}`);
263
285
  print(`Team: ${result.team}`);
264
286
  print(`User ID: ${result.userId}`);
@@ -315,6 +337,7 @@ async function runDoctor() {
315
337
  const nodeMajor = parseNodeMajor();
316
338
  if (Number.isNaN(nodeMajor) || nodeMajor < MIN_NODE_MAJOR) {
317
339
  error(`Node.js ${process.versions.node} detected (requires Node ${MIN_NODE_MAJOR}+)`);
340
+ print("Code: runtime_node_unsupported");
318
341
  print();
319
342
  print("Next action:");
320
343
  print(` npx -y @jtalk22/slack-mcp --doctor # rerun after upgrading Node ${MIN_NODE_MAJOR}+`);
@@ -325,6 +348,7 @@ async function runDoctor() {
325
348
  const creds = getDoctorCredentials();
326
349
  if (!creds) {
327
350
  error("Credentials: not found");
351
+ print("Code: missing_credentials");
328
352
  print();
329
353
  print("Next action:");
330
354
  print(" npx -y @jtalk22/slack-mcp --setup");
@@ -345,6 +369,7 @@ async function runDoctor() {
345
369
  if (!validation.valid) {
346
370
  const exitCode = classifyAuthError(validation.error);
347
371
  error(`Slack auth failed: ${validation.error}`);
372
+ print(`Code: ${exitCode === 2 ? "auth_invalid" : "runtime_auth_check_failed"}`);
348
373
  print();
349
374
  print("Next action:");
350
375
  if (exitCode === 2) {
@@ -357,6 +382,7 @@ async function runDoctor() {
357
382
  }
358
383
 
359
384
  success(`Slack auth valid for ${validation.user} @ ${validation.team}`);
385
+ print("Code: ok");
360
386
  print();
361
387
  print("Ready. Next command:");
362
388
  print(" npx -y @jtalk22/slack-mcp");
@@ -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);
@@ -1,11 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { spawnSync } from "node:child_process";
4
- import { mkdtempSync, rmSync } from "node:fs";
4
+ import { mkdtempSync, readFileSync, rmSync } from "node:fs";
5
5
  import { tmpdir } from "node:os";
6
- import { join } from "node:path";
6
+ import { dirname, join } from "node:path";
7
+ import { fileURLToPath } from "node:url";
7
8
 
8
9
  const PKG = "@jtalk22/slack-mcp";
10
+ const repoRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
11
+ const strictPublished = process.argv.includes("--strict-published");
12
+ const localVersion = JSON.parse(readFileSync(join(repoRoot, "package.json"), "utf8")).version;
9
13
 
10
14
  function runNpx(args, options = {}) {
11
15
  const cmdArgs = ["-y", PKG, ...args];
@@ -45,6 +49,40 @@ function printResult(label, result) {
45
49
  }
46
50
  }
47
51
 
52
+ function runLocalSetupStatus(options = {}) {
53
+ const result = spawnSync("node", [join(repoRoot, "scripts/setup-wizard.js"), "--status"], {
54
+ cwd: repoRoot,
55
+ env: options.env,
56
+ encoding: "utf8",
57
+ timeout: 120000,
58
+ });
59
+
60
+ return {
61
+ args: "node scripts/setup-wizard.js --status",
62
+ status: result.status,
63
+ stdout: (result.stdout || "").trim(),
64
+ stderr: (result.stderr || "").trim(),
65
+ error: result.error,
66
+ };
67
+ }
68
+
69
+ function runLocalDoctor(options = {}) {
70
+ const result = spawnSync("node", [join(repoRoot, "scripts/setup-wizard.js"), "--doctor"], {
71
+ cwd: repoRoot,
72
+ env: options.env,
73
+ encoding: "utf8",
74
+ timeout: 120000,
75
+ });
76
+
77
+ return {
78
+ args: "node scripts/setup-wizard.js --doctor",
79
+ status: result.status,
80
+ stdout: (result.stdout || "").trim(),
81
+ stderr: (result.stderr || "").trim(),
82
+ error: result.error,
83
+ };
84
+ }
85
+
48
86
  function main() {
49
87
  const testHome = mkdtempSync(join(tmpdir(), "slack-mcp-install-check-"));
50
88
 
@@ -61,6 +99,18 @@ function main() {
61
99
  "Expected --version to exit 0",
62
100
  versionResult.stderr || versionResult.stdout,
63
101
  );
102
+ const publishedMatchesLocal = versionResult.stdout.includes(localVersion);
103
+ if (strictPublished) {
104
+ assert(
105
+ publishedMatchesLocal,
106
+ `Expected published npx version to match local ${localVersion}`,
107
+ versionResult.stdout,
108
+ );
109
+ } else if (!publishedMatchesLocal) {
110
+ console.log(
111
+ `warning: npx resolved ${versionResult.stdout || "unknown"} while local version is ${localVersion}; strict published checks are deferred until publish.`
112
+ );
113
+ }
64
114
 
65
115
  const helpResult = runNpx(["--help"], { cwd: testHome, env });
66
116
  printResult("help", helpResult);
@@ -77,6 +127,61 @@ function main() {
77
127
  "Expected --status to exit non-zero when credentials are missing",
78
128
  statusResult.stderr || statusResult.stdout,
79
129
  );
130
+ if (strictPublished) {
131
+ assert(
132
+ !statusResult.stderr.includes("Attempting Chrome auto-extraction"),
133
+ "Expected npx --status to be read-only without auto-extraction side effects",
134
+ statusResult.stderr,
135
+ );
136
+ } else if (statusResult.stderr.includes("Attempting Chrome auto-extraction")) {
137
+ console.log("warning: published npx --status still has extraction side effects; re-run with --strict-published after publish.");
138
+ }
139
+
140
+ const localStatusResult = runLocalSetupStatus({ env });
141
+ printResult("local-status", localStatusResult);
142
+ assert(
143
+ localStatusResult.status !== 0,
144
+ "Expected local --status to exit non-zero when credentials are missing",
145
+ localStatusResult.stderr || localStatusResult.stdout,
146
+ );
147
+ assert(
148
+ !localStatusResult.stderr.includes("Attempting Chrome auto-extraction"),
149
+ "Expected local --status to be read-only without auto-extraction side effects",
150
+ localStatusResult.stderr,
151
+ );
152
+
153
+ const localDoctorMissingResult = runLocalDoctor({ env });
154
+ printResult("local-doctor-missing", localDoctorMissingResult);
155
+ assert(
156
+ localDoctorMissingResult.status === 1,
157
+ "Expected local --doctor to exit 1 when credentials are missing",
158
+ localDoctorMissingResult.stderr || localDoctorMissingResult.stdout,
159
+ );
160
+
161
+ const invalidEnv = {
162
+ ...env,
163
+ SLACK_TOKEN: "xoxc-invalid-token",
164
+ SLACK_COOKIE: "xoxd-invalid-cookie",
165
+ };
166
+ const localDoctorInvalidResult = runLocalDoctor({ env: invalidEnv });
167
+ printResult("local-doctor-invalid", localDoctorInvalidResult);
168
+ assert(
169
+ localDoctorInvalidResult.status === 2,
170
+ "Expected local --doctor to exit 2 when credentials are invalid",
171
+ localDoctorInvalidResult.stderr || localDoctorInvalidResult.stdout,
172
+ );
173
+
174
+ const runtimeEnv = {
175
+ ...invalidEnv,
176
+ SLACK_MCP_AUTH_TEST_URL: "http://127.0.0.1:9/auth.test"
177
+ };
178
+ const localDoctorRuntimeResult = runLocalDoctor({ env: runtimeEnv });
179
+ printResult("local-doctor-runtime", localDoctorRuntimeResult);
180
+ assert(
181
+ localDoctorRuntimeResult.status === 3,
182
+ "Expected local --doctor to exit 3 when runtime connectivity fails",
183
+ localDoctorRuntimeResult.stderr || localDoctorRuntimeResult.stdout,
184
+ );
80
185
 
81
186
  console.log("\nInstall flow verification passed.");
82
187
  } finally {
@@ -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.3";
33
+ const SERVER_VERSION = "2.0.0";
34
34
  const PORT = process.env.PORT || 3000;
35
35
 
36
36
  // Create MCP server
@@ -74,13 +74,29 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
74
74
  return await handleListUsers(args);
75
75
  default:
76
76
  return {
77
- content: [{ type: "text", text: `Unknown tool: ${name}` }],
77
+ content: [{
78
+ type: "text",
79
+ text: JSON.stringify({
80
+ status: "error",
81
+ code: "unknown_tool",
82
+ message: `Unknown tool: ${name}`,
83
+ next_action: "Call tools/list to inspect available tool names."
84
+ }, null, 2)
85
+ }],
78
86
  isError: true
79
87
  };
80
88
  }
81
89
  } catch (error) {
82
90
  return {
83
- content: [{ type: "text", text: `Error: ${error.message}` }],
91
+ content: [{
92
+ type: "text",
93
+ text: JSON.stringify({
94
+ status: "error",
95
+ code: "tool_call_failed",
96
+ message: String(error?.message || error),
97
+ next_action: "Retry with validated input payload."
98
+ }, null, 2)
99
+ }],
84
100
  isError: true
85
101
  };
86
102
  }
@@ -110,7 +126,13 @@ const httpServer = http.createServer(async (req, res) => {
110
126
  // Health check endpoint
111
127
  if (req.url === '/health') {
112
128
  res.writeHead(200, { 'Content-Type': 'application/json' });
113
- res.end(JSON.stringify({ status: 'ok', server: SERVER_NAME, version: SERVER_VERSION }));
129
+ res.end(JSON.stringify({
130
+ status: 'ok',
131
+ code: 'ok',
132
+ message: 'HTTP transport healthy',
133
+ server: SERVER_NAME,
134
+ version: SERVER_VERSION
135
+ }));
114
136
  return;
115
137
  }
116
138