@jtalk22/slack-mcp 1.1.9 → 1.2.1
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/LICENSE +1 -1
- package/README.md +71 -79
- package/docs/API.md +1 -1
- package/docs/SETUP.md +1 -1
- package/docs/assets/icon-512.png +0 -0
- package/docs/assets/icon.svg +27 -0
- package/docs/images/demo-claude-v1.2.gif +0 -0
- package/docs/images/demo-poster.png +0 -0
- package/docs/images/demo-readme.gif +0 -0
- package/docs/images/diagram-oauth-comparison.svg +80 -0
- package/docs/images/diagram-session-flow.svg +105 -0
- package/docs/videos/.gitkeep +0 -0
- package/docs/videos/demo-claude-v1.2.webm +0 -0
- package/lib/slack-client.js +35 -6
- package/lib/tools.js +69 -0
- package/package.json +10 -5
- package/public/demo-claude.html +1838 -0
- package/public/demo-video.html +151 -0
- package/scripts/record-demo.js +149 -0
- package/scripts/setup-wizard.js +363 -0
- package/src/cli.js +57 -0
- package/src/server-http.js +1 -1
- package/src/server.js +170 -3
- package/src/web-server.js +3 -3
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Slack MCP Server Demo</title>
|
|
7
|
+
<style>
|
|
8
|
+
* {
|
|
9
|
+
margin: 0;
|
|
10
|
+
padding: 0;
|
|
11
|
+
box-sizing: border-box;
|
|
12
|
+
}
|
|
13
|
+
body {
|
|
14
|
+
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
15
|
+
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
|
16
|
+
min-height: 100vh;
|
|
17
|
+
display: flex;
|
|
18
|
+
flex-direction: column;
|
|
19
|
+
align-items: center;
|
|
20
|
+
justify-content: center;
|
|
21
|
+
padding: 2rem;
|
|
22
|
+
}
|
|
23
|
+
.container {
|
|
24
|
+
max-width: 900px;
|
|
25
|
+
width: 100%;
|
|
26
|
+
}
|
|
27
|
+
h1 {
|
|
28
|
+
color: #ffffff;
|
|
29
|
+
font-size: 1.75rem;
|
|
30
|
+
font-weight: 600;
|
|
31
|
+
text-align: center;
|
|
32
|
+
margin-bottom: 0.5rem;
|
|
33
|
+
}
|
|
34
|
+
.subtitle {
|
|
35
|
+
color: #94a3b8;
|
|
36
|
+
text-align: center;
|
|
37
|
+
margin-bottom: 1.5rem;
|
|
38
|
+
font-size: 1rem;
|
|
39
|
+
}
|
|
40
|
+
.video-wrapper {
|
|
41
|
+
position: relative;
|
|
42
|
+
border-radius: 12px;
|
|
43
|
+
overflow: hidden;
|
|
44
|
+
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
|
|
45
|
+
background: #0f0f1a;
|
|
46
|
+
}
|
|
47
|
+
video {
|
|
48
|
+
width: 100%;
|
|
49
|
+
display: block;
|
|
50
|
+
border-radius: 12px;
|
|
51
|
+
}
|
|
52
|
+
.controls {
|
|
53
|
+
display: flex;
|
|
54
|
+
justify-content: center;
|
|
55
|
+
gap: 1rem;
|
|
56
|
+
margin-top: 1.5rem;
|
|
57
|
+
}
|
|
58
|
+
.btn {
|
|
59
|
+
padding: 0.75rem 1.5rem;
|
|
60
|
+
border-radius: 8px;
|
|
61
|
+
border: none;
|
|
62
|
+
font-size: 0.875rem;
|
|
63
|
+
font-weight: 500;
|
|
64
|
+
cursor: pointer;
|
|
65
|
+
transition: all 0.2s;
|
|
66
|
+
}
|
|
67
|
+
.btn-primary {
|
|
68
|
+
background: #4ecdc4;
|
|
69
|
+
color: #1a1a2e;
|
|
70
|
+
}
|
|
71
|
+
.btn-primary:hover {
|
|
72
|
+
background: #5eead4;
|
|
73
|
+
transform: translateY(-1px);
|
|
74
|
+
}
|
|
75
|
+
.btn-secondary {
|
|
76
|
+
background: rgba(255, 255, 255, 0.1);
|
|
77
|
+
color: #ffffff;
|
|
78
|
+
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
79
|
+
}
|
|
80
|
+
.btn-secondary:hover {
|
|
81
|
+
background: rgba(255, 255, 255, 0.15);
|
|
82
|
+
}
|
|
83
|
+
.back-link {
|
|
84
|
+
margin-top: 2rem;
|
|
85
|
+
text-align: center;
|
|
86
|
+
}
|
|
87
|
+
.back-link a {
|
|
88
|
+
color: #94a3b8;
|
|
89
|
+
text-decoration: none;
|
|
90
|
+
font-size: 0.875rem;
|
|
91
|
+
}
|
|
92
|
+
.back-link a:hover {
|
|
93
|
+
color: #ffffff;
|
|
94
|
+
}
|
|
95
|
+
</style>
|
|
96
|
+
</head>
|
|
97
|
+
<body>
|
|
98
|
+
<div class="container">
|
|
99
|
+
<h1>Slack MCP Server</h1>
|
|
100
|
+
<p class="subtitle">Full workspace access via local session mirroring</p>
|
|
101
|
+
|
|
102
|
+
<div class="video-wrapper">
|
|
103
|
+
<video id="demo" poster="../docs/images/demo-poster.png" playsinline>
|
|
104
|
+
<source src="../docs/videos/demo-claude-v1.2.webm" type="video/webm">
|
|
105
|
+
<source src="../docs/images/demo-claude-v1.2.gif" type="image/gif">
|
|
106
|
+
Your browser does not support the video tag.
|
|
107
|
+
</video>
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
<div class="controls">
|
|
111
|
+
<button class="btn btn-primary" onclick="togglePlay()">Play / Pause</button>
|
|
112
|
+
<button class="btn btn-secondary" onclick="restart()">Restart</button>
|
|
113
|
+
</div>
|
|
114
|
+
|
|
115
|
+
<div class="back-link">
|
|
116
|
+
<a href="https://github.com/jtalk22/slack-mcp-server">← Back to Repository</a>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
<script>
|
|
121
|
+
const video = document.getElementById('demo');
|
|
122
|
+
|
|
123
|
+
// Autoplay with 1 second delay
|
|
124
|
+
setTimeout(() => {
|
|
125
|
+
video.play().catch(() => {
|
|
126
|
+
// Autoplay blocked, user will need to click
|
|
127
|
+
console.log('Autoplay blocked, click to play');
|
|
128
|
+
});
|
|
129
|
+
}, 1000);
|
|
130
|
+
|
|
131
|
+
function togglePlay() {
|
|
132
|
+
if (video.paused) {
|
|
133
|
+
video.play();
|
|
134
|
+
} else {
|
|
135
|
+
video.pause();
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function restart() {
|
|
140
|
+
video.currentTime = 0;
|
|
141
|
+
video.play();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Loop the video
|
|
145
|
+
video.addEventListener('ended', () => {
|
|
146
|
+
video.currentTime = 0;
|
|
147
|
+
video.play();
|
|
148
|
+
});
|
|
149
|
+
</script>
|
|
150
|
+
</body>
|
|
151
|
+
</html>
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Demo video recording script using Playwright
|
|
4
|
+
* Records the Claude Desktop demo in fullscreen mode with auto-play
|
|
5
|
+
*
|
|
6
|
+
* Usage: npm run record-demo
|
|
7
|
+
* Output: docs/videos/demo-claude-TIMESTAMP.webm
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { chromium } from 'playwright';
|
|
11
|
+
import { fileURLToPath } from 'url';
|
|
12
|
+
import { dirname, join } from 'path';
|
|
13
|
+
import { mkdirSync, existsSync } from 'fs';
|
|
14
|
+
|
|
15
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
16
|
+
const __dirname = dirname(__filename);
|
|
17
|
+
const projectRoot = join(__dirname, '..');
|
|
18
|
+
|
|
19
|
+
// Configuration
|
|
20
|
+
const CONFIG = {
|
|
21
|
+
viewport: { width: 1280, height: 800 },
|
|
22
|
+
speed: '0.5', // Slow speed for video recording
|
|
23
|
+
scenarioCount: 5,
|
|
24
|
+
// Title and closing card durations (ms)
|
|
25
|
+
introDuration: 4000, // Title card (3s visible + fade)
|
|
26
|
+
outroDuration: 5500, // Closing card (4s visible + fades)
|
|
27
|
+
// Approximate duration per scenario at 0.5x speed (in ms)
|
|
28
|
+
scenarioDurations: {
|
|
29
|
+
search: 26000, // +1s for transition fade
|
|
30
|
+
thread: 26000,
|
|
31
|
+
list: 21000,
|
|
32
|
+
send: 19000,
|
|
33
|
+
multi: 36000
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
async function recordDemo() {
|
|
38
|
+
console.log('╔════════════════════════════════════════════════════════════╗');
|
|
39
|
+
console.log('║ Slack MCP Server - Demo Video Recording ║');
|
|
40
|
+
console.log('╚════════════════════════════════════════════════════════════╝');
|
|
41
|
+
console.log();
|
|
42
|
+
|
|
43
|
+
// Ensure videos directory exists
|
|
44
|
+
const videosDir = join(projectRoot, 'docs', 'videos');
|
|
45
|
+
if (!existsSync(videosDir)) {
|
|
46
|
+
mkdirSync(videosDir, { recursive: true });
|
|
47
|
+
console.log(`📁 Created directory: ${videosDir}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Generate timestamped filename
|
|
51
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
52
|
+
const videoFilename = `demo-claude-${timestamp}.webm`;
|
|
53
|
+
|
|
54
|
+
console.log('🚀 Launching browser...');
|
|
55
|
+
const browser = await chromium.launch({
|
|
56
|
+
headless: false, // Need visible browser for recording
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const context = await browser.newContext({
|
|
60
|
+
viewport: CONFIG.viewport,
|
|
61
|
+
recordVideo: {
|
|
62
|
+
dir: videosDir,
|
|
63
|
+
size: CONFIG.viewport
|
|
64
|
+
},
|
|
65
|
+
colorScheme: 'dark'
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const page = await context.newPage();
|
|
69
|
+
|
|
70
|
+
// Load the demo
|
|
71
|
+
const demoPath = join(projectRoot, 'public', 'demo-claude.html');
|
|
72
|
+
console.log(`📄 Loading: ${demoPath}`);
|
|
73
|
+
await page.goto(`file://${demoPath}`);
|
|
74
|
+
await page.waitForTimeout(1000);
|
|
75
|
+
|
|
76
|
+
// Hold on initial frame for a few seconds (visible first frame in GIF)
|
|
77
|
+
console.log('⏸️ Holding initial frame (3s)...');
|
|
78
|
+
await page.waitForTimeout(3000);
|
|
79
|
+
|
|
80
|
+
// Set slow speed for video recording
|
|
81
|
+
console.log(`⏱️ Setting speed to ${CONFIG.speed}x...`);
|
|
82
|
+
await page.selectOption('#speedSelect', CONFIG.speed);
|
|
83
|
+
await page.waitForTimeout(300);
|
|
84
|
+
|
|
85
|
+
// Start auto-play BEFORE entering fullscreen (button hidden in fullscreen)
|
|
86
|
+
console.log('▶️ Starting auto-play...');
|
|
87
|
+
await page.click('#autoPlayBtn');
|
|
88
|
+
await page.waitForTimeout(500);
|
|
89
|
+
|
|
90
|
+
// Now enter fullscreen mode
|
|
91
|
+
console.log('🖥️ Entering fullscreen mode...');
|
|
92
|
+
console.log();
|
|
93
|
+
await page.keyboard.press('f');
|
|
94
|
+
await page.waitForTimeout(500);
|
|
95
|
+
|
|
96
|
+
// Wait for title card
|
|
97
|
+
console.log('📺 Showing title card...');
|
|
98
|
+
await page.waitForTimeout(CONFIG.introDuration);
|
|
99
|
+
|
|
100
|
+
// Wait for each scenario
|
|
101
|
+
const scenarios = ['search', 'thread', 'list', 'send', 'multi'];
|
|
102
|
+
let totalWait = 0;
|
|
103
|
+
|
|
104
|
+
for (let i = 0; i < scenarios.length; i++) {
|
|
105
|
+
const scenario = scenarios[i];
|
|
106
|
+
const duration = CONFIG.scenarioDurations[scenario];
|
|
107
|
+
totalWait += duration;
|
|
108
|
+
|
|
109
|
+
console.log(` 📍 Scenario ${i + 1}/${scenarios.length}: ${scenario} (${Math.round(duration/1000)}s)`);
|
|
110
|
+
await page.waitForTimeout(duration);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Wait for closing card
|
|
114
|
+
console.log();
|
|
115
|
+
console.log('🎬 Showing closing screen...');
|
|
116
|
+
await page.waitForTimeout(CONFIG.outroDuration);
|
|
117
|
+
|
|
118
|
+
// Exit fullscreen
|
|
119
|
+
console.log('🔚 Exiting fullscreen...');
|
|
120
|
+
await page.keyboard.press('Escape');
|
|
121
|
+
await page.waitForTimeout(500);
|
|
122
|
+
|
|
123
|
+
// Close context to flush video
|
|
124
|
+
console.log('💾 Saving video...');
|
|
125
|
+
const video = await page.video();
|
|
126
|
+
await context.close();
|
|
127
|
+
await browser.close();
|
|
128
|
+
|
|
129
|
+
// Get the actual video path
|
|
130
|
+
const videoPath = await video.path();
|
|
131
|
+
|
|
132
|
+
console.log();
|
|
133
|
+
console.log('╔════════════════════════════════════════════════════════════╗');
|
|
134
|
+
console.log('║ ✅ Recording Complete! ║');
|
|
135
|
+
console.log('╚════════════════════════════════════════════════════════════╝');
|
|
136
|
+
console.log();
|
|
137
|
+
console.log(`📹 Video saved: ${videoPath}`);
|
|
138
|
+
console.log();
|
|
139
|
+
console.log('Next steps:');
|
|
140
|
+
console.log(' 1. Review the video in a media player');
|
|
141
|
+
console.log(' 2. Convert to GIF: npm run gif (requires gifski)');
|
|
142
|
+
console.log(' 3. Or with FFmpeg:');
|
|
143
|
+
console.log(` ffmpeg -i "${videoPath}" -vf "fps=15,scale=800:-1:flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse" docs/images/demo-claude.gif`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
recordDemo().catch(err => {
|
|
147
|
+
console.error('❌ Recording failed:', err.message);
|
|
148
|
+
process.exit(1);
|
|
149
|
+
});
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* slack-mcp-server Setup Wizard
|
|
4
|
+
*
|
|
5
|
+
* Interactive setup that:
|
|
6
|
+
* - Detects platform
|
|
7
|
+
* - Auto-extracts tokens on macOS
|
|
8
|
+
* - Guides manual entry on Linux/Windows
|
|
9
|
+
* - Validates tokens against Slack API
|
|
10
|
+
* - Saves to ~/.slack-mcp-tokens.json
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { platform } from "os";
|
|
14
|
+
import * as readline from "readline";
|
|
15
|
+
import { loadTokens, saveTokens, extractFromChrome, isAutoRefreshAvailable, TOKEN_FILE } from "../lib/token-store.js";
|
|
16
|
+
import { slackAPI } from "../lib/slack-client.js";
|
|
17
|
+
|
|
18
|
+
const IS_MACOS = platform() === 'darwin';
|
|
19
|
+
const VERSION = "1.2.1";
|
|
20
|
+
|
|
21
|
+
// ANSI colors
|
|
22
|
+
const colors = {
|
|
23
|
+
reset: '\x1b[0m',
|
|
24
|
+
bold: '\x1b[1m',
|
|
25
|
+
dim: '\x1b[2m',
|
|
26
|
+
green: '\x1b[32m',
|
|
27
|
+
yellow: '\x1b[33m',
|
|
28
|
+
blue: '\x1b[34m',
|
|
29
|
+
red: '\x1b[31m',
|
|
30
|
+
cyan: '\x1b[36m',
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
function print(msg = '') {
|
|
34
|
+
console.log(msg);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function printBox(lines, width = 60) {
|
|
38
|
+
const border = '─'.repeat(width);
|
|
39
|
+
print(`┌${border}┐`);
|
|
40
|
+
for (const line of lines) {
|
|
41
|
+
const padding = ' '.repeat(Math.max(0, width - line.length));
|
|
42
|
+
print(`│ ${line}${padding}│`);
|
|
43
|
+
}
|
|
44
|
+
print(`└${border}┘`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function success(msg) {
|
|
48
|
+
print(`${colors.green}✓${colors.reset} ${msg}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function warn(msg) {
|
|
52
|
+
print(`${colors.yellow}⚠${colors.reset} ${msg}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function error(msg) {
|
|
56
|
+
print(`${colors.red}✗${colors.reset} ${msg}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function info(msg) {
|
|
60
|
+
print(`${colors.blue}ℹ${colors.reset} ${msg}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function question(rl, prompt) {
|
|
64
|
+
return new Promise(resolve => rl.question(prompt, resolve));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function pressEnterToContinue(rl) {
|
|
68
|
+
await question(rl, `\n${colors.dim}[Press Enter to continue, Ctrl+C to cancel]${colors.reset}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function validateTokens(token, cookie) {
|
|
72
|
+
const hadOldToken = Object.prototype.hasOwnProperty.call(process.env, "SLACK_TOKEN");
|
|
73
|
+
const hadOldCookie = Object.prototype.hasOwnProperty.call(process.env, "SLACK_COOKIE");
|
|
74
|
+
const oldToken = process.env.SLACK_TOKEN;
|
|
75
|
+
const oldCookie = process.env.SLACK_COOKIE;
|
|
76
|
+
|
|
77
|
+
// Temporarily set env vars for validation
|
|
78
|
+
process.env.SLACK_TOKEN = token;
|
|
79
|
+
process.env.SLACK_COOKIE = cookie;
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
const result = await slackAPI("auth.test", {});
|
|
83
|
+
return { valid: true, user: result.user, team: result.team };
|
|
84
|
+
} catch (e) {
|
|
85
|
+
return { valid: false, error: e.message };
|
|
86
|
+
} finally {
|
|
87
|
+
// Always restore prior process env state
|
|
88
|
+
if (hadOldToken) process.env.SLACK_TOKEN = oldToken;
|
|
89
|
+
else delete process.env.SLACK_TOKEN;
|
|
90
|
+
|
|
91
|
+
if (hadOldCookie) process.env.SLACK_COOKIE = oldCookie;
|
|
92
|
+
else delete process.env.SLACK_COOKIE;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function runMacOSSetup(rl) {
|
|
97
|
+
print();
|
|
98
|
+
info("Detected platform: macOS");
|
|
99
|
+
info("Auto-extraction available via AppleScript");
|
|
100
|
+
print();
|
|
101
|
+
print("Requirements:");
|
|
102
|
+
print(" • Chrome browser installed");
|
|
103
|
+
print(" • Logged into Slack in a Chrome tab");
|
|
104
|
+
print(" • That Slack tab currently open");
|
|
105
|
+
|
|
106
|
+
await pressEnterToContinue(rl);
|
|
107
|
+
|
|
108
|
+
print();
|
|
109
|
+
print("Checking for Chrome...");
|
|
110
|
+
|
|
111
|
+
// Check if Chrome is running with a Slack tab
|
|
112
|
+
const tokens = extractFromChrome();
|
|
113
|
+
|
|
114
|
+
if (!tokens) {
|
|
115
|
+
print();
|
|
116
|
+
error("Could not extract tokens from Chrome.");
|
|
117
|
+
print();
|
|
118
|
+
print("Make sure:");
|
|
119
|
+
print(" 1. Chrome is running");
|
|
120
|
+
print(" 2. You have a Slack tab open (app.slack.com)");
|
|
121
|
+
print(" 3. You're logged into that workspace");
|
|
122
|
+
print();
|
|
123
|
+
|
|
124
|
+
const retry = await question(rl, "Try manual entry instead? (y/n): ");
|
|
125
|
+
if (retry.toLowerCase() === 'y') {
|
|
126
|
+
return await runManualSetup(rl);
|
|
127
|
+
}
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
success(`Extracted token: ${tokens.token.substring(0, 20)}...`);
|
|
132
|
+
success(`Extracted cookie: ${tokens.cookie.substring(0, 20)}...`);
|
|
133
|
+
|
|
134
|
+
print();
|
|
135
|
+
print("Validating against Slack API...");
|
|
136
|
+
|
|
137
|
+
const validation = await validateTokens(tokens.token, tokens.cookie);
|
|
138
|
+
|
|
139
|
+
if (!validation.valid) {
|
|
140
|
+
error(`Token validation failed: ${validation.error}`);
|
|
141
|
+
print();
|
|
142
|
+
print("Try refreshing your Slack tab and running again.");
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
success(`Workspace: ${validation.team}`);
|
|
147
|
+
success(`User: ${validation.user}`);
|
|
148
|
+
|
|
149
|
+
print();
|
|
150
|
+
print(`Writing to ${TOKEN_FILE}...`);
|
|
151
|
+
saveTokens(tokens.token, tokens.cookie);
|
|
152
|
+
success("Tokens saved with chmod 600");
|
|
153
|
+
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function runManualSetup(rl) {
|
|
158
|
+
print();
|
|
159
|
+
if (IS_MACOS) {
|
|
160
|
+
info("Switching to manual token entry...");
|
|
161
|
+
} else {
|
|
162
|
+
info(`Detected platform: ${platform()}`);
|
|
163
|
+
warn("Auto-extraction not available on this platform.");
|
|
164
|
+
}
|
|
165
|
+
print();
|
|
166
|
+
print("Follow these steps to extract tokens from Chrome:");
|
|
167
|
+
print();
|
|
168
|
+
print(`${colors.bold}Step 1:${colors.reset} Open Chrome and navigate to your Slack workspace`);
|
|
169
|
+
print(" https://app.slack.com");
|
|
170
|
+
print();
|
|
171
|
+
print(`${colors.bold}Step 2:${colors.reset} Press F12 to open DevTools`);
|
|
172
|
+
print();
|
|
173
|
+
print(`${colors.bold}Step 3:${colors.reset} Go to the ${colors.cyan}Console${colors.reset} tab and paste this:`);
|
|
174
|
+
print();
|
|
175
|
+
printBox([
|
|
176
|
+
"JSON.parse(localStorage.localConfig_v2).teams[",
|
|
177
|
+
" Object.keys(JSON.parse(localStorage.localConfig_v2)",
|
|
178
|
+
" .teams)[0]].token",
|
|
179
|
+
], 55);
|
|
180
|
+
print();
|
|
181
|
+
print(` Copy the token (starts with ${colors.cyan}xoxc-${colors.reset})`);
|
|
182
|
+
print();
|
|
183
|
+
|
|
184
|
+
const token = await question(rl, `${colors.bold}Paste your token:${colors.reset} `);
|
|
185
|
+
|
|
186
|
+
if (!token.startsWith('xoxc-')) {
|
|
187
|
+
error("Invalid token. Token should start with 'xoxc-'");
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
print();
|
|
192
|
+
print(`${colors.bold}Step 4:${colors.reset} Go to ${colors.cyan}Application${colors.reset} tab → ${colors.cyan}Cookies${colors.reset} → slack.com`);
|
|
193
|
+
print(` Find the '${colors.cyan}d${colors.reset}' cookie and copy its value`);
|
|
194
|
+
print();
|
|
195
|
+
|
|
196
|
+
const cookie = await question(rl, `${colors.bold}Paste your cookie:${colors.reset} `);
|
|
197
|
+
|
|
198
|
+
if (!cookie.startsWith('xoxd-')) {
|
|
199
|
+
error("Invalid cookie. Cookie should start with 'xoxd-'");
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
print();
|
|
204
|
+
print("Validating against Slack API...");
|
|
205
|
+
|
|
206
|
+
const validation = await validateTokens(token, cookie);
|
|
207
|
+
|
|
208
|
+
if (!validation.valid) {
|
|
209
|
+
error(`Token validation failed: ${validation.error}`);
|
|
210
|
+
print();
|
|
211
|
+
print("Common issues:");
|
|
212
|
+
print(" • Tokens expired - try refreshing Slack and copying again");
|
|
213
|
+
print(" • Wrong workspace - make sure you copied from the right tab");
|
|
214
|
+
print(" • Incomplete copy - ensure you got the full token/cookie");
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
success(`Workspace: ${validation.team}`);
|
|
219
|
+
success(`User: ${validation.user}`);
|
|
220
|
+
|
|
221
|
+
print();
|
|
222
|
+
print(`Writing to ${TOKEN_FILE}...`);
|
|
223
|
+
saveTokens(token, cookie);
|
|
224
|
+
success("Tokens saved with chmod 600");
|
|
225
|
+
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async function showStatus() {
|
|
230
|
+
const creds = loadTokens();
|
|
231
|
+
|
|
232
|
+
if (!creds) {
|
|
233
|
+
error("No tokens found");
|
|
234
|
+
print();
|
|
235
|
+
print("Run setup wizard: npx @jtalk22/slack-mcp --setup");
|
|
236
|
+
process.exit(1);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
print(`Token source: ${creds.source}`);
|
|
240
|
+
print(`Token file: ${TOKEN_FILE}`);
|
|
241
|
+
if (creds.updatedAt) {
|
|
242
|
+
print(`Last updated: ${creds.updatedAt}`);
|
|
243
|
+
}
|
|
244
|
+
print();
|
|
245
|
+
|
|
246
|
+
try {
|
|
247
|
+
// Need to set env vars for slackAPI to work
|
|
248
|
+
process.env.SLACK_TOKEN = creds.token;
|
|
249
|
+
process.env.SLACK_COOKIE = creds.cookie;
|
|
250
|
+
|
|
251
|
+
const result = await slackAPI("auth.test", {});
|
|
252
|
+
success("Status: VALID");
|
|
253
|
+
print(`User: ${result.user}`);
|
|
254
|
+
print(`Team: ${result.team}`);
|
|
255
|
+
print(`User ID: ${result.user_id}`);
|
|
256
|
+
} catch (e) {
|
|
257
|
+
error("Status: INVALID");
|
|
258
|
+
print(`Error: ${e.message}`);
|
|
259
|
+
print();
|
|
260
|
+
print("Run setup wizard to refresh: npx @jtalk22/slack-mcp --setup");
|
|
261
|
+
process.exit(1);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async function showHelp() {
|
|
266
|
+
print(`${colors.bold}slack-mcp-server v${VERSION}${colors.reset}`);
|
|
267
|
+
print();
|
|
268
|
+
print("Full Slack access for Claude via MCP. Session mirroring bypasses OAuth.");
|
|
269
|
+
print();
|
|
270
|
+
print(`${colors.bold}Usage:${colors.reset}`);
|
|
271
|
+
print(" npx @jtalk22/slack-mcp Start MCP server (stdio)");
|
|
272
|
+
print(" npx @jtalk22/slack-mcp --setup Interactive token setup wizard");
|
|
273
|
+
print(" npx @jtalk22/slack-mcp --status Check token health");
|
|
274
|
+
print(" npx @jtalk22/slack-mcp --version Print version");
|
|
275
|
+
print(" npx @jtalk22/slack-mcp --help Show this help");
|
|
276
|
+
print();
|
|
277
|
+
print(`${colors.bold}npm scripts:${colors.reset}`);
|
|
278
|
+
print(" npm start Start MCP server");
|
|
279
|
+
print(" npm run web Start REST API + Web UI (port 3000)");
|
|
280
|
+
print(" npm run tokens:auto Auto-extract from Chrome (macOS)");
|
|
281
|
+
print(" npm run tokens:status Check token health");
|
|
282
|
+
print();
|
|
283
|
+
print(`${colors.bold}More info:${colors.reset}`);
|
|
284
|
+
print(" https://github.com/jtalk22/slack-mcp-server");
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async function main() {
|
|
288
|
+
const args = process.argv.slice(2);
|
|
289
|
+
const command = args[0];
|
|
290
|
+
|
|
291
|
+
switch (command) {
|
|
292
|
+
case '--setup':
|
|
293
|
+
case 'setup':
|
|
294
|
+
break; // Continue to wizard
|
|
295
|
+
case '--status':
|
|
296
|
+
case 'status':
|
|
297
|
+
await showStatus();
|
|
298
|
+
return;
|
|
299
|
+
case '--version':
|
|
300
|
+
case '-v':
|
|
301
|
+
print(`slack-mcp-server v${VERSION}`);
|
|
302
|
+
return;
|
|
303
|
+
case '--help':
|
|
304
|
+
case '-h':
|
|
305
|
+
case 'help':
|
|
306
|
+
await showHelp();
|
|
307
|
+
return;
|
|
308
|
+
default:
|
|
309
|
+
if (command) {
|
|
310
|
+
error(`Unknown command: ${command}`);
|
|
311
|
+
print();
|
|
312
|
+
}
|
|
313
|
+
await showHelp();
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Run setup wizard
|
|
318
|
+
const rl = readline.createInterface({
|
|
319
|
+
input: process.stdin,
|
|
320
|
+
output: process.stdout
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
print();
|
|
324
|
+
printBox([
|
|
325
|
+
`🔧 slack-mcp-server Setup Wizard v${VERSION}`,
|
|
326
|
+
'',
|
|
327
|
+
'This wizard will extract your Slack session tokens',
|
|
328
|
+
'from Chrome and configure slack-mcp-server.',
|
|
329
|
+
'',
|
|
330
|
+
'Your tokens will be stored locally at:',
|
|
331
|
+
` ${TOKEN_FILE}`,
|
|
332
|
+
], 58);
|
|
333
|
+
|
|
334
|
+
try {
|
|
335
|
+
let success;
|
|
336
|
+
|
|
337
|
+
if (IS_MACOS && isAutoRefreshAvailable()) {
|
|
338
|
+
success = await runMacOSSetup(rl);
|
|
339
|
+
} else {
|
|
340
|
+
success = await runManualSetup(rl);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
print();
|
|
344
|
+
if (success) {
|
|
345
|
+
print(`${colors.green}${colors.bold}Setup complete!${colors.reset}`);
|
|
346
|
+
print();
|
|
347
|
+
print("Next steps:");
|
|
348
|
+
print(" • Verify: npx @jtalk22/slack-mcp --status");
|
|
349
|
+
print(" • Start server: npx @jtalk22/slack-mcp");
|
|
350
|
+
print(" • Or add to Claude Desktop config");
|
|
351
|
+
} else {
|
|
352
|
+
print(`${colors.red}Setup failed.${colors.reset} See errors above.`);
|
|
353
|
+
process.exit(1);
|
|
354
|
+
}
|
|
355
|
+
} finally {
|
|
356
|
+
rl.close();
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
main().catch(e => {
|
|
361
|
+
error(`Error: ${e.message}`);
|
|
362
|
+
process.exit(1);
|
|
363
|
+
});
|