@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.
Files changed (55) hide show
  1. package/README.md +112 -64
  2. package/docs/CLOUDFLARE-BROWSER-TOOLKIT.md +67 -0
  3. package/docs/DEPLOYMENT-MODES.md +10 -3
  4. package/docs/HN-LAUNCH.md +47 -36
  5. package/docs/INDEX.md +4 -1
  6. package/docs/INSTALL-PROOF.md +5 -5
  7. package/docs/LAUNCH-COPY-v3.0.0.md +73 -0
  8. package/docs/LAUNCH-MATRIX.md +4 -2
  9. package/docs/LAUNCH-OPS.md +24 -23
  10. package/docs/RELEASE-HEALTH.md +9 -0
  11. package/docs/TROUBLESHOOTING.md +27 -0
  12. package/docs/WEB-API.md +13 -4
  13. package/docs/images/demo-channel-messages.png +0 -0
  14. package/docs/images/demo-channels.png +0 -0
  15. package/docs/images/demo-claude-mobile-360x800.png +0 -0
  16. package/docs/images/demo-claude-mobile-390x844.png +0 -0
  17. package/docs/images/demo-main-mobile-360x800.png +0 -0
  18. package/docs/images/demo-main-mobile-390x844.png +0 -0
  19. package/docs/images/demo-main.png +0 -0
  20. package/docs/images/demo-poster.png +0 -0
  21. package/docs/images/demo-sidebar.png +0 -0
  22. package/docs/images/web-api-mobile-360x800.png +0 -0
  23. package/docs/images/web-api-mobile-390x844.png +0 -0
  24. package/package.json +14 -6
  25. package/public/demo-claude.html +83 -10
  26. package/public/demo-video.html +33 -4
  27. package/public/demo.html +136 -2
  28. package/public/index.html +132 -69
  29. package/scripts/capture-screenshots.js +103 -53
  30. package/scripts/check-version-parity.js +25 -11
  31. package/scripts/cloudflare-browser-tool.js +237 -0
  32. package/scripts/collect-release-health.js +1 -1
  33. package/scripts/record-demo.js +22 -9
  34. package/scripts/release-preflight.js +243 -0
  35. package/scripts/setup-wizard.js +1 -1
  36. package/scripts/verify-install-flow.js +2 -1
  37. package/scripts/verify-web.js +49 -1
  38. package/server.json +47 -0
  39. package/smithery.yaml +34 -0
  40. package/src/server-http.js +98 -5
  41. package/src/server.js +18 -6
  42. package/src/web-server.js +5 -3
  43. package/docs/LAUNCH-COPY-v2.0.0.md +0 -59
  44. package/docs/images/demo-claude-v1.2.gif +0 -0
  45. package/docs/images/demo-readme.gif +0 -0
  46. package/docs/release-health/2026-02-25.md +0 -33
  47. package/docs/release-health/2026-02-26.md +0 -33
  48. package/docs/release-health/24h-delta.md +0 -21
  49. package/docs/release-health/24h-end.md +0 -33
  50. package/docs/release-health/24h-start.md +0 -33
  51. package/docs/release-health/latest.md +0 -33
  52. package/docs/release-health/launch-log-template.md +0 -21
  53. package/docs/release-health/version-parity.md +0 -21
  54. package/docs/videos/.gitkeep +0 -0
  55. 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();
@@ -23,7 +23,7 @@ import {
23
23
  } from "../lib/token-store.js";
24
24
 
25
25
  const IS_MACOS = platform() === 'darwin';
26
- const VERSION = "2.0.0";
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", PKG, ...args];
16
+ const cmdArgs = ["-y", PUBLISHED_SPEC, ...args];
16
17
  const result = spawnSync("npx", cmdArgs, {
17
18
  cwd: options.cwd,
18
19
  env: options.env,
@@ -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. Server shuts down cleanly
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
+ })
@@ -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 = "2.0.0";
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
- // CORS headers
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 2.0.0
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 = "2.0.0";
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
- main().catch(error => {
324
- console.error("Fatal error:", error);
325
- process.exit(1);
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 2.0.0
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: "2.0.0",
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 v2.0.0`);
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}`);