yt-slack-mcp 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +84 -85
- package/index.js +512 -463
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,101 +1,100 @@
|
|
|
1
1
|
# yt-slack-mcp
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A host-agnostic [Model Context Protocol](https://modelcontextprotocol.io) server for Slack. It communicates over stdio and can be used with any MCP client that launches local servers.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Read Slack channel history and thread replies.
|
|
8
|
+
- Read message permalinks from channels, DMs, and group DMs.
|
|
9
|
+
- Return image attachments inline, extract text from PDFs, and return `text/*` attachment contents inline.
|
|
10
|
+
- Retrieve private attachment contents through the authenticated Slack browser session.
|
|
11
|
+
- Reply to a thread or add a reaction only in explicitly allowed channel IDs.
|
|
12
|
+
|
|
13
|
+
Attachment limits: images and text files are limited to 10 MB; text attachments are truncated at 100,000 characters. PDFs are limited to 20 MB, 50 pages, and 100,000 extracted characters.
|
|
6
14
|
|
|
7
15
|
## Tools
|
|
8
16
|
|
|
9
17
|
| Tool | Description |
|
|
10
18
|
|------|-------------|
|
|
11
|
-
| `slack_read_permalink` | Read a message or thread from a permalink
|
|
12
|
-
| `slack_get_replies` | Read replies for a thread by channel
|
|
13
|
-
| `slack_get_history` | Read recent channel history |
|
|
14
|
-
| `slack_reply_to_thread` | Reply to a thread
|
|
15
|
-
| `slack_add_reaction` | Add a reaction
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
Save each token as a single-line file:
|
|
31
|
-
|
|
19
|
+
| `slack_read_permalink` | Read a message or thread from a channel, DM, or group-DM permalink, including supported attachments. |
|
|
20
|
+
| `slack_get_replies` | Read replies for a thread by channel ID and timestamp. |
|
|
21
|
+
| `slack_get_history` | Read recent channel history. |
|
|
22
|
+
| `slack_reply_to_thread` | Reply to a thread; restricted to allowed channels. |
|
|
23
|
+
| `slack_add_reaction` | Add a reaction; restricted to allowed channels. |
|
|
24
|
+
|
|
25
|
+
## Setup
|
|
26
|
+
|
|
27
|
+
### 1. Obtain browser session tokens
|
|
28
|
+
|
|
29
|
+
- `xoxc`: Slack web localStorage token (from your logged-in browser session).
|
|
30
|
+
- `xoxd`: Slack cookie named `d`.
|
|
31
|
+
|
|
32
|
+
These may need refreshing when Slack rotates/invalidates the session.
|
|
33
|
+
|
|
34
|
+
### 2. Store the secrets
|
|
35
|
+
|
|
36
|
+
Save each token as a single-line file:
|
|
37
|
+
|
|
32
38
|
```
|
|
33
|
-
~/.config/
|
|
34
|
-
~/.config/
|
|
39
|
+
~/.config/yt-slack-mcp/secrets/slack-xoxc.txt
|
|
40
|
+
~/.config/yt-slack-mcp/secrets/slack-xoxd.txt
|
|
35
41
|
```
|
|
36
42
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
### 3. (Optional) Configure allowed write channels and paths
|
|
40
|
-
|
|
41
|
-
All configuration can live in a JSON config file at
|
|
42
|
-
`~/.config/yt-slack-mcp/config.json` (override with `SLACK_MCP_CONFIG`):
|
|
43
|
-
|
|
44
|
-
```json
|
|
43
|
+
The server reads these paths by default. Override them with the config file or environment variables if your MCP host manages secrets elsewhere.
|
|
44
|
+
|
|
45
|
+
### 3. (Optional) Configure allowed write channels and paths
|
|
46
|
+
|
|
47
|
+
All configuration can live in a JSON config file at
|
|
48
|
+
`~/.config/yt-slack-mcp/config.json` (override with `SLACK_MCP_CONFIG`):
|
|
49
|
+
|
|
50
|
+
```json
|
|
45
51
|
{
|
|
46
|
-
"secretsDir": "~/.config/
|
|
47
|
-
"xoxcPath": "~/.config/
|
|
48
|
-
"xoxdPath": "~/.config/
|
|
49
|
-
"allowedWriteChannels": ["C0123456789", "C0987654321"],
|
|
50
|
-
"userAgent": "Mozilla/5.0"
|
|
51
|
-
}
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
By default the write tools (`slack_reply_to_thread`, `slack_add_reaction`)
|
|
55
|
-
are locked down — no channel is writable. Every field is optional; a
|
|
56
|
-
missing file just means defaults. Env vars always override the config file.
|
|
57
|
-
|
|
58
|
-
## Configuration
|
|
59
|
-
|
|
60
|
-
Config resolution order: built-in defaults → config file → env vars.
|
|
61
|
-
|
|
62
|
-
| Config file key | Env var | Default | Description |
|
|
63
|
-
|-----------------|---------|---------|-------------|
|
|
64
|
-
| `secretsDir` | `SLACK_MCP_SECRETS_DIR` | `~/.config/
|
|
65
|
-
| `xoxcPath` | `SLACK_MCP_XOXC_PATH` | `$SECRETS_DIR/slack-xoxc.txt` | Path to the `xoxc` token |
|
|
66
|
-
| `xoxdPath` | `SLACK_MCP_XOXD_PATH` | `$SECRETS_DIR/slack-xoxd.txt` | Path to the `xoxd` token |
|
|
67
|
-
| `allowedWriteChannels` | `SLACK_MCP_ALLOWED_WRITE_CHANNELS` | *(none)* | Channel IDs allowed for write tools (comma-separated in env) |
|
|
68
|
-
| `userAgent` | `SLACK_MCP_USER_AGENT` | `Mozilla/5.0` | HTTP `User-Agent` header |
|
|
69
|
-
| *(config file path)* | `SLACK_MCP_CONFIG` | `~/.config/yt-slack-mcp/config.json` | Path to the JSON config file |
|
|
70
|
-
|
|
71
|
-
## MCP host
|
|
52
|
+
"secretsDir": "~/.config/yt-slack-mcp/secrets",
|
|
53
|
+
"xoxcPath": "~/.config/yt-slack-mcp/secrets/slack-xoxc.txt",
|
|
54
|
+
"xoxdPath": "~/.config/yt-slack-mcp/secrets/slack-xoxd.txt",
|
|
55
|
+
"allowedWriteChannels": ["C0123456789", "C0987654321"],
|
|
56
|
+
"userAgent": "Mozilla/5.0"
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
By default the write tools (`slack_reply_to_thread`, `slack_add_reaction`)
|
|
61
|
+
are locked down — no channel is writable. Every field is optional; a
|
|
62
|
+
missing file just means defaults. Env vars always override the config file.
|
|
63
|
+
|
|
64
|
+
## Configuration
|
|
65
|
+
|
|
66
|
+
Config resolution order: built-in defaults → config file → env vars.
|
|
67
|
+
|
|
68
|
+
| Config file key | Env var | Default | Description |
|
|
69
|
+
|-----------------|---------|---------|-------------|
|
|
70
|
+
| `secretsDir` | `SLACK_MCP_SECRETS_DIR` | `~/.config/yt-slack-mcp/secrets` | Directory holding the token files |
|
|
71
|
+
| `xoxcPath` | `SLACK_MCP_XOXC_PATH` | `$SECRETS_DIR/slack-xoxc.txt` | Path to the `xoxc` token |
|
|
72
|
+
| `xoxdPath` | `SLACK_MCP_XOXD_PATH` | `$SECRETS_DIR/slack-xoxd.txt` | Path to the `xoxd` token |
|
|
73
|
+
| `allowedWriteChannels` | `SLACK_MCP_ALLOWED_WRITE_CHANNELS` | *(none)* | Channel IDs allowed for write tools (comma-separated in env) |
|
|
74
|
+
| `userAgent` | `SLACK_MCP_USER_AGENT` | `Mozilla/5.0` | HTTP `User-Agent` header |
|
|
75
|
+
| *(config file path)* | `SLACK_MCP_CONFIG` | `~/.config/yt-slack-mcp/config.json` | Path to the JSON config file |
|
|
76
|
+
|
|
77
|
+
## MCP host configuration
|
|
78
|
+
|
|
79
|
+
Configure your MCP host to run `yt-slack-mcp` over stdio. The host-specific configuration varies, but the command is:
|
|
72
80
|
|
|
73
|
-
### opencode
|
|
74
|
-
|
|
75
|
-
```jsonc
|
|
76
|
-
{
|
|
77
|
-
"mcp": {
|
|
78
|
-
"slack": {
|
|
79
|
-
"type": "local",
|
|
80
|
-
"command": ["npx", "yt-slack-mcp"]
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
81
|
```
|
|
85
|
-
|
|
86
|
-
### Claude Desktop
|
|
87
|
-
|
|
88
|
-
```json
|
|
89
|
-
{
|
|
90
|
-
"mcpServers": {
|
|
91
|
-
"slack": {
|
|
92
|
-
"command": "npx",
|
|
93
|
-
"args": ["yt-slack-mcp"]
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
}
|
|
82
|
+
npx yt-slack-mcp
|
|
97
83
|
```
|
|
98
84
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
85
|
+
For example, Claude Desktop uses:
|
|
86
|
+
|
|
87
|
+
```json
|
|
88
|
+
{
|
|
89
|
+
"mcpServers": {
|
|
90
|
+
"slack": {
|
|
91
|
+
"command": "npx",
|
|
92
|
+
"args": ["yt-slack-mcp"]
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## License
|
|
99
|
+
|
|
100
|
+
MIT
|
package/index.js
CHANGED
|
@@ -1,463 +1,512 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
// Slack MCP server. Mirrors yt-gmail-mcp / yt-zoho-mcp's structure: pure
|
|
4
|
-
// helpers and the execute-tool factory are exported for unit tests; main()
|
|
5
|
-
// only runs when the file is executed directly.
|
|
6
|
-
|
|
7
|
-
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
8
|
-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
9
|
-
import {
|
|
10
|
-
CallToolRequestSchema,
|
|
11
|
-
ListToolsRequestSchema,
|
|
12
|
-
} from "@modelcontextprotocol/sdk/types.js";
|
|
13
|
-
import { z } from "zod";
|
|
14
|
-
import { zodToJsonSchema } from "zod-to-json-schema";
|
|
15
|
-
import fs from "fs";
|
|
16
|
-
import os from "os";
|
|
17
|
-
import path from "path";
|
|
18
|
-
import { fileURLToPath } from "url";
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const DEFAULT_SECRETS_DIR = path.join(os.homedir(), ".config", "
|
|
22
|
-
const DEFAULT_CONFIG_PATH = path.join(os.homedir(), ".config", "yt-slack-mcp", "config.json");
|
|
23
|
-
|
|
24
|
-
const maxImageBytes = 10 * 1024 * 1024;
|
|
25
|
-
const maxPdfBytes = 20 * 1024 * 1024;
|
|
26
|
-
const maxPdfPages = 50;
|
|
27
|
-
const maxPdfTextCharacters = 100_000;
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
.
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const
|
|
66
|
-
env.
|
|
67
|
-
const
|
|
68
|
-
env.
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
if (item
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
{
|
|
296
|
-
name
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
};
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// Slack MCP server. Mirrors yt-gmail-mcp / yt-zoho-mcp's structure: pure
|
|
4
|
+
// helpers and the execute-tool factory are exported for unit tests; main()
|
|
5
|
+
// only runs when the file is executed directly.
|
|
6
|
+
|
|
7
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
8
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
9
|
+
import {
|
|
10
|
+
CallToolRequestSchema,
|
|
11
|
+
ListToolsRequestSchema,
|
|
12
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
13
|
+
import { z } from "zod";
|
|
14
|
+
import { zodToJsonSchema } from "zod-to-json-schema";
|
|
15
|
+
import fs from "fs";
|
|
16
|
+
import os from "os";
|
|
17
|
+
import path from "path";
|
|
18
|
+
import { fileURLToPath } from "url";
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
const DEFAULT_SECRETS_DIR = path.join(os.homedir(), ".config", "yt-slack-mcp", "secrets");
|
|
22
|
+
const DEFAULT_CONFIG_PATH = path.join(os.homedir(), ".config", "yt-slack-mcp", "config.json");
|
|
23
|
+
|
|
24
|
+
const maxImageBytes = 10 * 1024 * 1024;
|
|
25
|
+
const maxPdfBytes = 20 * 1024 * 1024;
|
|
26
|
+
const maxPdfPages = 50;
|
|
27
|
+
const maxPdfTextCharacters = 100_000;
|
|
28
|
+
const maxTextFileBytes = 10 * 1024 * 1024;
|
|
29
|
+
const maxTextFileCharacters = 100_000;
|
|
30
|
+
|
|
31
|
+
function expandHome(filePath) {
|
|
32
|
+
if (!filePath) {
|
|
33
|
+
return filePath;
|
|
34
|
+
}
|
|
35
|
+
if (filePath === "~") {
|
|
36
|
+
return os.homedir();
|
|
37
|
+
}
|
|
38
|
+
if (filePath.startsWith("~/") || filePath.startsWith("~\\")) {
|
|
39
|
+
return path.join(os.homedir(), filePath.slice(2));
|
|
40
|
+
}
|
|
41
|
+
return filePath;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function parseChannelList(value) {
|
|
45
|
+
if (value === undefined || value === null || value === "") {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
return value
|
|
49
|
+
.split(",")
|
|
50
|
+
.map((c) => c.trim())
|
|
51
|
+
.filter(Boolean);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function loadConfig(env = process.env) {
|
|
55
|
+
const configPath = env.SLACK_MCP_CONFIG || DEFAULT_CONFIG_PATH;
|
|
56
|
+
let fileConfig = {};
|
|
57
|
+
if (fs.existsSync(configPath)) {
|
|
58
|
+
try {
|
|
59
|
+
fileConfig = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
60
|
+
} catch (e) {
|
|
61
|
+
throw new Error(`invalid Slack MCP config ${configPath}: ${e.message}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const secretsDir =
|
|
66
|
+
env.SLACK_MCP_SECRETS_DIR || expandHome(fileConfig.secretsDir) || DEFAULT_SECRETS_DIR;
|
|
67
|
+
const xoxcPath =
|
|
68
|
+
env.SLACK_MCP_XOXC_PATH || expandHome(fileConfig.xoxcPath) || path.join(secretsDir, "slack-xoxc.txt");
|
|
69
|
+
const xoxdPath =
|
|
70
|
+
env.SLACK_MCP_XOXD_PATH || expandHome(fileConfig.xoxdPath) || path.join(secretsDir, "slack-xoxd.txt");
|
|
71
|
+
const allowedWriteChannels =
|
|
72
|
+
parseChannelList(env.SLACK_MCP_ALLOWED_WRITE_CHANNELS) ?? fileConfig.allowedWriteChannels ?? [];
|
|
73
|
+
const userAgent = env.SLACK_MCP_USER_AGENT || fileConfig.userAgent || "Mozilla/5.0";
|
|
74
|
+
|
|
75
|
+
return { configPath, secretsDir, xoxcPath, xoxdPath, allowedWriteChannels, userAgent };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function readSecret(filePath, label) {
|
|
79
|
+
if (!fs.existsSync(filePath)) {
|
|
80
|
+
throw new Error(`missing ${label} (looked at ${filePath})`);
|
|
81
|
+
}
|
|
82
|
+
return fs.readFileSync(filePath, "utf8").trim().replace(/^['"]|['"]$/g, "");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function createSlackClient({ xoxcToken, xoxdToken, userAgent = "Mozilla/5.0" }) {
|
|
86
|
+
const slackHeaders = () => ({
|
|
87
|
+
authorization: `Bearer ${xoxcToken}`,
|
|
88
|
+
cookie: `d=${xoxdToken}`,
|
|
89
|
+
"user-agent": userAgent,
|
|
90
|
+
"content-type": "application/x-www-form-urlencoded",
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const slackApi = async (method, params, httpMethod = "GET") => {
|
|
94
|
+
const url = new URL(`https://slack.com/api/${method}`);
|
|
95
|
+
const options = { method: httpMethod, headers: slackHeaders() };
|
|
96
|
+
if (httpMethod === "GET") {
|
|
97
|
+
for (const [key, value] of Object.entries(params)) {
|
|
98
|
+
if (value !== undefined && value !== null) {
|
|
99
|
+
url.searchParams.set(key, value);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
} else {
|
|
103
|
+
options.body = new URLSearchParams(params).toString();
|
|
104
|
+
}
|
|
105
|
+
const response = await fetch(url, options);
|
|
106
|
+
const data = await response.json();
|
|
107
|
+
if (!data.ok) {
|
|
108
|
+
throw new Error(`slack ${method} failed: ${data.error || response.statusText}`);
|
|
109
|
+
}
|
|
110
|
+
return data;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const getSlackFileData = async (file) => {
|
|
114
|
+
if (file.url_private_download || file.url_private || !file.id) {
|
|
115
|
+
return file;
|
|
116
|
+
}
|
|
117
|
+
const response = await slackApi("files.info", { file: file.id });
|
|
118
|
+
return response.file || file;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const downloadSlackImage = async (file) => {
|
|
122
|
+
const fileData = await getSlackFileData(file);
|
|
123
|
+
const imageUrl = fileData.url_private_download || fileData.url_private;
|
|
124
|
+
if (!imageUrl) {
|
|
125
|
+
throw new Error("Slack did not provide a private image URL");
|
|
126
|
+
}
|
|
127
|
+
const response = await fetch(imageUrl, { headers: slackHeaders() });
|
|
128
|
+
if (!response.ok) {
|
|
129
|
+
throw new Error(`Slack image download failed: ${response.status}`);
|
|
130
|
+
}
|
|
131
|
+
const imageData = await response.arrayBuffer();
|
|
132
|
+
if (imageData.byteLength > maxImageBytes) {
|
|
133
|
+
throw new Error(`image exceeds ${maxImageBytes / 1024 / 1024} MB limit`);
|
|
134
|
+
}
|
|
135
|
+
const mimeType = response.headers.get("content-type") || fileData.mimetype;
|
|
136
|
+
if (!mimeType?.startsWith("image/")) {
|
|
137
|
+
throw new Error(`Slack returned ${mimeType || "an unknown content type"}`);
|
|
138
|
+
}
|
|
139
|
+
return { type: "image", data: Buffer.from(imageData).toString("base64"), mimeType: mimeType.split(";", 1)[0] };
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const extractPdfText = async (pdfData) => {
|
|
143
|
+
const pdfjs = await import("pdfjs-dist/legacy/build/pdf.mjs");
|
|
144
|
+
const document = await pdfjs.getDocument({ data: new Uint8Array(pdfData), useWorkerFetch: false, isEvalSupported: false }).promise;
|
|
145
|
+
const pageCount = Math.min(document.numPages, maxPdfPages);
|
|
146
|
+
const pages = [];
|
|
147
|
+
for (let pageNumber = 1; pageNumber <= pageCount; pageNumber++) {
|
|
148
|
+
const page = await document.getPage(pageNumber);
|
|
149
|
+
const textContent = await page.getTextContent();
|
|
150
|
+
const lines = [];
|
|
151
|
+
let line = "";
|
|
152
|
+
for (const item of textContent.items) {
|
|
153
|
+
if (!("str" in item)) continue;
|
|
154
|
+
line += `${item.str}${item.hasEOL ? "" : " "}`;
|
|
155
|
+
if (item.hasEOL) {
|
|
156
|
+
lines.push(line.trim());
|
|
157
|
+
line = "";
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (line.trim()) lines.push(line.trim());
|
|
161
|
+
pages.push(`Page ${pageNumber}\n${lines.join("\n")}`);
|
|
162
|
+
}
|
|
163
|
+
let text = pages.join("\n\n").trim();
|
|
164
|
+
if (document.numPages > maxPdfPages) {
|
|
165
|
+
text += `\n\n[PDF text truncated after ${maxPdfPages} pages]`;
|
|
166
|
+
}
|
|
167
|
+
if (text.length > maxPdfTextCharacters) {
|
|
168
|
+
text = `${text.slice(0, maxPdfTextCharacters)}\n\n[PDF text truncated at ${maxPdfTextCharacters} characters]`;
|
|
169
|
+
}
|
|
170
|
+
return text || "[PDF contains no extractable text]";
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const downloadSlackPdf = async (file) => {
|
|
174
|
+
const fileData = await getSlackFileData(file);
|
|
175
|
+
const pdfUrl = fileData.url_private_download || fileData.url_private;
|
|
176
|
+
if (!pdfUrl) {
|
|
177
|
+
throw new Error("Slack did not provide a private PDF URL");
|
|
178
|
+
}
|
|
179
|
+
const response = await fetch(pdfUrl, { headers: slackHeaders() });
|
|
180
|
+
if (!response.ok) {
|
|
181
|
+
throw new Error(`Slack PDF download failed: ${response.status}`);
|
|
182
|
+
}
|
|
183
|
+
const pdfData = await response.arrayBuffer();
|
|
184
|
+
if (pdfData.byteLength > maxPdfBytes) {
|
|
185
|
+
throw new Error(`PDF exceeds ${maxPdfBytes / 1024 / 1024} MB limit`);
|
|
186
|
+
}
|
|
187
|
+
const mimeType = response.headers.get("content-type") || fileData.mimetype;
|
|
188
|
+
if (mimeType !== "application/pdf" && !(fileData.name || "").toLowerCase().endsWith(".pdf")) {
|
|
189
|
+
throw new Error(`Slack returned ${mimeType || "an unknown content type"}`);
|
|
190
|
+
}
|
|
191
|
+
return extractPdfText(pdfData);
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const downloadSlackText = async (file) => {
|
|
195
|
+
const fileData = await getSlackFileData(file);
|
|
196
|
+
const textUrl = fileData.url_private_download || fileData.url_private;
|
|
197
|
+
if (!textUrl) {
|
|
198
|
+
throw new Error("Slack did not provide a private text file URL");
|
|
199
|
+
}
|
|
200
|
+
const response = await fetch(textUrl, { headers: slackHeaders() });
|
|
201
|
+
if (!response.ok) {
|
|
202
|
+
throw new Error(`Slack text file download failed: ${response.status}`);
|
|
203
|
+
}
|
|
204
|
+
const contentLength = Number(response.headers.get("content-length"));
|
|
205
|
+
if (Number.isFinite(contentLength) && contentLength > maxTextFileBytes) {
|
|
206
|
+
await response.body?.cancel();
|
|
207
|
+
throw new Error(`text file exceeds ${maxTextFileBytes / 1024 / 1024} MB limit`);
|
|
208
|
+
}
|
|
209
|
+
const reader = response.body?.getReader();
|
|
210
|
+
if (!reader) {
|
|
211
|
+
throw new Error("Slack returned an empty text file response");
|
|
212
|
+
}
|
|
213
|
+
const chunks = [];
|
|
214
|
+
let byteLength = 0;
|
|
215
|
+
while (true) {
|
|
216
|
+
const { done, value } = await reader.read();
|
|
217
|
+
if (done) break;
|
|
218
|
+
byteLength += value.byteLength;
|
|
219
|
+
if (byteLength > maxTextFileBytes) {
|
|
220
|
+
await reader.cancel();
|
|
221
|
+
throw new Error(`text file exceeds ${maxTextFileBytes / 1024 / 1024} MB limit`);
|
|
222
|
+
}
|
|
223
|
+
chunks.push(Buffer.from(value));
|
|
224
|
+
}
|
|
225
|
+
let text = Buffer.concat(chunks, byteLength).toString("utf8");
|
|
226
|
+
if (text.length > maxTextFileCharacters) {
|
|
227
|
+
text = `${text.slice(0, maxTextFileCharacters)}\n\n[Text file truncated at ${maxTextFileCharacters} characters]`;
|
|
228
|
+
}
|
|
229
|
+
return text;
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
const formatMessages = async (messages) => {
|
|
233
|
+
if (!messages.length) {
|
|
234
|
+
return [{ type: "text", text: "No messages found." }];
|
|
235
|
+
}
|
|
236
|
+
const content = [];
|
|
237
|
+
for (const message of messages) {
|
|
238
|
+
const author = message.user || message.username || message.bot_id || "unknown";
|
|
239
|
+
const lines = [`[${message.ts}] ${author}: ${message.text || ""}`];
|
|
240
|
+
if (message.files?.length) {
|
|
241
|
+
for (const file of message.files) {
|
|
242
|
+
lines.push(`file: ${file.title || file.name || file.id || "untitled"} (${file.mimetype || "unknown type"})`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (message.attachments?.length) {
|
|
246
|
+
for (const attachment of message.attachments) {
|
|
247
|
+
if (attachment.title) lines.push(`attachment: ${attachment.title}`);
|
|
248
|
+
if (attachment.text) lines.push(`attachment text: ${attachment.text}`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
content.push({ type: "text", text: lines.join("\n") });
|
|
252
|
+
for (const file of message.files || []) {
|
|
253
|
+
if (file.mimetype?.startsWith("image/")) {
|
|
254
|
+
try {
|
|
255
|
+
content.push(await downloadSlackImage(file));
|
|
256
|
+
} catch (error) {
|
|
257
|
+
content.push({ type: "text", text: `image unavailable: ${error.message}` });
|
|
258
|
+
}
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
if (file.mimetype === "application/pdf" || file.name?.toLowerCase().endsWith(".pdf")) {
|
|
262
|
+
try {
|
|
263
|
+
const title = file.title || file.name || file.id || "untitled";
|
|
264
|
+
content.push({ type: "text", text: `PDF text: ${title}\n\n${await downloadSlackPdf(file)}` });
|
|
265
|
+
} catch (error) {
|
|
266
|
+
content.push({ type: "text", text: `PDF unavailable: ${error.message}` });
|
|
267
|
+
}
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
if (file.mimetype?.startsWith("text/")) {
|
|
271
|
+
try {
|
|
272
|
+
const title = file.title || file.name || file.id || "untitled";
|
|
273
|
+
content.push({ type: "text", text: `Text file: ${title}\n\n${await downloadSlackText(file)}` });
|
|
274
|
+
} catch (error) {
|
|
275
|
+
content.push({ type: "text", text: `Text file unavailable: ${error.message}` });
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return content;
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
return { slackApi, formatMessages };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function parsePermalink(url) {
|
|
287
|
+
const match = url.match(/\/archives\/([CDG][A-Z0-9]+)\/p(\d{10})(\d{6})/);
|
|
288
|
+
if (!match) {
|
|
289
|
+
throw new Error("invalid Slack permalink");
|
|
290
|
+
}
|
|
291
|
+
return { channelId: match[1], timestamp: `${match[2]}.${match[3]}` };
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function requireString(value, name) {
|
|
295
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
296
|
+
throw new Error(`missing ${name}`);
|
|
297
|
+
}
|
|
298
|
+
return value.trim();
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function assertAllowedWriteChannel(channelId, allowed) {
|
|
302
|
+
if (!allowed.has(channelId)) {
|
|
303
|
+
throw new Error(`writes are not allowed for channel ${channelId}`);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// ── Schemas ─────────────────────────────────────────────────────────────────
|
|
308
|
+
|
|
309
|
+
const ReadPermalinkSchema = z.object({
|
|
310
|
+
url: z.string().describe("Slack permalink, e.g. https://<workspace>.slack.com/archives/C.../p..."),
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
const GetRepliesSchema = z.object({
|
|
314
|
+
channel_id: z.string(),
|
|
315
|
+
thread_ts: z.string(),
|
|
316
|
+
limit: z.number().int().min(1).max(200).optional().default(50),
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
const GetHistorySchema = z.object({
|
|
320
|
+
channel_id: z.string(),
|
|
321
|
+
limit: z.number().int().min(1).max(200).optional().default(20),
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
const ReplyToThreadSchema = z.object({
|
|
325
|
+
channel_id: z.string(),
|
|
326
|
+
thread_ts: z.string(),
|
|
327
|
+
text: z.string(),
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
const AddReactionSchema = z.object({
|
|
331
|
+
channel_id: z.string(),
|
|
332
|
+
timestamp: z.string(),
|
|
333
|
+
reaction: z.string().describe("Emoji name without colons."),
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
// ── Tool definitions ─────────────────────────────────────────────────────────
|
|
337
|
+
|
|
338
|
+
const TOOLS = [
|
|
339
|
+
{
|
|
340
|
+
name: "slack_read_permalink",
|
|
341
|
+
description: "Read a Slack message or thread from a permalink, including inline text files, extracted PDFs, and images.",
|
|
342
|
+
inputSchema: zodToJsonSchema(ReadPermalinkSchema),
|
|
343
|
+
},
|
|
344
|
+
{
|
|
345
|
+
name: "slack_get_replies",
|
|
346
|
+
description: "Read replies for a Slack thread by channel id and thread timestamp.",
|
|
347
|
+
inputSchema: zodToJsonSchema(GetRepliesSchema),
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
name: "slack_get_history",
|
|
351
|
+
description: "Read recent Slack channel history.",
|
|
352
|
+
inputSchema: zodToJsonSchema(GetHistorySchema),
|
|
353
|
+
},
|
|
354
|
+
{
|
|
355
|
+
name: "slack_reply_to_thread",
|
|
356
|
+
description: "Reply to a Slack thread. Restricted to allowed channels.",
|
|
357
|
+
inputSchema: zodToJsonSchema(ReplyToThreadSchema),
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
name: "slack_add_reaction",
|
|
361
|
+
description: "Add a reaction to a Slack message. Restricted to allowed channels.",
|
|
362
|
+
inputSchema: zodToJsonSchema(AddReactionSchema),
|
|
363
|
+
},
|
|
364
|
+
];
|
|
365
|
+
|
|
366
|
+
// ── Execute factory (exported for tests) ─────────────────────────────────────
|
|
367
|
+
|
|
368
|
+
function createExecuteToolCall({ slackApi, formatMessages, allowedWriteChannels }) {
|
|
369
|
+
const allowed = new Set(allowedWriteChannels);
|
|
370
|
+
return async function executeToolCall(name, args) {
|
|
371
|
+
switch (name) {
|
|
372
|
+
case "slack_read_permalink": {
|
|
373
|
+
const a = ReadPermalinkSchema.parse(args);
|
|
374
|
+
const { channelId, timestamp } = parsePermalink(a.url);
|
|
375
|
+
const messages = await slackApi("conversations.replies", {
|
|
376
|
+
channel: channelId,
|
|
377
|
+
ts: timestamp,
|
|
378
|
+
limit: "50",
|
|
379
|
+
});
|
|
380
|
+
return { content: await formatMessages(messages.messages || []) };
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
case "slack_get_replies": {
|
|
384
|
+
const a = GetRepliesSchema.parse(args);
|
|
385
|
+
const messages = await slackApi("conversations.replies", {
|
|
386
|
+
channel: a.channel_id,
|
|
387
|
+
ts: a.thread_ts,
|
|
388
|
+
limit: String(a.limit),
|
|
389
|
+
});
|
|
390
|
+
return { content: await formatMessages(messages.messages || []) };
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
case "slack_get_history": {
|
|
394
|
+
const a = GetHistorySchema.parse(args);
|
|
395
|
+
const messages = await slackApi("conversations.history", {
|
|
396
|
+
channel: a.channel_id,
|
|
397
|
+
limit: String(a.limit),
|
|
398
|
+
});
|
|
399
|
+
return { content: await formatMessages(messages.messages || []) };
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
case "slack_reply_to_thread": {
|
|
403
|
+
const a = ReplyToThreadSchema.parse(args);
|
|
404
|
+
const channelId = requireString(a.channel_id, "channel_id");
|
|
405
|
+
assertAllowedWriteChannel(channelId, allowed);
|
|
406
|
+
const response = await slackApi("chat.postMessage", {
|
|
407
|
+
channel: channelId,
|
|
408
|
+
thread_ts: requireString(a.thread_ts, "thread_ts"),
|
|
409
|
+
text: requireString(a.text, "text"),
|
|
410
|
+
}, "POST");
|
|
411
|
+
return { content: [{ type: "text", text: `posted reply at ${response.ts}` }] };
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
case "slack_add_reaction": {
|
|
415
|
+
const a = AddReactionSchema.parse(args);
|
|
416
|
+
const channelId = requireString(a.channel_id, "channel_id");
|
|
417
|
+
assertAllowedWriteChannel(channelId, allowed);
|
|
418
|
+
await slackApi("reactions.add", {
|
|
419
|
+
channel: channelId,
|
|
420
|
+
timestamp: requireString(a.timestamp, "timestamp"),
|
|
421
|
+
name: requireString(a.reaction, "reaction"),
|
|
422
|
+
}, "POST");
|
|
423
|
+
return { content: [{ type: "text", text: "added reaction" }] };
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
default:
|
|
427
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
428
|
+
}
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// ── Main ─────────────────────────────────────────────────────────────────────
|
|
433
|
+
|
|
434
|
+
async function main() {
|
|
435
|
+
let config;
|
|
436
|
+
try {
|
|
437
|
+
config = loadConfig();
|
|
438
|
+
} catch (e) {
|
|
439
|
+
console.error(`Error: ${e.message}`);
|
|
440
|
+
process.exit(1);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
let xoxcToken, xoxdToken;
|
|
444
|
+
try {
|
|
445
|
+
xoxcToken = readSecret(config.xoxcPath, "slack-xoxc.txt");
|
|
446
|
+
xoxdToken = readSecret(config.xoxdPath, "slack-xoxd.txt");
|
|
447
|
+
} catch (e) {
|
|
448
|
+
console.error(`Error: ${e.message}`);
|
|
449
|
+
process.exit(1);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const { slackApi, formatMessages } = createSlackClient({ xoxcToken, xoxdToken, userAgent: config.userAgent });
|
|
453
|
+
|
|
454
|
+
const server = new Server(
|
|
455
|
+
{ name: "yt-slack-mcp", version: "0.1.0" },
|
|
456
|
+
{
|
|
457
|
+
capabilities: { tools: {} },
|
|
458
|
+
instructions:
|
|
459
|
+
"MCP server for Slack. Read with slack_read_permalink, slack_get_replies, slack_get_history; write with slack_reply_to_thread, slack_add_reaction (restricted to allowed channels).",
|
|
460
|
+
}
|
|
461
|
+
);
|
|
462
|
+
|
|
463
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
|
|
464
|
+
|
|
465
|
+
const executeToolCall = createExecuteToolCall({
|
|
466
|
+
slackApi,
|
|
467
|
+
formatMessages,
|
|
468
|
+
allowedWriteChannels: config.allowedWriteChannels,
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
472
|
+
const { name, arguments: args } = req.params;
|
|
473
|
+
try {
|
|
474
|
+
return await executeToolCall(name, args);
|
|
475
|
+
} catch (e) {
|
|
476
|
+
return {
|
|
477
|
+
content: [{ type: "text", text: `Error: ${e.message}` }],
|
|
478
|
+
isError: true,
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
const transport = new StdioServerTransport();
|
|
484
|
+
await server.connect(transport);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const isMain =
|
|
488
|
+
process.argv[1] &&
|
|
489
|
+
fs.realpathSync(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
490
|
+
|
|
491
|
+
if (isMain) {
|
|
492
|
+
main().catch((e) => {
|
|
493
|
+
console.error("Fatal:", e);
|
|
494
|
+
process.exit(1);
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
export {
|
|
499
|
+
parsePermalink,
|
|
500
|
+
requireString,
|
|
501
|
+
assertAllowedWriteChannel,
|
|
502
|
+
createExecuteToolCall,
|
|
503
|
+
createSlackClient,
|
|
504
|
+
loadConfig,
|
|
505
|
+
expandHome,
|
|
506
|
+
parseChannelList,
|
|
507
|
+
ReadPermalinkSchema,
|
|
508
|
+
GetRepliesSchema,
|
|
509
|
+
GetHistorySchema,
|
|
510
|
+
ReplyToThreadSchema,
|
|
511
|
+
AddReactionSchema,
|
|
512
|
+
};
|