@jtalk22/slack-mcp 1.0.6 → 1.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 +13 -1
- package/package.json +1 -1
- package/public/demo.html +715 -611
- package/public/index.html +128 -16
- package/scripts/verify-web.js +221 -0
- package/src/server.js +2 -2
- package/src/web-server.js +9 -6
package/public/index.html
CHANGED
|
@@ -127,16 +127,73 @@
|
|
|
127
127
|
cursor: pointer;
|
|
128
128
|
}
|
|
129
129
|
.tabs button.active { background: #4ecdc4; color: #1a1a2e; }
|
|
130
|
+
/* Auth Modal */
|
|
131
|
+
.modal-overlay {
|
|
132
|
+
position: fixed;
|
|
133
|
+
top: 0; left: 0; right: 0; bottom: 0;
|
|
134
|
+
background: rgba(0,0,0,0.85);
|
|
135
|
+
display: flex;
|
|
136
|
+
align-items: center;
|
|
137
|
+
justify-content: center;
|
|
138
|
+
z-index: 1000;
|
|
139
|
+
}
|
|
140
|
+
.modal-overlay.hidden { display: none; }
|
|
141
|
+
.modal {
|
|
142
|
+
background: #16213e;
|
|
143
|
+
padding: 40px;
|
|
144
|
+
border-radius: 12px;
|
|
145
|
+
max-width: 450px;
|
|
146
|
+
text-align: center;
|
|
147
|
+
border: 2px solid #4ecdc4;
|
|
148
|
+
}
|
|
149
|
+
.modal h2 { color: #4ecdc4; margin-bottom: 20px; }
|
|
150
|
+
.modal p { color: #aaa; margin-bottom: 25px; line-height: 1.6; }
|
|
151
|
+
.modal code {
|
|
152
|
+
background: #0f3460;
|
|
153
|
+
padding: 3px 8px;
|
|
154
|
+
border-radius: 4px;
|
|
155
|
+
color: #4ecdc4;
|
|
156
|
+
}
|
|
157
|
+
.modal input {
|
|
158
|
+
width: 100%;
|
|
159
|
+
padding: 14px;
|
|
160
|
+
border: 2px solid #0f3460;
|
|
161
|
+
border-radius: 6px;
|
|
162
|
+
background: #0f3460;
|
|
163
|
+
color: #fff;
|
|
164
|
+
font-size: 14px;
|
|
165
|
+
margin-bottom: 15px;
|
|
166
|
+
}
|
|
167
|
+
.modal input:focus { border-color: #4ecdc4; outline: none; }
|
|
168
|
+
.modal button {
|
|
169
|
+
width: 100%;
|
|
170
|
+
padding: 14px;
|
|
171
|
+
background: #4ecdc4;
|
|
172
|
+
color: #1a1a2e;
|
|
173
|
+
border: none;
|
|
174
|
+
border-radius: 6px;
|
|
175
|
+
cursor: pointer;
|
|
176
|
+
font-weight: bold;
|
|
177
|
+
font-size: 16px;
|
|
178
|
+
}
|
|
179
|
+
.modal button:hover { background: #45b7aa; }
|
|
180
|
+
.modal .error { color: #ff6b6b; margin-top: 10px; font-size: 14px; }
|
|
130
181
|
</style>
|
|
131
182
|
</head>
|
|
132
183
|
<body>
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
<div class="
|
|
136
|
-
<
|
|
137
|
-
<
|
|
138
|
-
<
|
|
184
|
+
<!-- Auth Modal -->
|
|
185
|
+
<div id="authModal" class="modal-overlay">
|
|
186
|
+
<div class="modal">
|
|
187
|
+
<h2>Enter API Key</h2>
|
|
188
|
+
<p>Copy the API key from the console where you ran<br><code>npm run web</code></p>
|
|
189
|
+
<input type="text" id="modalApiKey" placeholder="smcp_xxxxxxxxxxxx" autofocus>
|
|
190
|
+
<button onclick="submitApiKey()">Connect</button>
|
|
191
|
+
<div id="modalError" class="error"></div>
|
|
139
192
|
</div>
|
|
193
|
+
</div>
|
|
194
|
+
|
|
195
|
+
<div class="container">
|
|
196
|
+
<h1>Slack Web API <span id="status" class="status"></span></h1>
|
|
140
197
|
<div class="grid">
|
|
141
198
|
<div class="sidebar">
|
|
142
199
|
<h3>CONVERSATIONS</h3>
|
|
@@ -165,33 +222,88 @@
|
|
|
165
222
|
</div>
|
|
166
223
|
</div>
|
|
167
224
|
<script>
|
|
168
|
-
|
|
169
|
-
const DEFAULT_KEY = 'slack-mcp-local';
|
|
170
|
-
let apiKey = localStorage.getItem('slackApiKey') || DEFAULT_KEY;
|
|
225
|
+
let apiKey = null;
|
|
171
226
|
let currentChannel = null;
|
|
172
227
|
|
|
173
|
-
|
|
174
|
-
|
|
228
|
+
// Initialize: check URL param, then localStorage
|
|
229
|
+
(function init() {
|
|
230
|
+
const params = new URLSearchParams(window.location.search);
|
|
231
|
+
const keyFromUrl = params.get('key');
|
|
232
|
+
|
|
233
|
+
if (keyFromUrl) {
|
|
234
|
+
// Save key from magic link and strip from URL
|
|
235
|
+
apiKey = keyFromUrl;
|
|
236
|
+
localStorage.setItem('slackApiKey', apiKey);
|
|
237
|
+
history.replaceState({}, '', window.location.pathname);
|
|
238
|
+
} else {
|
|
239
|
+
apiKey = localStorage.getItem('slackApiKey');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (apiKey) {
|
|
243
|
+
hideModal();
|
|
244
|
+
connect();
|
|
245
|
+
} else {
|
|
246
|
+
showModal();
|
|
247
|
+
}
|
|
248
|
+
})();
|
|
249
|
+
|
|
250
|
+
function showModal(errorMsg) {
|
|
251
|
+
document.getElementById('authModal').classList.remove('hidden');
|
|
252
|
+
document.getElementById('modalError').textContent = errorMsg || '';
|
|
253
|
+
document.getElementById('modalApiKey').focus();
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function hideModal() {
|
|
257
|
+
document.getElementById('authModal').classList.add('hidden');
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function submitApiKey() {
|
|
261
|
+
const input = document.getElementById('modalApiKey').value.trim();
|
|
262
|
+
if (!input) {
|
|
263
|
+
document.getElementById('modalError').textContent = 'Please enter an API key';
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
apiKey = input;
|
|
267
|
+
localStorage.setItem('slackApiKey', apiKey);
|
|
268
|
+
hideModal();
|
|
269
|
+
connect();
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Handle Enter key in modal
|
|
273
|
+
document.getElementById('modalApiKey').addEventListener('keypress', (e) => {
|
|
274
|
+
if (e.key === 'Enter') submitApiKey();
|
|
275
|
+
});
|
|
276
|
+
|
|
175
277
|
async function api(endpoint, options = {}) {
|
|
176
278
|
const res = await fetch(endpoint, {
|
|
177
279
|
...options,
|
|
178
280
|
headers: { 'Authorization': 'Bearer ' + apiKey, 'Content-Type': 'application/json', ...options.headers }
|
|
179
281
|
});
|
|
282
|
+
if (res.status === 401 || res.status === 403) {
|
|
283
|
+
// Auth failed - clear stored key and show modal
|
|
284
|
+
localStorage.removeItem('slackApiKey');
|
|
285
|
+
apiKey = null;
|
|
286
|
+
showModal('Invalid or expired API key. Check console for current key.');
|
|
287
|
+
throw new Error('Authentication failed');
|
|
288
|
+
}
|
|
180
289
|
if (!res.ok) { const err = await res.json(); throw new Error(err.error || 'API error'); }
|
|
181
290
|
return res.json();
|
|
182
291
|
}
|
|
292
|
+
|
|
183
293
|
async function connect() {
|
|
184
|
-
apiKey = document.getElementById('apiKey').value.trim() || DEFAULT_KEY;
|
|
185
|
-
localStorage.setItem('slackApiKey', apiKey);
|
|
186
294
|
const status = document.getElementById('status');
|
|
295
|
+
status.textContent = 'Connecting...';
|
|
296
|
+
status.className = 'status';
|
|
187
297
|
try {
|
|
188
298
|
const health = await api('/health');
|
|
189
|
-
status.textContent = '
|
|
299
|
+
status.textContent = '● ' + health.user + ' @ ' + health.team;
|
|
190
300
|
status.className = 'status ok';
|
|
191
301
|
loadConversations('im,mpim');
|
|
192
302
|
} catch (e) {
|
|
193
|
-
|
|
194
|
-
|
|
303
|
+
if (e.message !== 'Authentication failed') {
|
|
304
|
+
status.textContent = 'Error: ' + e.message;
|
|
305
|
+
status.className = 'status error';
|
|
306
|
+
}
|
|
195
307
|
}
|
|
196
308
|
}
|
|
197
309
|
async function loadConversations(types) {
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Web UI Verification Script
|
|
4
|
+
*
|
|
5
|
+
* Tests:
|
|
6
|
+
* 1. Server starts and prints Magic Link
|
|
7
|
+
* 2. /demo.html contains "STATIC PREVIEW" banner
|
|
8
|
+
* 3. /?key=... serves the dashboard (index.html)
|
|
9
|
+
* 4. Server shuts down cleanly
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { spawn } from "child_process";
|
|
13
|
+
import { dirname, join } from "path";
|
|
14
|
+
import { fileURLToPath } from "url";
|
|
15
|
+
|
|
16
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
const SERVER_PATH = join(__dirname, "../src/web-server.js");
|
|
18
|
+
const PORT = 3456; // Use non-standard port to avoid conflicts
|
|
19
|
+
const TIMEOUT = 15000;
|
|
20
|
+
|
|
21
|
+
let serverProc = null;
|
|
22
|
+
|
|
23
|
+
function log(msg) {
|
|
24
|
+
console.log(` ${msg}`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function cleanup() {
|
|
28
|
+
if (serverProc) {
|
|
29
|
+
serverProc.kill("SIGTERM");
|
|
30
|
+
serverProc = null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
process.on("exit", cleanup);
|
|
35
|
+
process.on("SIGINT", () => { cleanup(); process.exit(1); });
|
|
36
|
+
process.on("SIGTERM", () => { cleanup(); process.exit(1); });
|
|
37
|
+
|
|
38
|
+
async function startServer() {
|
|
39
|
+
return new Promise((resolve, reject) => {
|
|
40
|
+
let magicLink = null;
|
|
41
|
+
let apiKey = null;
|
|
42
|
+
let output = "";
|
|
43
|
+
|
|
44
|
+
serverProc = spawn("node", [SERVER_PATH], {
|
|
45
|
+
env: { ...process.env, PORT: String(PORT) },
|
|
46
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const timeout = setTimeout(() => {
|
|
50
|
+
reject(new Error("Server startup timeout - no magic link detected"));
|
|
51
|
+
}, TIMEOUT);
|
|
52
|
+
|
|
53
|
+
serverProc.stderr.on("data", (data) => {
|
|
54
|
+
const text = data.toString();
|
|
55
|
+
output += text;
|
|
56
|
+
|
|
57
|
+
// Look for magic link pattern
|
|
58
|
+
const match = text.match(/Dashboard:\s*(http:\/\/[^\s]+)/);
|
|
59
|
+
if (match) {
|
|
60
|
+
magicLink = match[1];
|
|
61
|
+
// Extract key from URL
|
|
62
|
+
const keyMatch = magicLink.match(/[?&]key=([^&\s]+)/);
|
|
63
|
+
if (keyMatch) {
|
|
64
|
+
apiKey = keyMatch[1];
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Server is ready when we see the full banner
|
|
69
|
+
if (output.includes("Dashboard:") && output.includes("API Key:")) {
|
|
70
|
+
clearTimeout(timeout);
|
|
71
|
+
resolve({ magicLink, apiKey });
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
serverProc.on("error", (err) => {
|
|
76
|
+
clearTimeout(timeout);
|
|
77
|
+
reject(err);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
serverProc.on("exit", (code) => {
|
|
81
|
+
if (code !== null && code !== 0) {
|
|
82
|
+
clearTimeout(timeout);
|
|
83
|
+
reject(new Error(`Server exited with code ${code}`));
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function testDemoPage() {
|
|
90
|
+
const url = `http://localhost:${PORT}/demo.html`;
|
|
91
|
+
const res = await fetch(url);
|
|
92
|
+
|
|
93
|
+
if (!res.ok) {
|
|
94
|
+
throw new Error(`Failed to fetch demo.html: ${res.status}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const html = await res.text();
|
|
98
|
+
|
|
99
|
+
if (!html.includes("STATIC PREVIEW")) {
|
|
100
|
+
throw new Error("demo.html missing 'STATIC PREVIEW' banner");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!html.includes("Who is Alex?")) {
|
|
104
|
+
throw new Error("demo.html missing anonymized 'Who is Alex?' scenario");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function testDashboard(apiKey) {
|
|
111
|
+
const url = `http://localhost:${PORT}/?key=${apiKey}`;
|
|
112
|
+
const res = await fetch(url);
|
|
113
|
+
|
|
114
|
+
if (!res.ok) {
|
|
115
|
+
throw new Error(`Failed to fetch dashboard: ${res.status}`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const html = await res.text();
|
|
119
|
+
|
|
120
|
+
// Should serve index.html (the dashboard)
|
|
121
|
+
if (!html.includes("Slack Web API")) {
|
|
122
|
+
throw new Error("Dashboard page missing 'Slack Web API' title");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (!html.includes("authModal")) {
|
|
126
|
+
throw new Error("Dashboard missing auth modal");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function testApiWithKey(apiKey) {
|
|
133
|
+
// Test that API rejects bad key
|
|
134
|
+
const badRes = await fetch(`http://localhost:${PORT}/health`, {
|
|
135
|
+
headers: { "Authorization": "Bearer bad-key" }
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
if (badRes.status !== 401) {
|
|
139
|
+
throw new Error(`Expected 401 for bad key, got ${badRes.status}`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function main() {
|
|
146
|
+
console.log("╔════════════════════════════════════════╗");
|
|
147
|
+
console.log("║ Web UI Verification Tests ║");
|
|
148
|
+
console.log("╚════════════════════════════════════════╝");
|
|
149
|
+
|
|
150
|
+
const results = [];
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
// Test 1: Server starts with magic link
|
|
154
|
+
console.log("\n[TEST 1] Server Startup & Magic Link");
|
|
155
|
+
console.log("─".repeat(40));
|
|
156
|
+
|
|
157
|
+
const { magicLink, apiKey } = await startServer();
|
|
158
|
+
|
|
159
|
+
if (!magicLink) {
|
|
160
|
+
throw new Error("No magic link found");
|
|
161
|
+
}
|
|
162
|
+
if (!apiKey) {
|
|
163
|
+
throw new Error("No API key found in magic link");
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
log(`Magic Link: ${magicLink}`);
|
|
167
|
+
log(`API Key: ${apiKey.substring(0, 20)}...`);
|
|
168
|
+
log("PASS: Server started with magic link");
|
|
169
|
+
results.push(true);
|
|
170
|
+
|
|
171
|
+
// Test 2: Demo page
|
|
172
|
+
console.log("\n[TEST 2] Demo Page (/demo.html)");
|
|
173
|
+
console.log("─".repeat(40));
|
|
174
|
+
|
|
175
|
+
await testDemoPage();
|
|
176
|
+
log("PASS: Demo page serves correctly with STATIC PREVIEW banner");
|
|
177
|
+
results.push(true);
|
|
178
|
+
|
|
179
|
+
// Test 3: Dashboard
|
|
180
|
+
console.log("\n[TEST 3] Dashboard (/?key=...)");
|
|
181
|
+
console.log("─".repeat(40));
|
|
182
|
+
|
|
183
|
+
await testDashboard(apiKey);
|
|
184
|
+
log("PASS: Dashboard serves with auth modal");
|
|
185
|
+
results.push(true);
|
|
186
|
+
|
|
187
|
+
// Test 4: API auth
|
|
188
|
+
console.log("\n[TEST 4] API Authentication");
|
|
189
|
+
console.log("─".repeat(40));
|
|
190
|
+
|
|
191
|
+
await testApiWithKey(apiKey);
|
|
192
|
+
log("PASS: API correctly rejects bad keys");
|
|
193
|
+
results.push(true);
|
|
194
|
+
|
|
195
|
+
} catch (err) {
|
|
196
|
+
console.log(` FAIL: ${err.message}`);
|
|
197
|
+
results.push(false);
|
|
198
|
+
} finally {
|
|
199
|
+
cleanup();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Summary
|
|
203
|
+
console.log("\n" + "═".repeat(40));
|
|
204
|
+
const passed = results.filter(r => r).length;
|
|
205
|
+
const total = results.length;
|
|
206
|
+
|
|
207
|
+
if (passed === total) {
|
|
208
|
+
console.log(`\n✓ ALL TESTS PASSED (${passed}/${total})`);
|
|
209
|
+
console.log("\nReady to bump version to 1.1.0");
|
|
210
|
+
process.exit(0);
|
|
211
|
+
} else {
|
|
212
|
+
console.log(`\n✗ TESTS FAILED (${passed}/${total})`);
|
|
213
|
+
process.exit(1);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
main().catch(e => {
|
|
218
|
+
console.error("Test error:", e);
|
|
219
|
+
cleanup();
|
|
220
|
+
process.exit(1);
|
|
221
|
+
});
|
package/src/server.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* - Network error retry with exponential backoff
|
|
12
12
|
* - Background token health monitoring
|
|
13
13
|
*
|
|
14
|
-
* @version 1.0
|
|
14
|
+
* @version 1.1.0
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
@@ -43,7 +43,7 @@ const BACKGROUND_REFRESH_INTERVAL = 4 * 60 * 60 * 1000;
|
|
|
43
43
|
|
|
44
44
|
// Package info
|
|
45
45
|
const SERVER_NAME = "slack-mcp-server";
|
|
46
|
-
const SERVER_VERSION = "1.0
|
|
46
|
+
const SERVER_VERSION = "1.1.0";
|
|
47
47
|
|
|
48
48
|
// Initialize server
|
|
49
49
|
const server = new Server(
|
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 1.0
|
|
8
|
+
* @version 1.1.0
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import express from "express";
|
|
@@ -281,11 +281,14 @@ async function main() {
|
|
|
281
281
|
}
|
|
282
282
|
|
|
283
283
|
app.listen(PORT, () => {
|
|
284
|
-
|
|
285
|
-
console.
|
|
286
|
-
console.
|
|
287
|
-
console.
|
|
288
|
-
console.
|
|
284
|
+
// Print to stderr to keep logs clean (stdout reserved for JSON in some setups)
|
|
285
|
+
console.error(`\n${"═".repeat(60)}`);
|
|
286
|
+
console.error(` Slack Web API Server v1.1.0`);
|
|
287
|
+
console.error(`${"═".repeat(60)}`);
|
|
288
|
+
console.error(`\n Dashboard: http://localhost:${PORT}/?key=${API_KEY}`);
|
|
289
|
+
console.error(`\n API Key: ${API_KEY}`);
|
|
290
|
+
console.error(`\n curl -H "Authorization: Bearer ${API_KEY}" http://localhost:${PORT}/health`);
|
|
291
|
+
console.error(`\n${"═".repeat(60)}\n`);
|
|
289
292
|
});
|
|
290
293
|
}
|
|
291
294
|
|