@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.
- package/README.md +23 -7
- package/docs/API.md +11 -4
- package/docs/COMMUNICATION-STYLE.md +1 -0
- package/docs/COMPATIBILITY.md +19 -0
- package/docs/HN-LAUNCH.md +32 -32
- package/docs/INDEX.md +10 -1
- package/docs/INSTALL-PROOF.md +18 -0
- package/docs/LAUNCH-COPY-v2.0.0.md +59 -0
- package/docs/LAUNCH-MATRIX.md +20 -0
- package/docs/LAUNCH-OPS.md +70 -0
- package/docs/RELEASE-HEALTH.md +81 -0
- package/docs/SETUP.md +2 -0
- package/docs/TROUBLESHOOTING.md +6 -4
- package/docs/WEB-API.md +12 -1
- package/docs/release-health/2026-02-25.md +33 -0
- package/docs/release-health/2026-02-26.md +33 -0
- package/docs/release-health/24h-delta.md +21 -0
- package/docs/release-health/24h-end.md +33 -0
- package/docs/release-health/24h-start.md +33 -0
- package/docs/release-health/latest.md +33 -0
- package/docs/release-health/launch-log-template.md +21 -0
- package/docs/release-health/version-parity.md +21 -0
- package/lib/handlers.js +121 -85
- package/lib/slack-client.js +37 -17
- package/lib/token-store.js +103 -30
- package/package.json +26 -39
- package/public/demo-claude.html +4 -4
- package/public/demo-video.html +2 -2
- package/public/demo.html +2 -2
- package/scripts/build-release-health-delta.js +201 -0
- package/scripts/check-public-language.sh +25 -0
- package/scripts/check-version-parity.js +162 -0
- package/scripts/collect-release-health.js +150 -0
- package/scripts/setup-wizard.js +35 -9
- package/scripts/token-cli.js +6 -4
- package/scripts/verify-install-flow.js +107 -2
- package/src/server-http.js +26 -4
- package/src/server.js +23 -7
- 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
|
+
});
|
package/scripts/setup-wizard.js
CHANGED
|
@@ -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 = "
|
|
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(
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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 =
|
|
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
|
-
|
|
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");
|
package/scripts/token-cli.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Token CLI - Manage Slack tokens
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
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 =
|
|
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
|
-
|
|
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 {
|
package/src/server-http.js
CHANGED
|
@@ -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 = "
|
|
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: [{
|
|
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: [{
|
|
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({
|
|
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
|
|