@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/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
- <div class="container">
134
- <h1>Slack Web API</h1>
135
- <div class="auth-section">
136
- <input type="text" id="apiKey" placeholder="API Key (default: slack-mcp-local)">
137
- <button onclick="connect()">Connect</button>
138
- <span id="status" class="status"></span>
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
- // Default API key matches server default - auto-connects on first visit
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
- document.getElementById('apiKey').value = apiKey;
174
- setTimeout(connect, 100);
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 = 'Connected as ' + health.user + ' @ ' + health.team;
299
+ status.textContent = ' ' + health.user + ' @ ' + health.team;
190
300
  status.className = 'status ok';
191
301
  loadConversations('im,mpim');
192
302
  } catch (e) {
193
- status.textContent = 'Error: ' + e.message;
194
- status.className = 'status error';
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.6
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.6";
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.5
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
- console.log(`\n🚀 Slack Web API Server running at http://localhost:${PORT}`);
285
- console.log(`\n🔑 API Key: ${API_KEY}`);
286
- console.log(`\nExample usage:`);
287
- console.log(` curl -H "Authorization: Bearer ${API_KEY}" http://localhost:${PORT}/health`);
288
- console.log(` curl -H "Authorization: Bearer ${API_KEY}" http://localhost:${PORT}/conversations`);
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