@jtalk22/slack-mcp 4.3.0 → 4.4.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 CHANGED
@@ -20,7 +20,7 @@ npx -y @jtalk22/slack-mcp --setup
20
20
 
21
21
  ## Why This Exists
22
22
 
23
- Slack's official MCP server requires a registered app, admin approval, and [doesn't work with Claude Code or GitHub Copilot](https://github.com/anthropics/claude-code/issues/30564) due to OAuth/DCR incompatibility. Screenshotting messages is not a workflow.
23
+ Slack's official MCP server is OAuth-first and can require a registered app, admin approval, or client compatibility workarounds. See the tracked [Claude Code/GitHub Copilot compatibility discussion](https://github.com/anthropics/claude-code/issues/30564). Screenshotting messages is not a workflow.
24
24
 
25
25
  This server uses your browser's session tokens instead. If you can see it in Slack, your AI agent can see it too. No app install, no scopes, no admin.
26
26
 
@@ -137,10 +137,10 @@ Or via CLI: `codex mcp add slack -- npx -y @jtalk22/slack-mcp`
137
137
  | `slack_token_status` | Token age, health, and cache stats | read-only |
138
138
  | `slack_refresh_tokens` | Auto-extract fresh tokens from Chrome | read-only* |
139
139
  | `slack_list_conversations` | List DMs and channels | read-only |
140
- | `slack_conversations_history` | Get messages from a channel or DM | read-only |
141
- | `slack_get_full_conversation` | Export full history with threads | read-only |
142
- | `slack_search_messages` | Search across workspace | read-only |
143
- | `slack_get_thread` | Get thread replies | read-only |
140
+ | `slack_conversations_history` | Get messages from a channel or DM | read-only |
141
+ | `slack_get_full_conversation` | Export full history with threads | read-only |
142
+ | `slack_search_messages` | Search across workspace | read-only |
143
+ | `slack_get_thread` | Get thread replies | read-only |
144
144
  | `slack_users_info` | Get user details | read-only |
145
145
  | `slack_list_users` | List workspace users (paginated, 500+) | read-only |
146
146
  | `slack_users_search` | Search users by name, display name, or email | read-only |
@@ -161,6 +161,8 @@ Or via CLI: `codex mcp add slack -- npx -y @jtalk22/slack-mcp`
161
161
 
162
162
  † Hosted stubs return a structured upgrade payload (`signup_url`, `free_tier_quota`, `pro_value_prop`) — no Slack write occurs from OSS. Activate the brain at [mcp.revasserlabs.com](https://mcp.revasserlabs.com) (free tier, no card).
163
163
 
164
+ ‡ Also accepts `include_rich_message_fields` to return attachments, blocks, files, reactions, and metadata — see [Rich Message Fields](#rich-message-fields).
165
+
164
166
  ## Install
165
167
 
166
168
  **Node.js 20+**
@@ -171,6 +173,8 @@ npx -y @jtalk22/slack-mcp --setup
171
173
 
172
174
  The setup wizard handles token extraction and validation.
173
175
 
176
+ After setup, have your client run `slack_health_check` — a workspace name in the response confirms you are connected.
177
+
174
178
  <details>
175
179
  <summary><strong>Claude Desktop (macOS)</strong></summary>
176
180
 
@@ -230,6 +234,8 @@ Add to `~/.claude.json`:
230
234
  }
231
235
  ```
232
236
 
237
+ Or via CLI: `claude mcp add slack -- npx -y @jtalk22/slack-mcp`
238
+
233
239
  </details>
234
240
 
235
241
  <details>
@@ -318,6 +324,35 @@ Full release notes on [GitHub releases/latest](https://github.com/jtalk22/slack-
318
324
 
319
325
  </details>
320
326
 
327
+ ## Rich Message Fields
328
+
329
+ Added in 4.4.0. The four read tools marked ‡ above accept `include_rich_message_fields: true`, which surfaces the parts of a message that live outside `text` — `attachments`, `blocks`, `files`, `reactions`, `metadata`, plus `subtype`/`bot_id`/`app_id` (automated/bot/app markers) and `team` (workspace id).
330
+
331
+ An attachment-only alert reads as empty without the flag:
332
+
333
+ ```json
334
+ { "ts": "1767368030.607599", "user": "incident-bot", "text": "" }
335
+ ```
336
+
337
+ With `include_rich_message_fields: true`, the content is surfaced:
338
+
339
+ ```json
340
+ {
341
+ "ts": "1767368030.607599",
342
+ "user": "incident-bot",
343
+ "text": "",
344
+ "subtype": "bot_message",
345
+ "bot_id": "B012345",
346
+ "attachments": [{ "title": "PagerDuty", "text": "P1 — API latency > 2s" }]
347
+ }
348
+ ```
349
+
350
+ Output shape only — no extra permissions. `blocks` can be large, so it is opt-in per call to keep client context lean. For the full developer payload inside `metadata`, also set `include_all_metadata: true` (an independent Slack flag).
351
+
352
+ `slack_search_messages` accepts the flag, but Slack's search API does not return rich fields on matches — read full content with `slack_conversations_history` or `slack_get_thread` on the match's channel and timestamp.
353
+
354
+ Patch by [@rvandam](https://github.com/rvandam) (#143).
355
+
321
356
  ## Hosted HTTP Mode
322
357
 
323
358
  For remote MCP endpoints (Cloudflare Worker, VPS, etc.):
@@ -346,6 +381,7 @@ More: [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md)
346
381
 
347
382
  - [Setup Guide](docs/SETUP.md)
348
383
  - [API Reference](docs/API.md)
384
+ - [Roadmap](docs/ROADMAP.md)
349
385
  - [Architecture](docs/ARCHITECTURE.md)
350
386
  - [Deployment Modes](docs/DEPLOYMENT-MODES.md)
351
387
  - [Use Case Recipes](docs/USE_CASE_RECIPES.md)
package/docs/API.md CHANGED
@@ -119,9 +119,11 @@ Get messages from a channel or DM.
119
119
  |------|------|---------|-------------|
120
120
  | channel_id | string | *required* | Channel or DM ID |
121
121
  | limit | number | 50 | Messages to fetch (max 100) |
122
- | oldest | string | - | Unix timestamp, get messages after |
123
- | latest | string | - | Unix timestamp, get messages before |
122
+ | oldest | string | - | Unix timestamp, get messages after; matching boundary timestamp is included |
123
+ | latest | string | - | Unix timestamp, get messages before; matching boundary timestamp is included |
124
124
  | resolve_users | boolean | true | Convert user IDs to names |
125
+ | include_rich_message_fields | boolean | false | Include Slack attachments, blocks, metadata, files, and reactions when present |
126
+ | include_all_metadata | boolean | false | Pass Slack's `include_all_metadata` option to `conversations.history` |
125
127
 
126
128
  **Returns:**
127
129
  ```json
@@ -136,12 +138,32 @@ Get messages from a channel or DM.
136
138
  "user_id": "U05GPEVH7J9",
137
139
  "text": "Hello!",
138
140
  "datetime": "2026-01-02T15:33:50.000Z",
139
- "has_thread": false
141
+ "has_thread": false,
142
+ "attachments": [
143
+ {
144
+ "text": "Additional message context"
145
+ }
146
+ ]
140
147
  }
141
148
  ]
