@jtalk22/slack-mcp 2.0.0 → 3.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 +112 -64
- package/docs/CLOUDFLARE-BROWSER-TOOLKIT.md +67 -0
- package/docs/DEPLOYMENT-MODES.md +10 -3
- package/docs/HN-LAUNCH.md +47 -36
- package/docs/INDEX.md +4 -1
- package/docs/INSTALL-PROOF.md +5 -5
- package/docs/LAUNCH-COPY-v3.0.0.md +73 -0
- package/docs/LAUNCH-MATRIX.md +4 -2
- package/docs/LAUNCH-OPS.md +24 -23
- package/docs/RELEASE-HEALTH.md +9 -0
- package/docs/TROUBLESHOOTING.md +27 -0
- package/docs/WEB-API.md +13 -4
- package/docs/images/demo-channel-messages.png +0 -0
- package/docs/images/demo-channels.png +0 -0
- package/docs/images/demo-claude-mobile-360x800.png +0 -0
- package/docs/images/demo-claude-mobile-390x844.png +0 -0
- package/docs/images/demo-main-mobile-360x800.png +0 -0
- package/docs/images/demo-main-mobile-390x844.png +0 -0
- package/docs/images/demo-main.png +0 -0
- package/docs/images/demo-poster.png +0 -0
- package/docs/images/demo-sidebar.png +0 -0
- package/docs/images/web-api-mobile-360x800.png +0 -0
- package/docs/images/web-api-mobile-390x844.png +0 -0
- package/package.json +14 -6
- package/public/demo-claude.html +83 -10
- package/public/demo-video.html +33 -4
- package/public/demo.html +136 -2
- package/public/index.html +132 -69
- package/scripts/capture-screenshots.js +103 -53
- package/scripts/check-version-parity.js +25 -11
- package/scripts/cloudflare-browser-tool.js +237 -0
- package/scripts/collect-release-health.js +1 -1
- package/scripts/record-demo.js +22 -9
- package/scripts/release-preflight.js +243 -0
- package/scripts/setup-wizard.js +1 -1
- package/scripts/verify-install-flow.js +2 -1
- package/scripts/verify-web.js +49 -1
- package/server.json +47 -0
- package/smithery.yaml +34 -0
- package/src/server-http.js +98 -5
- package/src/server.js +18 -6
- package/src/web-server.js +5 -3
- package/docs/LAUNCH-COPY-v2.0.0.md +0 -59
- package/docs/images/demo-claude-v1.2.gif +0 -0
- package/docs/images/demo-readme.gif +0 -0
- package/docs/release-health/2026-02-25.md +0 -33
- package/docs/release-health/2026-02-26.md +0 -33
- package/docs/release-health/24h-delta.md +0 -21
- package/docs/release-health/24h-end.md +0 -33
- package/docs/release-health/24h-start.md +0 -33
- package/docs/release-health/latest.md +0 -33
- package/docs/release-health/launch-log-template.md +0 -21
- package/docs/release-health/version-parity.md +0 -21
- package/docs/videos/.gitkeep +0 -0
- package/docs/videos/demo-claude-v1.2.webm +0 -0
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawnSync } from "child_process";
|
|
4
|
+
import { mkdirSync, writeFileSync } from "fs";
|
|
5
|
+
import { dirname, resolve } from "path";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
7
|
+
|
|
8
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const ROOT = resolve(__dirname, "..");
|
|
10
|
+
const REPORT_PATH = resolve(ROOT, "docs", "release-health", "prepublish-dry-run.md");
|
|
11
|
+
|
|
12
|
+
const EXPECTED_NAME = process.env.EXPECTED_GIT_NAME || "jtalk22";
|
|
13
|
+
const EXPECTED_EMAIL = process.env.EXPECTED_GIT_EMAIL || "james@revasser.nyc";
|
|
14
|
+
const OWNER_RANGE = process.env.OWNER_CHECK_RANGE || "origin/main..HEAD";
|
|
15
|
+
|
|
16
|
+
function run(command, args = [], options = {}) {
|
|
17
|
+
return spawnSync(command, args, {
|
|
18
|
+
cwd: ROOT,
|
|
19
|
+
encoding: "utf8",
|
|
20
|
+
env: process.env,
|
|
21
|
+
maxBuffer: 20 * 1024 * 1024,
|
|
22
|
+
...options
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function trimOutput(text = "", maxChars = 1200) {
|
|
27
|
+
const normalized = String(text || "").trim();
|
|
28
|
+
if (!normalized) return "";
|
|
29
|
+
if (normalized.length <= maxChars) return normalized;
|
|
30
|
+
return `${normalized.slice(0, maxChars)}... [truncated]`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function stepResult(name, command, ok, details = "", commandOutput = "") {
|
|
34
|
+
return { name, command, ok, details, commandOutput };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function gitIdentityStep() {
|
|
38
|
+
const nameResult = run("git", ["config", "--get", "user.name"]);
|
|
39
|
+
const emailResult = run("git", ["config", "--get", "user.email"]);
|
|
40
|
+
|
|
41
|
+
const actualName = nameResult.stdout.trim();
|
|
42
|
+
const actualEmail = emailResult.stdout.trim();
|
|
43
|
+
const ok =
|
|
44
|
+
nameResult.status === 0 &&
|
|
45
|
+
emailResult.status === 0 &&
|
|
46
|
+
actualName === EXPECTED_NAME &&
|
|
47
|
+
actualEmail === EXPECTED_EMAIL;
|
|
48
|
+
|
|
49
|
+
const details = ok
|
|
50
|
+
? `Configured as ${actualName} <${actualEmail}>`
|
|
51
|
+
: `Expected ${EXPECTED_NAME} <${EXPECTED_EMAIL}>, found ${actualName || "(missing)"} <${actualEmail || "(missing)"}>`;
|
|
52
|
+
|
|
53
|
+
return stepResult(
|
|
54
|
+
"Git identity",
|
|
55
|
+
"git config --get user.name && git config --get user.email",
|
|
56
|
+
ok,
|
|
57
|
+
details,
|
|
58
|
+
`${nameResult.stdout}${nameResult.stderr}${emailResult.stdout}${emailResult.stderr}`
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function ownerAttributionStep() {
|
|
63
|
+
const result = run("bash", ["scripts/check-owner-attribution.sh", OWNER_RANGE]);
|
|
64
|
+
return stepResult(
|
|
65
|
+
"Owner attribution",
|
|
66
|
+
`bash scripts/check-owner-attribution.sh ${OWNER_RANGE}`,
|
|
67
|
+
result.status === 0,
|
|
68
|
+
result.status === 0 ? "All commits in range are owner-attributed." : "Owner attribution check failed.",
|
|
69
|
+
`${result.stdout}${result.stderr}`
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function publicLanguageStep() {
|
|
74
|
+
const result = run("bash", ["scripts/check-public-language.sh"]);
|
|
75
|
+
return stepResult(
|
|
76
|
+
"Public language",
|
|
77
|
+
"bash scripts/check-public-language.sh",
|
|
78
|
+
result.status === 0,
|
|
79
|
+
result.status === 0 ? "Public wording guardrail passed." : "Disallowed wording found.",
|
|
80
|
+
`${result.stdout}${result.stderr}`
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function markerScanStep() {
|
|
85
|
+
const pattern = "Co-authored-by|co-authored-by|Generated with|generated with";
|
|
86
|
+
const scanPaths = [
|
|
87
|
+
"README.md",
|
|
88
|
+
"docs",
|
|
89
|
+
"public",
|
|
90
|
+
".github/RELEASE_NOTES_TEMPLATE.md",
|
|
91
|
+
".github/ISSUE_REPLY_TEMPLATE.md"
|
|
92
|
+
];
|
|
93
|
+
const result = run("rg", [
|
|
94
|
+
"-n",
|
|
95
|
+
pattern,
|
|
96
|
+
"--glob",
|
|
97
|
+
"!docs/release-health/**",
|
|
98
|
+
...scanPaths
|
|
99
|
+
]);
|
|
100
|
+
|
|
101
|
+
if (result.status === 1) {
|
|
102
|
+
return stepResult(
|
|
103
|
+
"Public attribution markers",
|
|
104
|
+
"marker-scan",
|
|
105
|
+
true,
|
|
106
|
+
"No non-owner attribution markers found in public surfaces."
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return stepResult(
|
|
111
|
+
"Public attribution markers",
|
|
112
|
+
"marker-scan",
|
|
113
|
+
false,
|
|
114
|
+
result.status === 0
|
|
115
|
+
? "Found disallowed markers on public surfaces."
|
|
116
|
+
: "Marker scan failed.",
|
|
117
|
+
`${result.stdout}${result.stderr}`
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function runNodeStep(name, scriptPath, extraArgs = []) {
|
|
122
|
+
const result = run("node", [scriptPath, ...extraArgs]);
|
|
123
|
+
return stepResult(
|
|
124
|
+
name,
|
|
125
|
+
`node ${scriptPath}${extraArgs.length ? ` ${extraArgs.join(" ")}` : ""}`,
|
|
126
|
+
result.status === 0,
|
|
127
|
+
result.status === 0 ? "Passed." : "Failed.",
|
|
128
|
+
`${result.stdout}${result.stderr}`
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function npmPackSnapshot() {
|
|
133
|
+
const result = run("npm", ["pack", "--dry-run", "--json"]);
|
|
134
|
+
if (result.status !== 0) {
|
|
135
|
+
return {
|
|
136
|
+
ok: false,
|
|
137
|
+
details: "Unable to generate npm pack snapshot.",
|
|
138
|
+
output: `${result.stdout}${result.stderr}`
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
const parsed = JSON.parse(result.stdout);
|
|
144
|
+
const entry = Array.isArray(parsed) ? parsed[0] : parsed;
|
|
145
|
+
const fileCount = Array.isArray(entry.files) ? entry.files.length : 0;
|
|
146
|
+
const details = `package size ${entry.size} bytes, unpacked ${entry.unpackedSize} bytes, files ${fileCount}`;
|
|
147
|
+
return { ok: true, details, output: result.stdout };
|
|
148
|
+
} catch (error) {
|
|
149
|
+
return {
|
|
150
|
+
ok: false,
|
|
151
|
+
details: "npm pack output was not valid JSON.",
|
|
152
|
+
output: `${result.stdout}\n${String(error)}`
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function buildReport(results, packSnapshot) {
|
|
158
|
+
const generated = new Date().toISOString();
|
|
159
|
+
const failed = results.filter((step) => !step.ok);
|
|
160
|
+
const lines = [];
|
|
161
|
+
lines.push("# Prepublish Dry Run");
|
|
162
|
+
lines.push("");
|
|
163
|
+
lines.push(`- Generated: ${generated}`);
|
|
164
|
+
lines.push(`- Expected owner: \`${EXPECTED_NAME} <${EXPECTED_EMAIL}>\``);
|
|
165
|
+
lines.push(`- Owner range: \`${OWNER_RANGE}\``);
|
|
166
|
+
lines.push("");
|
|
167
|
+
lines.push("## Step Matrix");
|
|
168
|
+
lines.push("");
|
|
169
|
+
lines.push("| Step | Status | Command | Details |");
|
|
170
|
+
lines.push("|---|---|---|---|");
|
|
171
|
+
for (const step of results) {
|
|
172
|
+
lines.push(`| ${step.name} | ${step.ok ? "pass" : "fail"} | \`${step.command}\` | ${step.details} |`);
|
|
173
|
+
}
|
|
174
|
+
lines.push("");
|
|
175
|
+
lines.push("## npm Pack Snapshot");
|
|
176
|
+
lines.push("");
|
|
177
|
+
lines.push(`- Status: ${packSnapshot.ok ? "pass" : "fail"}`);
|
|
178
|
+
lines.push(`- Details: ${packSnapshot.details}`);
|
|
179
|
+
lines.push("");
|
|
180
|
+
|
|
181
|
+
if (failed.length === 0 && packSnapshot.ok) {
|
|
182
|
+
lines.push("## Result");
|
|
183
|
+
lines.push("");
|
|
184
|
+
lines.push("Prepublish dry run passed.");
|
|
185
|
+
} else {
|
|
186
|
+
lines.push("## Result");
|
|
187
|
+
lines.push("");
|
|
188
|
+
lines.push("Prepublish dry run failed.");
|
|
189
|
+
lines.push("");
|
|
190
|
+
lines.push("### Failing checks");
|
|
191
|
+
for (const step of failed) {
|
|
192
|
+
lines.push(`- ${step.name}`);
|
|
193
|
+
}
|
|
194
|
+
if (!packSnapshot.ok) lines.push("- npm pack snapshot");
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
lines.push("");
|
|
198
|
+
lines.push("## Command Output (Truncated)");
|
|
199
|
+
lines.push("");
|
|
200
|
+
for (const step of results) {
|
|
201
|
+
lines.push(`### ${step.name}`);
|
|
202
|
+
lines.push("");
|
|
203
|
+
lines.push("```text");
|
|
204
|
+
lines.push(trimOutput(step.commandOutput) || "(no output)");
|
|
205
|
+
lines.push("```");
|
|
206
|
+
lines.push("");
|
|
207
|
+
}
|
|
208
|
+
lines.push("### npm Pack Snapshot");
|
|
209
|
+
lines.push("");
|
|
210
|
+
lines.push("```text");
|
|
211
|
+
lines.push(trimOutput(packSnapshot.output, 4000) || "(no output)");
|
|
212
|
+
lines.push("```");
|
|
213
|
+
lines.push("");
|
|
214
|
+
|
|
215
|
+
return lines.join("\n");
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function main() {
|
|
219
|
+
const steps = [
|
|
220
|
+
gitIdentityStep(),
|
|
221
|
+
ownerAttributionStep(),
|
|
222
|
+
publicLanguageStep(),
|
|
223
|
+
markerScanStep(),
|
|
224
|
+
runNodeStep("Core verification", "scripts/verify-core.js"),
|
|
225
|
+
runNodeStep("Web verification", "scripts/verify-web.js"),
|
|
226
|
+
runNodeStep("Install-flow verification", "scripts/verify-install-flow.js"),
|
|
227
|
+
runNodeStep("Version parity", "scripts/check-version-parity.js", ["--allow-propagation"])
|
|
228
|
+
];
|
|
229
|
+
|
|
230
|
+
const packSnapshot = npmPackSnapshot();
|
|
231
|
+
const report = buildReport(steps, packSnapshot);
|
|
232
|
+
|
|
233
|
+
mkdirSync(resolve(ROOT, "docs", "release-health"), { recursive: true });
|
|
234
|
+
writeFileSync(REPORT_PATH, `${report}\n`);
|
|
235
|
+
|
|
236
|
+
console.log(`Wrote ${REPORT_PATH}`);
|
|
237
|
+
const failed = steps.some((step) => !step.ok) || !packSnapshot.ok;
|
|
238
|
+
if (failed) {
|
|
239
|
+
process.exitCode = 1;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
main();
|
package/scripts/setup-wizard.js
CHANGED
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
} from "../lib/token-store.js";
|
|
24
24
|
|
|
25
25
|
const IS_MACOS = platform() === 'darwin';
|
|
26
|
-
const VERSION = "
|
|
26
|
+
const VERSION = "3.0.0";
|
|
27
27
|
const MIN_NODE_MAJOR = 20;
|
|
28
28
|
const AUTH_TEST_URL = process.env.SLACK_MCP_AUTH_TEST_URL || "https://slack.com/api/auth.test";
|
|
29
29
|
|
|
@@ -9,10 +9,11 @@ import { fileURLToPath } from "node:url";
|
|
|
9
9
|
const PKG = "@jtalk22/slack-mcp";
|
|
10
10
|
const repoRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
11
11
|
const strictPublished = process.argv.includes("--strict-published");
|
|
12
|
+
const PUBLISHED_SPEC = `${PKG}@latest`;
|
|
12
13
|
const localVersion = JSON.parse(readFileSync(join(repoRoot, "package.json"), "utf8")).version;
|
|
13
14
|
|
|
14
15
|
function runNpx(args, options = {}) {
|
|
15
|
-
const cmdArgs = ["-y",
|
|
16
|
+
const cmdArgs = ["-y", PUBLISHED_SPEC, ...args];
|
|
16
17
|
const result = spawnSync("npx", cmdArgs, {
|
|
17
18
|
cwd: options.cwd,
|
|
18
19
|
env: options.env,
|
package/scripts/verify-web.js
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
* 1. Server starts and prints Magic Link
|
|
7
7
|
* 2. /demo.html contains "STATIC PREVIEW" banner
|
|
8
8
|
* 3. /?key=... serves the dashboard (index.html)
|
|
9
|
-
* 4.
|
|
9
|
+
* 4. /demo-video.html media assets are reachable
|
|
10
|
+
* 5. Server shuts down cleanly
|
|
10
11
|
*/
|
|
11
12
|
|
|
12
13
|
import { spawn } from "child_process";
|
|
@@ -142,6 +143,45 @@ async function testApiWithKey(apiKey) {
|
|
|
142
143
|
return true;
|
|
143
144
|
}
|
|
144
145
|
|
|
146
|
+
async function testDemoVideoAssets() {
|
|
147
|
+
const demoVideoUrl = `http://localhost:${PORT}/demo-video.html`;
|
|
148
|
+
const demoVideoRes = await fetch(demoVideoUrl);
|
|
149
|
+
|
|
150
|
+
if (!demoVideoRes.ok) {
|
|
151
|
+
throw new Error(`Failed to fetch demo-video.html: ${demoVideoRes.status}`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const demoVideoHtml = await demoVideoRes.text();
|
|
155
|
+
const requiredAssetCandidates = [
|
|
156
|
+
[
|
|
157
|
+
"/docs/images/demo-poster.png",
|
|
158
|
+
"https://jtalk22.github.io/slack-mcp-server/docs/images/demo-poster.png",
|
|
159
|
+
],
|
|
160
|
+
[
|
|
161
|
+
"/docs/videos/demo-claude.webm",
|
|
162
|
+
"https://jtalk22.github.io/slack-mcp-server/docs/videos/demo-claude.webm",
|
|
163
|
+
],
|
|
164
|
+
];
|
|
165
|
+
|
|
166
|
+
for (const candidates of requiredAssetCandidates) {
|
|
167
|
+
const matched = candidates.find((candidate) => demoVideoHtml.includes(candidate));
|
|
168
|
+
if (!matched) {
|
|
169
|
+
throw new Error(`demo-video.html missing expected media reference: ${candidates.join(" OR ")}`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const assetUrl = matched.startsWith("http")
|
|
173
|
+
? matched
|
|
174
|
+
: `http://localhost:${PORT}${matched}`;
|
|
175
|
+
|
|
176
|
+
const assetRes = await fetch(assetUrl);
|
|
177
|
+
if (!assetRes.ok) {
|
|
178
|
+
throw new Error(`Demo media not reachable: ${assetUrl} (status ${assetRes.status})`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
|
|
145
185
|
async function main() {
|
|
146
186
|
console.log("╔════════════════════════════════════════╗");
|
|
147
187
|
console.log("║ Web UI Verification Tests ║");
|
|
@@ -192,6 +232,14 @@ async function main() {
|
|
|
192
232
|
log("PASS: API correctly rejects bad keys");
|
|
193
233
|
results.push(true);
|
|
194
234
|
|
|
235
|
+
// Test 5: Demo video/media paths
|
|
236
|
+
console.log("\n[TEST 5] Demo Video Media Reachability");
|
|
237
|
+
console.log("─".repeat(40));
|
|
238
|
+
|
|
239
|
+
await testDemoVideoAssets();
|
|
240
|
+
log("PASS: demo-video media assets are reachable");
|
|
241
|
+
results.push(true);
|
|
242
|
+
|
|
195
243
|
} catch (err) {
|
|
196
244
|
console.log(` FAIL: ${err.message}`);
|
|
197
245
|
results.push(false);
|
package/server.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
|
+
"name": "io.github.jtalk22/slack-mcp-server",
|
|
4
|
+
"title": "Slack MCP Server",
|
|
5
|
+
"description": "Session-based Slack access for Claude: DMs, channels, search, and threads via your Slack session.",
|
|
6
|
+
"websiteUrl": "https://jtalk22.github.io/slack-mcp-server/public/demo.html",
|
|
7
|
+
"icons": [
|
|
8
|
+
{
|
|
9
|
+
"src": "https://raw.githubusercontent.com/jtalk22/slack-mcp-server/main/docs/assets/icon-512.png",
|
|
10
|
+
"mimeType": "image/png",
|
|
11
|
+
"sizes": [
|
|
12
|
+
"512x512"
|
|
13
|
+
]
|
|
14
|
+
}
|
|
15
|
+
],
|
|
16
|
+
"repository": {
|
|
17
|
+
"url": "https://github.com/jtalk22/slack-mcp-server",
|
|
18
|
+
"source": "github"
|
|
19
|
+
},
|
|
20
|
+
"version": "3.0.0",
|
|
21
|
+
"packages": [
|
|
22
|
+
{
|
|
23
|
+
"registryType": "npm",
|
|
24
|
+
"identifier": "@jtalk22/slack-mcp",
|
|
25
|
+
"version": "3.0.0",
|
|
26
|
+
"transport": {
|
|
27
|
+
"type": "stdio"
|
|
28
|
+
},
|
|
29
|
+
"environmentVariables": [
|
|
30
|
+
{
|
|
31
|
+
"description": "Slack xoxc- token from browser session",
|
|
32
|
+
"isRequired": false,
|
|
33
|
+
"format": "string",
|
|
34
|
+
"isSecret": true,
|
|
35
|
+
"name": "SLACK_TOKEN"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"description": "Slack xoxd- cookie from browser session",
|
|
39
|
+
"isRequired": false,
|
|
40
|
+
"format": "string",
|
|
41
|
+
"isSecret": true,
|
|
42
|
+
"name": "SLACK_COOKIE"
|
|
43
|
+
}
|
|
44
|
+
]
|
|
45
|
+
}
|
|
46
|
+
]
|
|
47
|
+
}
|
package/smithery.yaml
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Smithery configuration for slack-mcp-server
|
|
2
|
+
# https://smithery.ai/docs/build/project-config/smithery-yaml
|
|
3
|
+
|
|
4
|
+
startCommand:
|
|
5
|
+
type: stdio
|
|
6
|
+
configSchema:
|
|
7
|
+
type: object
|
|
8
|
+
properties:
|
|
9
|
+
slackToken:
|
|
10
|
+
type: string
|
|
11
|
+
title: "Slack Token"
|
|
12
|
+
description: "Your xoxc- token from browser session. Optional on macOS (auto-extracted from Chrome)."
|
|
13
|
+
pattern: "^xoxc-.*$"
|
|
14
|
+
slackCookie:
|
|
15
|
+
type: string
|
|
16
|
+
title: "Slack Cookie"
|
|
17
|
+
description: "Your xoxd- cookie from browser session. Optional on macOS (auto-extracted from Chrome)."
|
|
18
|
+
pattern: "^xoxd-.*$"
|
|
19
|
+
autoRefresh:
|
|
20
|
+
type: boolean
|
|
21
|
+
title: "Auto Refresh"
|
|
22
|
+
description: "Enable automatic token refresh from Chrome (macOS only)"
|
|
23
|
+
default: true
|
|
24
|
+
additionalProperties: false
|
|
25
|
+
commandFunction: |-
|
|
26
|
+
(config) => ({
|
|
27
|
+
command: 'node',
|
|
28
|
+
args: ['src/server.js'],
|
|
29
|
+
env: {
|
|
30
|
+
...(config.slackToken && { SLACK_TOKEN: config.slackToken }),
|
|
31
|
+
...(config.slackCookie && { SLACK_COOKIE: config.slackCookie }),
|
|
32
|
+
...(config.autoRefresh === false && { SLACK_NO_AUTO_REFRESH: 'true' })
|
|
33
|
+
}
|
|
34
|
+
})
|
package/src/server-http.js
CHANGED
|
@@ -30,8 +30,51 @@ 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 = "3.0.0";
|
|
34
34
|
const PORT = process.env.PORT || 3000;
|
|
35
|
+
const HTTP_INSECURE = process.env.SLACK_MCP_HTTP_INSECURE === "1";
|
|
36
|
+
const HTTP_AUTH_TOKEN = process.env.SLACK_MCP_HTTP_AUTH_TOKEN || process.env.SLACK_API_KEY || null;
|
|
37
|
+
const HTTP_ALLOWED_ORIGINS = new Set(
|
|
38
|
+
String(process.env.SLACK_MCP_HTTP_ALLOWED_ORIGINS || "")
|
|
39
|
+
.split(",")
|
|
40
|
+
.map(origin => origin.trim())
|
|
41
|
+
.filter(Boolean)
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
function structuredError(code, message, nextAction = null, details = null) {
|
|
45
|
+
const payload = {
|
|
46
|
+
status: "error",
|
|
47
|
+
code,
|
|
48
|
+
message,
|
|
49
|
+
next_action: nextAction
|
|
50
|
+
};
|
|
51
|
+
if (details) payload.details = details;
|
|
52
|
+
return payload;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function parseBearerToken(req) {
|
|
56
|
+
const auth = req.headers?.authorization;
|
|
57
|
+
if (!auth) return null;
|
|
58
|
+
const [scheme, token] = String(auth).split(" ");
|
|
59
|
+
if (scheme?.toLowerCase() !== "bearer" || !token) return null;
|
|
60
|
+
return token.trim();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function applyCors(req, res) {
|
|
64
|
+
const origin = String(req.headers?.origin || "");
|
|
65
|
+
const allowOrigin =
|
|
66
|
+
HTTP_INSECURE
|
|
67
|
+
? "*"
|
|
68
|
+
: (origin && HTTP_ALLOWED_ORIGINS.has(origin) ? origin : null);
|
|
69
|
+
|
|
70
|
+
if (allowOrigin) {
|
|
71
|
+
res.setHeader("Access-Control-Allow-Origin", allowOrigin);
|
|
72
|
+
if (allowOrigin !== "*") res.setHeader("Vary", "Origin");
|
|
73
|
+
}
|
|
74
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
75
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, mcp-session-id");
|
|
76
|
+
return allowOrigin;
|
|
77
|
+
}
|
|
35
78
|
|
|
36
79
|
// Create MCP server
|
|
37
80
|
const server = new Server(
|
|
@@ -112,12 +155,20 @@ await server.connect(transport);
|
|
|
112
155
|
|
|
113
156
|
// Create HTTP server
|
|
114
157
|
const httpServer = http.createServer(async (req, res) => {
|
|
115
|
-
|
|
116
|
-
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
117
|
-
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
118
|
-
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, mcp-session-id');
|
|
158
|
+
const allowOrigin = applyCors(req, res);
|
|
119
159
|
|
|
120
160
|
if (req.method === 'OPTIONS') {
|
|
161
|
+
if (!HTTP_INSECURE && req.headers?.origin && !allowOrigin) {
|
|
162
|
+
res.writeHead(403, { "Content-Type": "application/json" });
|
|
163
|
+
res.end(JSON.stringify(
|
|
164
|
+
structuredError(
|
|
165
|
+
"cors_origin_denied",
|
|
166
|
+
"CORS origin is not allowed.",
|
|
167
|
+
"Set SLACK_MCP_HTTP_ALLOWED_ORIGINS to a comma-separated allowlist."
|
|
168
|
+
)
|
|
169
|
+
));
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
121
172
|
res.writeHead(204);
|
|
122
173
|
res.end();
|
|
123
174
|
return;
|
|
@@ -138,6 +189,33 @@ const httpServer = http.createServer(async (req, res) => {
|
|
|
138
189
|
|
|
139
190
|
// MCP endpoint
|
|
140
191
|
if (req.url === '/mcp' || req.url === '/') {
|
|
192
|
+
if (!HTTP_INSECURE) {
|
|
193
|
+
if (!HTTP_AUTH_TOKEN) {
|
|
194
|
+
res.writeHead(503, { "Content-Type": "application/json" });
|
|
195
|
+
res.end(JSON.stringify(
|
|
196
|
+
structuredError(
|
|
197
|
+
"http_auth_token_missing",
|
|
198
|
+
"HTTP auth token is not configured for /mcp.",
|
|
199
|
+
"Set SLACK_MCP_HTTP_AUTH_TOKEN (or SLACK_API_KEY), or set SLACK_MCP_HTTP_INSECURE=1 for local testing only."
|
|
200
|
+
)
|
|
201
|
+
));
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const bearer = parseBearerToken(req);
|
|
206
|
+
if (bearer !== HTTP_AUTH_TOKEN) {
|
|
207
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
208
|
+
res.end(JSON.stringify(
|
|
209
|
+
structuredError(
|
|
210
|
+
"unauthorized",
|
|
211
|
+
"Bearer token is invalid for /mcp.",
|
|
212
|
+
"Provide Authorization: Bearer <SLACK_MCP_HTTP_AUTH_TOKEN>."
|
|
213
|
+
)
|
|
214
|
+
));
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
141
219
|
await transport.handleRequest(req, res);
|
|
142
220
|
return;
|
|
143
221
|
}
|
|
@@ -149,4 +227,19 @@ const httpServer = http.createServer(async (req, res) => {
|
|
|
149
227
|
httpServer.listen(PORT, () => {
|
|
150
228
|
console.log(`${SERVER_NAME} v${SERVER_VERSION} HTTP server running on port ${PORT}`);
|
|
151
229
|
console.log(`MCP endpoint: http://localhost:${PORT}/mcp`);
|
|
230
|
+
if (HTTP_INSECURE) {
|
|
231
|
+
console.warn("WARNING: SLACK_MCP_HTTP_INSECURE=1 enabled. /mcp is unauthenticated and CORS is wildcard.");
|
|
232
|
+
} else {
|
|
233
|
+
if (!HTTP_AUTH_TOKEN) {
|
|
234
|
+
console.warn("WARNING: SLACK_MCP_HTTP_AUTH_TOKEN is not set. /mcp will reject requests with 503.");
|
|
235
|
+
} else {
|
|
236
|
+
console.log("HTTP auth: bearer token required for /mcp");
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (HTTP_ALLOWED_ORIGINS.size > 0) {
|
|
240
|
+
console.log(`CORS allowlist: ${Array.from(HTTP_ALLOWED_ORIGINS).join(", ")}`);
|
|
241
|
+
} else {
|
|
242
|
+
console.log("CORS allowlist: none (browser cross-origin requests denied by default)");
|
|
243
|
+
}
|
|
244
|
+
}
|
|
152
245
|
});
|
package/src/server.js
CHANGED
|
@@ -11,11 +11,12 @@
|
|
|
11
11
|
* - Network error retry with exponential backoff
|
|
12
12
|
* - Background token health monitoring
|
|
13
13
|
*
|
|
14
|
-
* @version
|
|
14
|
+
* @version 3.0.0
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
18
18
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
19
|
+
import { pathToFileURL } from "node:url";
|
|
19
20
|
import {
|
|
20
21
|
CallToolRequestSchema,
|
|
21
22
|
ListToolsRequestSchema,
|
|
@@ -47,7 +48,7 @@ const BACKGROUND_REFRESH_INTERVAL = 4 * 60 * 60 * 1000;
|
|
|
47
48
|
|
|
48
49
|
// Package info
|
|
49
50
|
const SERVER_NAME = "slack-mcp-server";
|
|
50
|
-
const SERVER_VERSION = "
|
|
51
|
+
const SERVER_VERSION = "3.0.0";
|
|
51
52
|
|
|
52
53
|
// MCP Prompts - predefined prompt templates for common Slack operations
|
|
53
54
|
const PROMPTS = [
|
|
@@ -320,10 +321,21 @@ async function main() {
|
|
|
320
321
|
console.error(`${SERVER_NAME} v${SERVER_VERSION} running`);
|
|
321
322
|
}
|
|
322
323
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
324
|
+
function isDirectExecution() {
|
|
325
|
+
if (!process.argv[1]) return false;
|
|
326
|
+
try {
|
|
327
|
+
return import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
328
|
+
} catch {
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (isDirectExecution()) {
|
|
334
|
+
main().catch(error => {
|
|
335
|
+
console.error("Fatal error:", error);
|
|
336
|
+
process.exit(1);
|
|
337
|
+
});
|
|
338
|
+
}
|
|
327
339
|
|
|
328
340
|
/**
|
|
329
341
|
* Smithery sandbox server for capability scanning
|
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
|
|
8
|
+
* @version 3.0.0
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import express from "express";
|
|
@@ -66,6 +66,8 @@ const API_KEY = getOrCreateAPIKey();
|
|
|
66
66
|
// Middleware
|
|
67
67
|
app.use(express.json());
|
|
68
68
|
app.use(express.static(join(__dirname, "../public")));
|
|
69
|
+
// Keep /docs URL compatibility for demo media and documentation links.
|
|
70
|
+
app.use("/docs", express.static(join(__dirname, "../docs")));
|
|
69
71
|
|
|
70
72
|
// CORS - restricted to localhost for security
|
|
71
73
|
// Using * would allow any website to make requests to your local server
|
|
@@ -132,7 +134,7 @@ function extractContent(result) {
|
|
|
132
134
|
app.get("/", (req, res) => {
|
|
133
135
|
res.json({
|
|
134
136
|
name: "Slack Web API Server",
|
|
135
|
-
version: "
|
|
137
|
+
version: "3.0.0",
|
|
136
138
|
status: "ok",
|
|
137
139
|
code: "ok",
|
|
138
140
|
message: "Web API server is running.",
|
|
@@ -333,7 +335,7 @@ async function main() {
|
|
|
333
335
|
app.listen(PORT, '127.0.0.1', () => {
|
|
334
336
|
// Print to stderr to keep logs clean (stdout reserved for JSON in some setups)
|
|
335
337
|
console.error(`\n${"═".repeat(60)}`);
|
|
336
|
-
console.error(` Slack Web API Server
|
|
338
|
+
console.error(` Slack Web API Server v3.0.0`);
|
|
337
339
|
console.error(`${"═".repeat(60)}`);
|
|
338
340
|
console.error(`\n Dashboard: http://localhost:${PORT}/?key=${API_KEY}`);
|
|
339
341
|
console.error(`\n API Key: ${API_KEY}`);
|