@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 +41 -5
- package/docs/API.md +32 -5
- package/lib/handlers.js +23 -14
- package/lib/rich-message-fields.js +38 -0
- package/lib/tools.js +32 -4
- package/package.json +2 -1
- package/public/index.html +79 -8
- package/public/share.html +2 -1
- package/server.json +2 -2
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
|
|
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.
|
|
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
|
-
<
|
|
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 →</a>
|
|
@@ -290,18 +328,33 @@
|
|
|
290
328
|
let apiKey = null;
|
|
291
329
|
let currentChannel = null;
|
|
292
330
|
|
|
293
|
-
|
|
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
|
|
352
|
+
// Save magic-link keys for this tab only and strip them from the URL.
|
|
300
353
|
apiKey = keyFromUrl;
|
|
301
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
31
|
+
"version": "4.4.0",
|
|
32
32
|
"transport": {
|
|
33
33
|
"type": "stdio"
|
|
34
34
|
},
|