142
149
  }
143
150
  ```
144
151
 
152
+ `include_rich_message_fields` changes this tool's **output shape only** — it surfaces fields Slack
153
+ already returns on the message object: `attachments`, `blocks`, `files`, `reactions`, `metadata`,
154
+ plus `subtype`, `bot_id`, `app_id` (markers that flag automated / bot / app messages) and `team`
155
+ (the workspace id, present on all messages). Especially `blocks` can be large, so it's opt-in per
156
+ call to keep MCP client context lean.
157
+
158
+ `include_all_metadata` is a **separate, independent** Slack request flag: its only effect is to add
159
+ the full `event_payload` inside a message's developer `metadata` (without it you still get
160
+ `metadata.event_type`). The two are orthogonal — set `include_rich_message_fields` to see `metadata`
161
+ in the output at all; additionally set `include_all_metadata` for the full payload.
162
+
163
+ Note: `slack_search_messages` matches are thin — Slack's search API does not return these rich
164
+ fields on matches (only `team`). To read rich content for a search hit, call
165
+ `slack_conversations_history` or `slack_get_thread` on the match's channel/ts.
166
+
145
167
  ---
146
168
 
147
169
  ### slack_get_full_conversation
@@ -152,10 +174,12 @@ Export full conversation with threads.
152
174
  | Name | Type | Default | Description |
153
175
  |------|------|---------|-------------|
154
176
  | channel_id | string | *required* | Channel or DM ID |
155
- | oldest | string | - | Unix timestamp start |
156
- | latest | string | - | Unix timestamp end |
177
+ | oldest | string | - | Unix timestamp start; matching boundary timestamp is included |
178
+ | latest | string | - | Unix timestamp end; matching boundary timestamp is included |
157
179
  | max_messages | number | 2000 | Max messages (up to 10000) |
158
180
  | include_threads | boolean | true | Fetch thread replies |
181
+ | include_rich_message_fields | boolean | false | Include Slack attachments, blocks, metadata, files, and reactions when present |
182
+ | include_all_metadata | boolean | false | Pass Slack's `include_all_metadata` option to `conversations.history` and `conversations.replies` |
159
183
  | output_file | string | - | Filename (saved to ~/.slack-mcp-exports/) |
160
184
 
161
185
  **Timestamps:**
@@ -188,6 +212,7 @@ Search messages across the workspace.
188
212
  |------|------|---------|-------------|
189
213
  | query | string | *required* | Search query |
190
214
  | count | number | 20 | Number of results (max 100) |
215
+ | include_rich_message_fields | boolean | false | Include Slack attachments, blocks, metadata, files, and reactions when present |
191
216
 
192
217
  **Query Syntax:**
193
218
  - `from:@username` - From specific user
@@ -249,6 +274,8 @@ Get all replies in a thread.
249
274
  |------|------|---------|-------------|
250
275
  | channel_id | string | *required* | Channel or DM ID |
251
276
  | thread_ts | string | *required* | Thread parent timestamp |
277
+ | include_rich_message_fields | boolean | false | Include Slack attachments, blocks, metadata, files, and reactions when present |
278
+ | include_all_metadata | boolean | false | Pass Slack's `include_all_metadata` option to `conversations.replies` |
252
279
 
253
280
  **Returns:**
254
281
  ```json
