@jtalk22/slack-mcp 3.2.5 → 4.1.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.
@@ -131,10 +131,18 @@ async function runMacOSSetup(rl) {
131
131
  }
132
132
  print();
133
133
  if (extractionError?.code === "apple_events_javascript_disabled") {
134
- print("Fix and retry:");
135
- print(" 1. In Chrome menu: View > Developer > Allow JavaScript from Apple Events");
136
- print(" 2. Keep Slack open in a Chrome tab (app.slack.com)");
137
- print(" 3. Re-run: npx -y @jtalk22/slack-mcp --setup");
134
+ print();
135
+ printBox([
136
+ "Chrome needs one setting enabled (one-time only):",
137
+ "",
138
+ "1. Open Chrome",
139
+ "2. Menu bar: View → Developer → Allow JavaScript",
140
+ " from Apple Events ✓",
141
+ "3. Run this command again",
142
+ ], 55);
143
+ print();
144
+ print("Once enabled, --setup extracts tokens automatically.");
145
+ print("No DevTools, no copy-paste, just one command.");
138
146
  } else {
139
147
  print("Make sure:");
140
148
  print(" 1. Chrome is running");
@@ -180,42 +188,83 @@ async function runManualSetup(rl) {
180
188
  print();
181
189
  if (IS_MACOS) {
182
190
  info("Switching to manual token entry...");
191
+ info("Note: On macOS, the session cookie can be extracted automatically.");
183
192
  } else {
184
193
  info(`Detected platform: ${platform()}`);
185
194
  warn("Auto-extraction not available on this platform.");
186
195
  }
196
+
197
+ const consoleHotkey = IS_MACOS ? "Cmd+Option+J" : "Ctrl+Shift+J";
198
+ // Token-only one-liner (cookie is HttpOnly and cannot be read via document.cookie)
199
+ const oneLiner = `copy(JSON.parse(localStorage.localConfig_v2).teams[Object.keys(JSON.parse(localStorage.localConfig_v2).teams)[0]].token)`;
200
+
187
201
  print();
188
- print("Follow these steps to extract tokens from Chrome:");
189
- print();
190
- print(`${colors.bold}Step 1:${colors.reset} Open Chrome and navigate to your Slack workspace`);
191
- print(" https://app.slack.com");
202
+ print(`${colors.bold}Quick extract (recommended):${colors.reset}`);
192
203
  print();
193
- print(`${colors.bold}Step 2:${colors.reset} Press F12 to open DevTools`);
204
+ print(` 1. Open Chrome → ${colors.cyan}app.slack.com${colors.reset} (must be logged in)`);
205
+ print(` 2. Press ${colors.cyan}${consoleHotkey}${colors.reset} to open the Console`);
206
+ print(` 3. Paste this one-liner and press Enter:`);
194
207
  print();
195
- print(`${colors.bold}Step 3:${colors.reset} Go to the ${colors.cyan}Console${colors.reset} tab and paste this:`);
208
+ printBox([oneLiner], oneLiner.length + 4);
196
209
  print();
197
- printBox([
198
- "JSON.parse(localStorage.localConfig_v2).teams[",
199
- " Object.keys(JSON.parse(localStorage.localConfig_v2)",
200
- " .teams)[0]].token",
201
- ], 55);
210
+ print(` 4. Your token is now on the clipboard. Paste below.`);
211
+ if (IS_MACOS) {
212
+ print(` ${colors.dim}(Cookie will be extracted automatically from Chrome)${colors.reset}`);
213
+ }
202
214
  print();
203
- print(` Copy the token (starts with ${colors.cyan}xoxc-${colors.reset})`);
215
+ print(`${colors.dim}(Or paste a JSON object with token+cookie, or a raw xoxc- token)${colors.reset}`);
204
216
  print();
205
217
 
206
- const token = await question(rl, `${colors.bold}Paste your token:${colors.reset} `);
218
+ const input = await question(rl, `${colors.bold}Paste token:${colors.reset} `);
219
+ const trimmed = input.trim();
207
220
 
208
- if (!token.startsWith('xoxc-')) {
209
- error("Invalid token. Token should start with 'xoxc-'");
210
- return false;
221
+ let token, cookie;
222
+
223
+ // Try JSON parse first (legacy one-liner output or manual JSON)
224
+ if (trimmed.startsWith('{')) {
225
+ try {
226
+ const parsed = JSON.parse(trimmed);
227
+ if (parsed.token) token = parsed.token;
228
+ if (parsed.cookie) cookie = parsed.cookie;
229
+ } catch (_) {
230
+ // Not valid JSON — fall through to raw token
231
+ }
211
232
  }
212
233
 
213
- print();
214
- print(`${colors.bold}Step 4:${colors.reset} Go to ${colors.cyan}Application${colors.reset} tab → ${colors.cyan}Cookies${colors.reset} → slack.com`);
215
- print(` Find the '${colors.cyan}d${colors.reset}' cookie and copy its value`);
216
- print();
234
+ // Treat as raw token
235
+ if (!token) {
236
+ token = trimmed;
237
+ if (!token.startsWith('xoxc-')) {
238
+ error("Invalid input. Expected a token starting with 'xoxc-'");
239
+ return false;
240
+ }
241
+ }
242
+
243
+ // On macOS, try to extract cookie from Chrome's cookie database automatically
244
+ if (!cookie && IS_MACOS) {
245
+ print();
246
+ print("Extracting session cookie from Chrome...");
247
+ const chromeTokens = extractFromChrome();
248
+ if (chromeTokens?.cookie) {
249
+ cookie = chromeTokens.cookie;
250
+ success("Cookie extracted from Chrome automatically");
251
+ } else {
252
+ warn("Could not extract cookie automatically.");
253
+ print(` Paste the cookie manually. In Chrome: ${colors.cyan}Application${colors.reset} tab → ${colors.cyan}Cookies${colors.reset} → find '${colors.cyan}d${colors.reset}'`);
254
+ print();
255
+ const cookieInput = await question(rl, `${colors.bold}Paste cookie (xoxd-...):${colors.reset} `);
256
+ cookie = cookieInput.trim();
257
+ }
258
+ }
217
259
 
218
- const cookie = await question(rl, `${colors.bold}Paste your cookie:${colors.reset} `);
260
+ // Non-macOS: always ask for cookie
261
+ if (!cookie) {
262
+ print();
263
+ print(`Paste the cookie. In Chrome: ${colors.cyan}Application${colors.reset} tab → ${colors.cyan}Cookies${colors.reset} → find '${colors.cyan}d${colors.reset}'`);
264
+ print();
265
+ const cookieInput = await question(rl, `${colors.bold}Paste cookie (xoxd-...):${colors.reset} `);
266
+ cookie = cookieInput.trim();
267
+ }
219
268
 
220
269
  if (!cookie.startsWith('xoxd-')) {
221
270
  error("Invalid cookie. Cookie should start with 'xoxd-'");
package/server.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
3
  "name": "io.github.jtalk22/slack-mcp-server",
4
4
  "title": "Slack MCP Server",
5
- "description": "Claude-first Slack MCP for self-host or managed Cloud, with Gemini CLI, deployment review, and secure-default HTTP.",
5
+ "description": "Slack MCP without OAuth no app registration, no admin approval. Works with Claude Code, Cursor, Copilot (where the official server doesn't). 16 tools, one command.",
6
6
  "websiteUrl": "https://mcp.revasserlabs.com",
7
7
  "icons": [
8
8
  {
@@ -17,7 +17,7 @@
17
17
  "url": "https://github.com/jtalk22/slack-mcp-server",
18
18
  "source": "github"
19
19
  },
20
- "version": "3.2.5",
20
+ "version": "4.1.0",
21
21
  "remotes": [
22
22
  {
23
23
  "type": "streamable-http",
@@ -28,7 +28,7 @@
28
28
  {
29
29
  "registryType": "npm",
30
30
  "identifier": "@jtalk22/slack-mcp",
31
- "version": "3.2.5",
31
+ "version": "4.1.0",
32
32
  "transport": {
33
33
  "type": "stdio"
34
34
  },
package/src/server.js CHANGED
@@ -150,7 +150,7 @@ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
150
150
  }
151
151
  case "summarize-channel": {
152
152
  const channelId = args?.channel_id || "";
153
- const days = parseInt(args?.days) || 7;
153
+ const days = parseInt(args?.days, 10) || 7;
154
154
  const since = Math.floor(Date.now() / 1000) - (days * 24 * 60 * 60);
155
155
  return {
156
156
  messages: [
package/src/web-server.js CHANGED
@@ -11,8 +11,7 @@ import express from "express";
11
11
  import { randomBytes } from "crypto";
12
12
  import { fileURLToPath } from "url";
13
13
  import { dirname, join } from "path";
14
- import { existsSync, readFileSync, writeFileSync } from "fs";
15
- import { execSync } from "child_process";
14
+ import { existsSync, readFileSync, writeFileSync, chmodSync } from "fs";
16
15
  import { homedir } from "os";
17
16
  import { loadTokensReadOnly } from "../lib/token-store.js";
18
17
  import { PUBLIC_METADATA, RELEASE_VERSION } from "../lib/public-metadata.js";
@@ -60,7 +59,7 @@ function getOrCreateAPIKey() {
60
59
  const newKey = `smcp_${randomBytes(24).toString('base64url')}`;
61
60
  try {
62
61
  writeFileSync(API_KEY_FILE, newKey);
63
- execSync(`chmod 600 "${API_KEY_FILE}"`);
62
+ chmodSync(API_KEY_FILE, 0o600);
64
63
  } catch {}
65
64
 
66
65
  return newKey;
@@ -98,6 +97,12 @@ app.use((req, res, next) => {
98
97
  next();
99
98
  });
100
99
 
100
+ function safeParseInt(value, fallback, max = 10000) {
101
+ const n = parseInt(value, 10);
102
+ if (isNaN(n) || n < 1) return fallback;
103
+ return Math.min(n, max);
104
+ }
105
+
101
106
  // API Key authentication
102
107
  function authenticate(req, res, next) {
103
108
  const authHeader = req.headers.authorization;
@@ -209,7 +214,7 @@ app.get("/conversations", authenticate, async (req, res) => {
209
214
  try {
210
215
  const result = await handleListConversations({
211
216
  types: req.query.types || "im,mpim",
212
- limit: parseInt(req.query.limit) || 100
217
+ limit: safeParseInt(req.query.limit, 100)
213
218
  });
214
219
  res.json(extractContent(result));
215
220
  } catch (e) {
@@ -222,7 +227,7 @@ app.get("/conversations/unreads", authenticate, async (req, res) => {
222
227
  try {
223
228
  const result = await handleConversationsUnreads({
224
229
  types: req.query.types || "im,mpim,public_channel,private_channel",
225
- limit: parseInt(req.query.limit) || 50
230
+ limit: safeParseInt(req.query.limit, 50)
226
231
  });
227
232
  res.json(extractContent(result));
228
233
  } catch (e) {
@@ -235,7 +240,7 @@ app.get("/conversations/:id/history", authenticate, async (req, res) => {
235
240
  try {
236
241
  const result = await handleConversationsHistory({
237
242
  channel_id: req.params.id,
238
- limit: parseInt(req.query.limit) || 50,
243
+ limit: safeParseInt(req.query.limit, 50),
239
244
  oldest: req.query.oldest,
240
245
  latest: req.query.latest,
241
246
  resolve_users: req.query.resolve_users !== "false"
@@ -253,7 +258,7 @@ app.get("/conversations/:id/full", authenticate, async (req, res) => {
253
258
  channel_id: req.params.id,
254
259
  oldest: req.query.oldest,
255
260
  latest: req.query.latest,
256
- max_messages: parseInt(req.query.max_messages) || 2000,
261
+ max_messages: safeParseInt(req.query.max_messages, 2000, 50000),
257
262
  include_threads: req.query.include_threads !== "false",
258
263
  output_file: req.query.output_file
259
264
  });
@@ -312,7 +317,7 @@ app.get("/search", authenticate, async (req, res) => {
312
317
  }
313
318
  const result = await handleSearchMessages({
314
319
  query: req.query.q,
315
- count: parseInt(req.query.count) || 20
320
+ count: safeParseInt(req.query.count, 20)
316
321
  });
317
322
  res.json(extractContent(result));
318
323
  } catch (e) {
@@ -347,7 +352,7 @@ app.post("/messages", authenticate, async (req, res) => {
347
352
  app.get("/users", authenticate, async (req, res) => {
348
353
  try {
349
354
  const result = await handleListUsers({
350
- limit: parseInt(req.query.limit) || 100
355
+ limit: safeParseInt(req.query.limit, 100)
351
356
  });
352
357
  res.json(extractContent(result));
353
358
  } catch (e) {
@@ -369,7 +374,7 @@ app.get("/users/search", authenticate, async (req, res) => {
369
374
  }
370
375
  const result = await handleUsersSearch({
371
376
  query: req.query.q,
372
- limit: parseInt(req.query.limit) || 20
377
+ limit: safeParseInt(req.query.limit, 20)
373
378
  });
374
379
  res.json(extractContent(result));
375
380
  } catch (e) {
@@ -452,9 +457,9 @@ async function main() {
452
457
  console.error(`\n${"═".repeat(60)}`);
453
458
  console.error(` Slack Web API Server v${RELEASE_VERSION}`);
454
459
  console.error(`${"═".repeat(60)}`);
455
- console.error(`\n Dashboard: http://localhost:${PORT}/?key=${API_KEY}`);
456
- console.error(`\n API Key: ${API_KEY}`);
457
- console.error(`\n curl -H "Authorization: Bearer ${API_KEY}" http://localhost:${PORT}/health`);
460
+ console.error(`\n Dashboard: http://localhost:${PORT}/?key=${API_KEY.slice(0, 8)}...`);
461
+ console.error(`\n API Key: ${API_KEY.slice(0, 8)}${"*".repeat(12)}`);
462
+ console.error(`\n curl -H "Authorization: Bearer <key>" http://localhost:${PORT}/health`);
458
463
  console.error(`\n Security: Bound to localhost only (127.0.0.1)`);
459
464
  console.error(`\n${"═".repeat(60)}\n`);
460
465
  });