@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.
- package/README.md +165 -57
- package/docs/SETUP.md +2 -34
- package/docs/TROUBLESHOOTING.md +2 -24
- package/lib/handlers.js +1 -1
- package/lib/public-metadata.js +0 -65
- package/lib/public-pages.js +35 -75
- package/lib/token-store.js +180 -69
- package/package.json +11 -7
- package/public/index.html +1 -1
- package/public/share.html +9 -18
- package/scripts/setup-wizard.js +74 -25
- package/server.json +3 -3
- package/src/server.js +1 -1
- package/src/web-server.js +18 -13
package/scripts/setup-wizard.js
CHANGED
|
@@ -131,10 +131,18 @@ async function runMacOSSetup(rl) {
|
|
|
131
131
|
}
|
|
132
132
|
print();
|
|
133
133
|
if (extractionError?.code === "apple_events_javascript_disabled") {
|
|
134
|
-
print(
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
208
|
+
printBox([oneLiner], oneLiner.length + 4);
|
|
196
209
|
print();
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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(
|
|
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
|
|
218
|
+
const input = await question(rl, `${colors.bold}Paste token:${colors.reset} `);
|
|
219
|
+
const trimmed = input.trim();
|
|
207
220
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
-
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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
|
});
|