package/lib/handlers.js CHANGED
@@ -20,6 +20,7 @@ import {
20
20
  listProfiles as workflowListProfiles,
21
21
  ALLOWED_WORKFLOW_KINDS_LIST,
22
22
  } from "./workflow-store.js";
23
+ import { withRichMessageFields } from "./rich-message-fields.js";
23
24
 
24
25
  // ============ Utilities ============
25
26
 
@@ -334,17 +335,19 @@ export async function handleListConversations(args) {
334
335
  */
335
336
  export async function handleConversationsHistory(args) {
336
337
  const resolveUsers = args.resolve_users !== false;
338
+ const includeRichMessageFields = parseBool(args.include_rich_message_fields);
337
339
  const result = await slackAPI("conversations.history", {
338
340
  channel: args.channel_id,
339
341
  limit: args.limit || 50,
340
342
  oldest: args.oldest,
341
343
  latest: args.latest,
342
- inclusive: true
344
+ inclusive: true,
345
+ include_all_metadata: parseBool(args.include_all_metadata)
343
346
  });
344
347
 
345
348
  const messages = await Promise.all((result.messages || []).map(async (msg) => {
346
349
  const userName = resolveUsers ? await resolveUser(msg.user) : msg.user;
347
- return {
350
+ return withRichMessageFields({
348
351
  ts: msg.ts,
349
352
  user: userName,
350
353
  user_id: msg.user,
@@ -352,7 +355,7 @@ export async function handleConversationsHistory(args) {
352
355
  datetime: formatTimestamp(msg.ts),
353
356
  has_thread: !!msg.thread_ts && msg.reply_count > 0,
354
357
  reply_count: msg.reply_count
355
- };
358
+ }, msg, includeRichMessageFields);
356
359
  }));
357
360
 
358
361
  return {
@@ -374,6 +377,7 @@ export async function handleConversationsHistory(args) {
374
377
  export async function handleGetFullConversation(args) {
375
378
  const maxMessages = Math.min(args.max_messages || 2000, 10000);
376
379
  const includeThreads = args.include_threads !== false;
380
+ const includeRichMessageFields = parseBool(args.include_rich_message_fields);
377
381
  const allMessages = [];
378
382
  let cursor;
379
383
  let hasMore = true;
@@ -386,36 +390,38 @@ export async function handleGetFullConversation(args) {
386
390
  oldest: args.oldest,
387
391
  latest: args.latest,
388
392
  cursor,
389
- inclusive: true
393
+ inclusive: true,
394
+ include_all_metadata: parseBool(args.include_all_metadata)
390
395
  });
391
396
 
392
397
  for (const msg of result.messages || []) {
393
398
  const userName = await resolveUser(msg.user);
394
- const message = {
399
+ const message = withRichMessageFields({
395
400
  ts: msg.ts,
396
401
  user: userName,
397
402
  user_id: msg.user,
398
403
  text: msg.text || "",
399
404
  datetime: formatTimestamp(msg.ts),
400
405
  replies: []
401
- };
406
+ }, msg, includeRichMessageFields);
402
407
 
403
408
  // Fetch thread replies if present
404
409
  if (includeThreads && msg.reply_count > 0) {
405
410
  try {
406
411
  const threadResult = await slackAPI("conversations.replies", {
407
412
  channel: args.channel_id,
408
- ts: msg.ts
413
+ ts: msg.ts,
414
+ include_all_metadata: parseBool(args.include_all_metadata)
409
415
  });
410
416
  // Skip first message (parent)
411
417
  for (const reply of (threadResult.messages || []).slice(1)) {
412
418
  const replyUserName = await resolveUser(reply.user);
413
- message.replies.push({
419
+ message.replies.push(withRichMessageFields({
414
420
  ts: reply.ts,
415
421
  user: replyUserName,
416
422
  text: reply.text || "",
417
423
  datetime: formatTimestamp(reply.ts)
418
- });
424
+ }, reply, includeRichMessageFields));
419
425
  }
420
426
  await sleep(50); // Rate limit
421
427
  } catch (e) {
@@ -470,6 +476,7 @@ export async function handleGetFullConversation(args) {
470
476
  * Search messages handler
471
477
  */
472
478
  export async function handleSearchMessages(args) {
479
+ const includeRichMessageFields = parseBool(args.include_rich_message_fields);
473
480
  const result = await slackAPI("search.messages", {
474
481
  query: args.query,
475
482
  count: args.count || 20,
@@ -477,7 +484,7 @@ export async function handleSearchMessages(args) {
477
484
  sort_dir: "desc"
478
485
  });
479
486
 
480
- const matches = await Promise.all((result.messages?.matches || []).map(async (m) => ({
487
+ const matches = await Promise.all((result.messages?.matches || []).map(async (m) => withRichMessageFields({
481
488
  ts: m.ts,
482
489
  channel: m.channel?.name || m.channel?.id,
483
490
  channel_id: m.channel?.id,
@@ -485,7 +492,7 @@ export async function handleSearchMessages(args) {
485
492
  text: m.text,
486
493
  datetime: formatTimestamp(m.ts),
487
494
  permalink: m.permalink
488
- })));
495
+ }, m, includeRichMessageFields)));
489
496
 
490
497
  return {
491
498
  content: [{
@@ -553,19 +560,21 @@ export async function handleSendMessage(args) {
553
560
  * Get thread handler
554
561
  */
555
562
  export async function handleGetThread(args) {
563
+ const includeRichMessageFields = parseBool(args.include_rich_message_fields);
556
564
  const result = await slackAPI("conversations.replies", {
557
565
  channel: args.channel_id,
558
- ts: args.thread_ts
566
+ ts: args.thread_ts,
567
+ include_all_metadata: parseBool(args.include_all_metadata)
559
568
  });
560
569
 
561
- const messages = await Promise.all((result.messages || []).map(async (msg) => ({
570
+ const messages = await Promise.all((result.messages || []).map(async (msg) => withRichMessageFields({
562
571
  ts: msg.ts,
563
572
  user: await resolveUser(msg.user),
564
573
  user_id: msg.user,
565
574
  text: msg.text || "",
566
575
  datetime: formatTimestamp(msg.ts),
567
576
  is_parent: msg.ts === args.thread_ts
568
- })));
577
+ }, msg, includeRichMessageFields)));
569
578
 
570
579
  return {
571
580
  content: [{
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Rich Slack message fields (opt-in).
3
+ *
4
+ * Slack stores a lot of message content outside the plain `text` field:
5
+ * attachments, Block Kit `blocks`, `metadata`, `files`, and `reactions` — plus
6
+ * `subtype`/`bot_id`/`app_id` (which flag bot / app messages) and `team` (the
7
+ * workspace id, present on every message). An attachment-only or block-only alert
8
+ * looks empty when you read `text` alone; surfacing these fields closes that blind spot.
9
+ *
10
+ * These are fields Slack already returns on the message object, so this is an
11
+ * output-shape opt-in only — independent of Slack's `include_all_metadata` request
12
+ * flag (which separately adds the `event_payload` inside `metadata`). They can be
13
+ * large (especially `blocks`), so callers opt in per request.
14
+ */
15
+
16
+ export const RICH_MESSAGE_KEYS = [
17
+ "attachments", "blocks", "metadata", "files", "reactions",
18
+ "subtype", "bot_id", "app_id", "team"
19
+ ];
20
+
21
+ /**
22
+ * Merge present rich fields from a raw Slack message onto a formatted output.
23
+ *
24
+ * Mutates and returns `output`. No-op when disabled or `msg` is falsy. Only keys
25
+ * actually present on `msg` are copied; `undefined` keys are skipped (a present
26
+ * falsy value such as `[]` is still copied).
27
+ */
28
+ export function withRichMessageFields(output, msg, includeRichMessageFields) {
29
+ if (!includeRichMessageFields || !msg) return output;
30
+
31
+ for (const key of RICH_MESSAGE_KEYS) {
32
+ if (msg[key] !== undefined) {
33
+ output[key] = msg[key];
34
+ }
35
+ }
36
+
37
+ return output;
38
+ }
package/lib/tools.js CHANGED
@@ -93,15 +93,23 @@ export const TOOLS = [
93
93
  },
94
94
  oldest: {
95
95
  type: "string",
96
- description: "Unix timestamp - get messages after this time"
96
+ description: "Unix timestamp - get messages after this time (boundary timestamp included)"
97
97
  },
98
98
  latest: {
99
99
  type: "string",
100
- description: "Unix timestamp - get messages before this time"
100
+ description: "Unix timestamp - get messages before this time (boundary timestamp included)"
101
101
  },
102
102
  resolve_users: {
103
103
  type: "boolean",
104
104
  description: "Convert user IDs to names (default true)"
105
+ },
106
+ include_rich_message_fields: {
107
+ type: "boolean",
108
+ description: "Include Slack message attachments, blocks, metadata, files, and reactions when present"
109
+ },
110
+ include_all_metadata: {
111
+ type: "boolean",
112
+ description: "Pass Slack's include_all_metadata option to conversations.history"
105
113
  }
106
114
  },
107
115
  required: ["channel_id"]
@@ -125,11 +133,11 @@ export const TOOLS = [
125
133
  },
126
134
  oldest: {
127
135
  type: "string",
128
- description: "Unix timestamp start (e.g., 1733011200 = Dec 1, 2025)"
136
+ description: "Unix timestamp start (e.g., 1733011200 = Dec 1, 2025; boundary timestamp included)"
129
137
  },
130
138
  latest: {
131
139
  type: "string",
132
- description: "Unix timestamp end"
140
+ description: "Unix timestamp end (boundary timestamp included)"
133
141
  },
134
142
  max_messages: {
135
143
  type: "number",
@@ -139,6 +147,14 @@ export const TOOLS = [
139
147
  type: "boolean",
140
148
  description: "Fetch thread replies (default true)"
141
149
  },
150
+ include_rich_message_fields: {
151
+ type: "boolean",
152
+ description: "Include Slack message attachments, blocks, metadata, files, and reactions when present"
153
+ },
154
+ include_all_metadata: {
155
+ type: "boolean",
156
+ description: "Pass Slack's include_all_metadata option to conversations.history and conversations.replies"
157
+ },
142
158
  output_file: {
143
159
  type: "string",
144
160
  description: "Filename to save export (saved to ~/.slack-mcp-exports/)"
@@ -166,6 +182,10 @@ export const TOOLS = [
166
182
  count: {
167
183
  type: "number",
168
184
  description: "Number of results (max 100, default 20)"
185
+ },
186
+ include_rich_message_fields: {
187
+ type: "boolean",
188
+ description: "Include Slack message attachments, blocks, metadata, files, and reactions when present"
169
189
  }
170
190
  },
171
191
  required: ["query"]
@@ -239,6 +259,14 @@ export const TOOLS = [
239
259
  thread_ts: {
240
260
  type: "string",
241
261
  description: "Thread parent message timestamp"
262
+ },
263
+ include_rich_message_fields: {
264
+ type: "boolean",
265
+ description: "Include Slack message attachments, blocks, metadata, files, and reactions when present"
266
+ },
267
+ include_all_metadata: {
268
+ type: "boolean",
269
+ description: "Pass Slack's include_all_metadata option to conversations.replies"
242
270
  }
243
271
  },
244
272
  required: ["channel_id", "thread_ts"]
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@jtalk22/slack-mcp",
3
3
  "mcpName": "io.github.jtalk22/slack-mcp-server",
4
- "version": "4.3.0",
4
+ "version": "4.4.0",
5
5
  "description": "Slack MCP without OAuth. 21 tools (16 read/write Slack + 2 workflow profile primitives + 3 hosted-brain upgrade stubs). Free OSS or hosted (free tier no card; $9/mo Pro = unlimited; morning DM rolling out Q2 2026).",
6
6
  "type": "module",
7
7
  "main": "src/server.js",
@@ -13,6 +13,7 @@
13
13
  "slack-mcp-setup": "scripts/setup-wizard.js"
14
14
  },
15
15
  "scripts": {
16
+ "test": "node --test",
16
17
  "start": "node src/server.js",
17
18
  "http": "node src/server-http.js",
18
19
  "web": "node src/web-server.js",
package/public/index.html CHANGED
@@ -190,6 +190,36 @@
190
190
  margin-bottom: 15px;
191
191
  }
192
192
  .modal input:focus { border-color: var(--teal); outline: none; }
193
+ .auth-options {
194
+ display: flex;
195
+ align-items: center;
196
+ gap: 8px;
197
+ margin: -4px 0 16px;
198
+ color: var(--muted);
199
+ font-size: 13px;
200
+ text-align: left;
201
+ }
202
+ .auth-options input { width: auto; margin: 0; }
203
+ .header-row {
204
+ display: flex;
205
+ justify-content: space-between;
206
+ align-items: flex-start;
207
+ gap: 12px;
208
+ margin-bottom: 20px;
209
+ }
210
+ .header-row h1 { margin-bottom: 0; }
211
+ .disconnect-btn {
212
+ display: none;
213
+ padding: 8px 12px;
214
+ background: transparent;
215
+ color: var(--muted);
216
+ border: 1px solid rgba(255, 255, 255, 0.18);
217
+ border-radius: 999px;
218
+ cursor: pointer;
219
+ white-space: nowrap;
220
+ }
221
+ .disconnect-btn.visible { display: inline-block; }
222
+ .disconnect-btn:hover { color: var(--text); border-color: rgba(255, 255, 255, 0.32); }
193
223
  .modal button {
194
224
  width: 100%;
195
225
  padding: 14px;
@@ -213,6 +243,10 @@
213
243
 
214
244
  @media (max-width: 640px) {
215
245
  body { padding: 12px; }
246
+ .header-row {
247
+ flex-direction: column;
248
+ align-items: stretch;
249
+ }
216
250
  h1 {
217
251
  display: flex;
218
252
  flex-direction: column;
@@ -247,14 +281,18 @@
247
281
  <div class="modal">
248
282
  <h2>Enter API Key</h2>
249
283
  <p>Copy the API key from the console where you ran<br><code>npm run web</code></p>
250
- <input type="text" id="modalApiKey" placeholder="smcp_xxxxxxxxxxxx" autofocus>
284
+ <input type="text" id="modalApiKey" placeholder="smcp_xxxxxxxxxxxx" autocomplete="off" autofocus>
285
+ <label class="auth-options"><input type="checkbox" id="rememberApiKey"> Remember this key on this device</label>
251
286
  <button onclick="submitApiKey()">Connect</button>
252
287
  <div id="modalError" class="error"></div>
253
288
  </div>
254
289
  </div>
255
290
 
256
291
  <div class="container">
257
- <h1>Slack Web API <span id="status" class="status"></span></h1>
292
+ <div class="header-row">
293
+ <h1>Slack Web API <span id="status" class="status"></span></h1>
294
+ <button type="button" id="disconnectButton" class="disconnect-btn" onclick="disconnect()">Disconnect</button>
295
+ </div>
258
296
  <div style="background:rgba(240,194,70,0.08);border:1px solid rgba(240,194,70,0.2);border-radius:8px;padding:8px 14px;margin-bottom:16px;display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:8px;font-size:13px;color:#d4c48a">
259
297
  <span>Hosted tiers live — <strong style="color:#f0c246">managed MCP endpoint, OAuth bridge for Claude.ai, encrypted storage</strong></span>
260
298
  <a href="https://mcp.revasserlabs.com" style="color:#f0c246;font-weight:600;text-decoration:none;white-space:nowrap" target="_blank">See tiers &rarr;</a>
@@ -290,18 +328,33 @@
290
328
  let apiKey = null;
291
329
  let currentChannel = null;
292
330
 
293
- // Initialize: check URL param, then localStorage
331
+ function clearStoredApiKey() {
332
+ sessionStorage.removeItem('slackApiKey');
333
+ localStorage.removeItem('slackApiKey');
334
+ }
335
+
336
+ function rememberStoredApiKey(key) {
337
+ sessionStorage.removeItem('slackApiKey');
338
+ localStorage.setItem('slackApiKey', key);
339
+ }
340
+
341
+ function storeSessionApiKey(key) {
342
+ localStorage.removeItem('slackApiKey');
343
+ sessionStorage.setItem('slackApiKey', key);
344
+ }
345
+
346
+ // Initialize: check URL param, session storage, then remembered local storage
294
347
  (function init() {
295
348
  const params = new URLSearchParams(window.location.search);
296
349
  const keyFromUrl = params.get('key');
297
350
 
298
351
  if (keyFromUrl) {
299
- // Save key from magic link and strip from URL
352
+ // Save magic-link keys for this tab only and strip them from the URL.
300
353
  apiKey = keyFromUrl;
301
- localStorage.setItem('slackApiKey', apiKey);
354
+ storeSessionApiKey(apiKey);
302
355
  history.replaceState({}, '', window.location.pathname);
303
356
  } else {
304
- apiKey = localStorage.getItem('slackApiKey');
357
+ apiKey = sessionStorage.getItem('slackApiKey') || localStorage.getItem('slackApiKey');
305
358
  }
306
359
 
307
360
  if (apiKey) {
@@ -329,7 +382,11 @@
329
382
  return;
330
383
  }
331
384
  apiKey = input;
332
- localStorage.setItem('slackApiKey', apiKey);
385
+ if (document.getElementById('rememberApiKey').checked) {
386
+ rememberStoredApiKey(apiKey);
387
+ } else {
388
+ storeSessionApiKey(apiKey);
389
+ }
333
390
  hideModal();
334
391
  connect();
335
392
  }
@@ -346,8 +403,9 @@
346
403
  });
347
404
  if (res.status === 401 || res.status === 403) {
348
405
  // Auth failed - clear stored key and show modal
349
- localStorage.removeItem('slackApiKey');
406
+ clearStoredApiKey();
350
407
  apiKey = null;
408
+ document.getElementById('disconnectButton').classList.remove('visible');
351
409
  showModal('Invalid or expired API key. Check console for current key.');
352
410
  throw new Error('Authentication failed');
353
411
  }
@@ -363,6 +421,7 @@
363
421
  const health = await api('/health');
364
422
  status.textContent = '● ' + health.user + ' @ ' + health.team;
365
423
  status.className = 'status ok';
424
+ document.getElementById('disconnectButton').classList.add('visible');
366
425
  loadConversations('im,mpim');
367
426
  } catch (e) {
368
427
  if (e.message !== 'Authentication failed') {
@@ -371,6 +430,18 @@
371
430
  }
372
431
  }
373
432
  }
433
+ function disconnect() {
434
+ clearStoredApiKey();
435
+ apiKey = null;
436
+ currentChannel = null;
437
+ document.getElementById('disconnectButton').classList.remove('visible');
438
+ document.getElementById('status').textContent = '';
439
+ document.getElementById('conversationList').innerHTML = '<li class="loading">Enter API key to connect</li>';
440
+ document.getElementById('channelName').textContent = 'Select a conversation';
441
+ document.getElementById('messages').innerHTML = '<div class="loading">Messages will appear here</div>';
442
+ showModal('Disconnected. Enter an API key to reconnect.');
443
+ }
444
+
374
445
  async function loadConversations(types, sourceButton = null) {
375
446
  const list = document.getElementById('conversationList');
376
447
  list.innerHTML = '<li class="loading">Loading...</li>';
package/public/share.html CHANGED
@@ -5,9 +5,10 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
6
  <title>Slack MCP Server</title>
7
7
  <meta name="description" content="No OAuth. No admin. 21 Slack tools for Claude, Cursor, Copilot, Gemini, and any MCP client. One command: npx -y @jtalk22/slack-mcp --setup">
8
+ <link rel="canonical" href="https://jtalk22.github.io/slack-mcp-server/public/share.html">
8
9
  <meta property="og:type" content="website">
9
10
  <meta property="og:title" content="Slack MCP Server — No OAuth, no admin, just your browser session">
10
- <meta property="og:description" content="Slack's official MCP needs OAuth + admin. This one uses your browser session. 21 tools, works with Claude, Cursor, Copilot, Gemini.">
11
+ <meta property="og:description" content="OAuth-free Slack MCP using your browser session. 21 tools, works with Claude, Cursor, Copilot, Gemini.">
11
12
  <meta property="og:url" content="https://jtalk22.github.io/slack-mcp-server/public/share.html">
12
13
  <meta property="og:image" content="https://jtalk22.github.io/slack-mcp-server/docs/images/social-preview-v3.png">
13
14
  <meta property="og:image:width" content="1280">
package/server.json CHANGED
@@ -17,7 +17,7 @@
17
17
  "url": "https://github.com/jtalk22/slack-mcp-server",
18
18
  "source": "github"
19
19
  },
20
- "version": "4.3.0",
20
+ "version": "4.4.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": "4.3.0",
31
+ "version": "4.4.0",
32
32
  "transport": {
33
33
  "type": "stdio"
34
34
  },