yt-slack-mcp 0.1.1 → 0.3.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/LICENSE +21 -21
- package/README.md +49 -46
- package/index.js +153 -7
- package/package.json +32 -32
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 Maksim Korinets
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Maksim Korinets
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,26 +1,28 @@
|
|
|
1
|
-
# yt-slack-mcp
|
|
2
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
14
|
-
|
|
15
|
-
## Tools
|
|
16
|
-
|
|
17
|
-
| Tool | Description |
|
|
18
|
-
|------|-------------|
|
|
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. |
|
|
1
|
+
# yt-slack-mcp
|
|
2
|
+
|
|
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
|
+
|
|
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, add a reaction, edit an authored message or thread reply, or upload local files 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.
|
|
14
|
+
|
|
15
|
+
## Tools
|
|
16
|
+
|
|
17
|
+
| Tool | Description |
|
|
18
|
+
|------|-------------|
|
|
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
|
+
| `slack_upload_files` | Upload up to 10 local files to a channel or thread, with an optional initial comment; returns file and message permalinks; restricted to allowed channels. |
|
|
25
|
+
| `slack_edit_message` | Edit an authored message or thread reply by channel ID and timestamp; replaces the message text (Slack removes any existing Block Kit blocks) and returns the updated timestamp and permalink; restricted to allowed channels. |
|
|
24
26
|
|
|
25
27
|
## Setup
|
|
26
28
|
|
|
@@ -31,16 +33,16 @@ Attachment limits: images and text files are limited to 10 MB; text attachments
|
|
|
31
33
|
|
|
32
34
|
These may need refreshing when Slack rotates/invalidates the session.
|
|
33
35
|
|
|
34
|
-
### 2. Store the secrets
|
|
36
|
+
### 2. Store the secrets
|
|
35
37
|
|
|
36
38
|
Save each token as a single-line file:
|
|
37
39
|
|
|
38
|
-
```
|
|
39
|
-
~/.config/yt-slack-mcp/secrets/slack-xoxc.txt
|
|
40
|
-
~/.config/yt-slack-mcp/secrets/slack-xoxd.txt
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
The server reads these paths by default. Override them with the config file or environment variables if your MCP host manages secrets elsewhere.
|
|
40
|
+
```
|
|
41
|
+
~/.config/yt-slack-mcp/secrets/slack-xoxc.txt
|
|
42
|
+
~/.config/yt-slack-mcp/secrets/slack-xoxd.txt
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The server reads these paths by default. Override them with the config file or environment variables if your MCP host manages secrets elsewhere.
|
|
44
46
|
|
|
45
47
|
### 3. (Optional) Configure allowed write channels and paths
|
|
46
48
|
|
|
@@ -48,17 +50,18 @@ All configuration can live in a JSON config file at
|
|
|
48
50
|
`~/.config/yt-slack-mcp/config.json` (override with `SLACK_MCP_CONFIG`):
|
|
49
51
|
|
|
50
52
|
```json
|
|
51
|
-
{
|
|
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",
|
|
53
|
+
{
|
|
54
|
+
"secretsDir": "~/.config/yt-slack-mcp/secrets",
|
|
55
|
+
"xoxcPath": "~/.config/yt-slack-mcp/secrets/slack-xoxc.txt",
|
|
56
|
+
"xoxdPath": "~/.config/yt-slack-mcp/secrets/slack-xoxd.txt",
|
|
55
57
|
"allowedWriteChannels": ["C0123456789", "C0987654321"],
|
|
56
58
|
"userAgent": "Mozilla/5.0"
|
|
57
59
|
}
|
|
58
60
|
```
|
|
59
61
|
|
|
60
|
-
By default the write tools (`slack_reply_to_thread`, `slack_add_reaction
|
|
61
|
-
are locked down — no channel is
|
|
62
|
+
By default the write tools (`slack_reply_to_thread`, `slack_add_reaction`,
|
|
63
|
+
`slack_edit_message`) and `slack_upload_files` are locked down — no channel is
|
|
64
|
+
writable. Each upload is limited to 10 MB. Every field is optional; a
|
|
62
65
|
missing file just means defaults. Env vars always override the config file.
|
|
63
66
|
|
|
64
67
|
## Configuration
|
|
@@ -67,22 +70,22 @@ Config resolution order: built-in defaults → config file → env vars.
|
|
|
67
70
|
|
|
68
71
|
| Config file key | Env var | Default | Description |
|
|
69
72
|
|-----------------|---------|---------|-------------|
|
|
70
|
-
| `secretsDir` | `SLACK_MCP_SECRETS_DIR` | `~/.config/yt-slack-mcp/secrets` | Directory holding the token files |
|
|
73
|
+
| `secretsDir` | `SLACK_MCP_SECRETS_DIR` | `~/.config/yt-slack-mcp/secrets` | Directory holding the token files |
|
|
71
74
|
| `xoxcPath` | `SLACK_MCP_XOXC_PATH` | `$SECRETS_DIR/slack-xoxc.txt` | Path to the `xoxc` token |
|
|
72
75
|
| `xoxdPath` | `SLACK_MCP_XOXD_PATH` | `$SECRETS_DIR/slack-xoxd.txt` | Path to the `xoxd` token |
|
|
73
76
|
| `allowedWriteChannels` | `SLACK_MCP_ALLOWED_WRITE_CHANNELS` | *(none)* | Channel IDs allowed for write tools (comma-separated in env) |
|
|
74
77
|
| `userAgent` | `SLACK_MCP_USER_AGENT` | `Mozilla/5.0` | HTTP `User-Agent` header |
|
|
75
78
|
| *(config file path)* | `SLACK_MCP_CONFIG` | `~/.config/yt-slack-mcp/config.json` | Path to the JSON config file |
|
|
76
79
|
|
|
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:
|
|
80
|
-
|
|
81
|
-
```
|
|
82
|
-
npx yt-slack-mcp
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
For example, Claude Desktop uses:
|
|
80
|
+
## MCP host configuration
|
|
81
|
+
|
|
82
|
+
Configure your MCP host to run `yt-slack-mcp` over stdio. The host-specific configuration varies, but the command is:
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
npx yt-slack-mcp
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
For example, Claude Desktop uses:
|
|
86
89
|
|
|
87
90
|
```json
|
|
88
91
|
{
|
package/index.js
CHANGED
|
@@ -18,7 +18,7 @@ import path from "path";
|
|
|
18
18
|
import { fileURLToPath } from "url";
|
|
19
19
|
|
|
20
20
|
|
|
21
|
-
const DEFAULT_SECRETS_DIR = path.join(os.homedir(), ".config", "yt-slack-mcp", "secrets");
|
|
21
|
+
const DEFAULT_SECRETS_DIR = path.join(os.homedir(), ".config", "yt-slack-mcp", "secrets");
|
|
22
22
|
const DEFAULT_CONFIG_PATH = path.join(os.homedir(), ".config", "yt-slack-mcp", "config.json");
|
|
23
23
|
|
|
24
24
|
const maxImageBytes = 10 * 1024 * 1024;
|
|
@@ -27,6 +27,7 @@ const maxPdfPages = 50;
|
|
|
27
27
|
const maxPdfTextCharacters = 100_000;
|
|
28
28
|
const maxTextFileBytes = 10 * 1024 * 1024;
|
|
29
29
|
const maxTextFileCharacters = 100_000;
|
|
30
|
+
const maxUploadBytes = 10 * 1024 * 1024;
|
|
30
31
|
|
|
31
32
|
function expandHome(filePath) {
|
|
32
33
|
if (!filePath) {
|
|
@@ -110,8 +111,8 @@ function createSlackClient({ xoxcToken, xoxdToken, userAgent = "Mozilla/5.0" })
|
|
|
110
111
|
return data;
|
|
111
112
|
};
|
|
112
113
|
|
|
113
|
-
const getSlackFileData = async (file) => {
|
|
114
|
-
if (file.url_private_download || file.url_private || !file.id) {
|
|
114
|
+
const getSlackFileData = async (file, { refresh = false } = {}) => {
|
|
115
|
+
if (!refresh && (file.url_private_download || file.url_private || !file.id)) {
|
|
115
116
|
return file;
|
|
116
117
|
}
|
|
117
118
|
const response = await slackApi("files.info", { file: file.id });
|
|
@@ -229,6 +230,71 @@ function createSlackClient({ xoxcToken, xoxdToken, userAgent = "Mozilla/5.0" })
|
|
|
229
230
|
return text;
|
|
230
231
|
};
|
|
231
232
|
|
|
233
|
+
const uploadSlackFiles = async ({ filePaths, channelId, threadTs, initialComment }) => {
|
|
234
|
+
const localFiles = filePaths.map((filePath) => {
|
|
235
|
+
const fileStats = fs.statSync(filePath);
|
|
236
|
+
if (!fileStats.isFile()) {
|
|
237
|
+
throw new Error(`not a file: ${filePath}`);
|
|
238
|
+
}
|
|
239
|
+
if (fileStats.size > maxUploadBytes) {
|
|
240
|
+
throw new Error(`file exceeds ${maxUploadBytes / 1024 / 1024} MB limit: ${filePath}`);
|
|
241
|
+
}
|
|
242
|
+
const data = fs.readFileSync(filePath);
|
|
243
|
+
if (data.length > maxUploadBytes) {
|
|
244
|
+
throw new Error(`file exceeds ${maxUploadBytes / 1024 / 1024} MB limit: ${filePath}`);
|
|
245
|
+
}
|
|
246
|
+
return { name: path.basename(filePath), data };
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
const uploadedFiles = [];
|
|
250
|
+
for (const file of localFiles) {
|
|
251
|
+
const upload = await slackApi("files.getUploadURLExternal", {
|
|
252
|
+
filename: file.name,
|
|
253
|
+
length: String(file.data.length),
|
|
254
|
+
}, "POST");
|
|
255
|
+
if (!upload.upload_url || !upload.file_id) {
|
|
256
|
+
throw new Error("Slack did not provide an upload URL or file ID");
|
|
257
|
+
}
|
|
258
|
+
const response = await fetch(upload.upload_url, {
|
|
259
|
+
method: "POST",
|
|
260
|
+
headers: { "content-type": "application/octet-stream" },
|
|
261
|
+
body: file.data,
|
|
262
|
+
});
|
|
263
|
+
if (!response.ok) {
|
|
264
|
+
throw new Error(`Slack file upload failed: ${response.status}`);
|
|
265
|
+
}
|
|
266
|
+
uploadedFiles.push({ id: upload.file_id, title: file.name });
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const completeParams = {
|
|
270
|
+
files: JSON.stringify(uploadedFiles),
|
|
271
|
+
channel_id: channelId,
|
|
272
|
+
};
|
|
273
|
+
if (threadTs) completeParams.thread_ts = threadTs;
|
|
274
|
+
if (initialComment) completeParams.initial_comment = initialComment;
|
|
275
|
+
const completed = await slackApi("files.completeUploadExternal", completeParams, "POST");
|
|
276
|
+
if (!completed.files?.length) {
|
|
277
|
+
throw new Error("Slack did not return uploaded file details");
|
|
278
|
+
}
|
|
279
|
+
return Promise.all(completed.files.map(async (file) => {
|
|
280
|
+
const fileData = await getSlackFileData(file, { refresh: true });
|
|
281
|
+
const shares = Object.values(fileData.shares || {});
|
|
282
|
+
const share = shares.flatMap((shareType) => shareType[channelId] || []).at(0);
|
|
283
|
+
if (!share?.ts) {
|
|
284
|
+
return fileData;
|
|
285
|
+
}
|
|
286
|
+
try {
|
|
287
|
+
const permalink = await slackApi("chat.getPermalink", {
|
|
288
|
+
channel: channelId,
|
|
289
|
+
message_ts: share.ts,
|
|
290
|
+
});
|
|
291
|
+
return { ...fileData, message_permalink: permalink.permalink };
|
|
292
|
+
} catch {
|
|
293
|
+
return fileData;
|
|
294
|
+
}
|
|
295
|
+
}));
|
|
296
|
+
};
|
|
297
|
+
|
|
232
298
|
const formatMessages = async (messages) => {
|
|
233
299
|
if (!messages.length) {
|
|
234
300
|
return [{ type: "text", text: "No messages found." }];
|
|
@@ -280,7 +346,7 @@ function createSlackClient({ xoxcToken, xoxdToken, userAgent = "Mozilla/5.0" })
|
|
|
280
346
|
return content;
|
|
281
347
|
};
|
|
282
348
|
|
|
283
|
-
return { slackApi, formatMessages };
|
|
349
|
+
return { slackApi, formatMessages, uploadSlackFiles };
|
|
284
350
|
}
|
|
285
351
|
|
|
286
352
|
function parsePermalink(url) {
|
|
@@ -333,6 +399,19 @@ const AddReactionSchema = z.object({
|
|
|
333
399
|
reaction: z.string().describe("Emoji name without colons."),
|
|
334
400
|
});
|
|
335
401
|
|
|
402
|
+
const UploadFilesSchema = z.object({
|
|
403
|
+
channel_id: z.string(),
|
|
404
|
+
file_paths: z.array(z.string().min(1)).min(1).max(10),
|
|
405
|
+
thread_ts: z.string().optional(),
|
|
406
|
+
initial_comment: z.string().optional(),
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
const EditMessageSchema = z.object({
|
|
410
|
+
channel_id: z.string(),
|
|
411
|
+
timestamp: z.string().describe("Timestamp of the message or thread reply to edit."),
|
|
412
|
+
text: z.string(),
|
|
413
|
+
});
|
|
414
|
+
|
|
336
415
|
// ── Tool definitions ─────────────────────────────────────────────────────────
|
|
337
416
|
|
|
338
417
|
const TOOLS = [
|
|
@@ -361,11 +440,21 @@ const TOOLS = [
|
|
|
361
440
|
description: "Add a reaction to a Slack message. Restricted to allowed channels.",
|
|
362
441
|
inputSchema: zodToJsonSchema(AddReactionSchema),
|
|
363
442
|
},
|
|
443
|
+
{
|
|
444
|
+
name: "slack_upload_files",
|
|
445
|
+
description: "Upload one or more local files to a channel or thread. Restricted to allowed channels.",
|
|
446
|
+
inputSchema: zodToJsonSchema(UploadFilesSchema),
|
|
447
|
+
},
|
|
448
|
+
{
|
|
449
|
+
name: "slack_edit_message",
|
|
450
|
+
description: "Edit an authored Slack message or thread reply by channel id and timestamp. Replaces the message text; any existing Block Kit blocks are removed by Slack. Restricted to allowed channels.",
|
|
451
|
+
inputSchema: zodToJsonSchema(EditMessageSchema),
|
|
452
|
+
},
|
|
364
453
|
];
|
|
365
454
|
|
|
366
455
|
// ── Execute factory (exported for tests) ─────────────────────────────────────
|
|
367
456
|
|
|
368
|
-
function createExecuteToolCall({ slackApi, formatMessages, allowedWriteChannels }) {
|
|
457
|
+
function createExecuteToolCall({ slackApi, formatMessages, uploadSlackFiles, allowedWriteChannels }) {
|
|
369
458
|
const allowed = new Set(allowedWriteChannels);
|
|
370
459
|
return async function executeToolCall(name, args) {
|
|
371
460
|
switch (name) {
|
|
@@ -423,6 +512,60 @@ function createExecuteToolCall({ slackApi, formatMessages, allowedWriteChannels
|
|
|
423
512
|
return { content: [{ type: "text", text: "added reaction" }] };
|
|
424
513
|
}
|
|
425
514
|
|
|
515
|
+
case "slack_upload_files": {
|
|
516
|
+
const a = UploadFilesSchema.parse(args);
|
|
517
|
+
const channelId = requireString(a.channel_id, "channel_id");
|
|
518
|
+
assertAllowedWriteChannel(channelId, allowed);
|
|
519
|
+
const files = await uploadSlackFiles({
|
|
520
|
+
filePaths: a.file_paths,
|
|
521
|
+
channelId,
|
|
522
|
+
threadTs: a.thread_ts && requireString(a.thread_ts, "thread_ts"),
|
|
523
|
+
initialComment: a.initial_comment,
|
|
524
|
+
});
|
|
525
|
+
return {
|
|
526
|
+
content: files.map((file) => ({
|
|
527
|
+
type: "text",
|
|
528
|
+
text: `uploaded file: ${file.title || file.name || file.id}\nfile permalink: ${file.permalink || "unavailable"}\nmessage permalink: ${file.message_permalink || "unavailable"}`,
|
|
529
|
+
})),
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
case "slack_edit_message": {
|
|
534
|
+
const a = EditMessageSchema.parse(args);
|
|
535
|
+
const channelId = requireString(a.channel_id, "channel_id");
|
|
536
|
+
assertAllowedWriteChannel(channelId, allowed);
|
|
537
|
+
const timestamp = requireString(a.timestamp, "timestamp");
|
|
538
|
+
const text = requireString(a.text, "text");
|
|
539
|
+
let updated;
|
|
540
|
+
try {
|
|
541
|
+
updated = await slackApi("chat.update", {
|
|
542
|
+
channel: channelId,
|
|
543
|
+
ts: timestamp,
|
|
544
|
+
text,
|
|
545
|
+
}, "POST");
|
|
546
|
+
} catch (error) {
|
|
547
|
+
const reason = String(error?.message ?? error).replace(/^slack chat\.update failed: /, "");
|
|
548
|
+
throw new Error(
|
|
549
|
+
`could not edit message ${timestamp} in ${channelId}: ${reason}. ` +
|
|
550
|
+
"Slack only allows editing messages authored by this account, within its edit window, and in a channel the caller can write to."
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
const messageTs = updated.ts || timestamp;
|
|
554
|
+
let permalink;
|
|
555
|
+
try {
|
|
556
|
+
const result = await slackApi("chat.getPermalink", {
|
|
557
|
+
channel: channelId,
|
|
558
|
+
message_ts: messageTs,
|
|
559
|
+
});
|
|
560
|
+
permalink = result.permalink;
|
|
561
|
+
} catch {
|
|
562
|
+
permalink = undefined;
|
|
563
|
+
}
|
|
564
|
+
const lines = [`edited message ${messageTs}`];
|
|
565
|
+
if (permalink) lines.push(`permalink: ${permalink}`);
|
|
566
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
567
|
+
}
|
|
568
|
+
|
|
426
569
|
default:
|
|
427
570
|
throw new Error(`Unknown tool: ${name}`);
|
|
428
571
|
}
|
|
@@ -449,14 +592,14 @@ async function main() {
|
|
|
449
592
|
process.exit(1);
|
|
450
593
|
}
|
|
451
594
|
|
|
452
|
-
const { slackApi, formatMessages } = createSlackClient({ xoxcToken, xoxdToken, userAgent: config.userAgent });
|
|
595
|
+
const { slackApi, formatMessages, uploadSlackFiles } = createSlackClient({ xoxcToken, xoxdToken, userAgent: config.userAgent });
|
|
453
596
|
|
|
454
597
|
const server = new Server(
|
|
455
598
|
{ name: "yt-slack-mcp", version: "0.1.0" },
|
|
456
599
|
{
|
|
457
600
|
capabilities: { tools: {} },
|
|
458
601
|
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).",
|
|
602
|
+
"MCP server for Slack. Read with slack_read_permalink, slack_get_replies, slack_get_history; write with slack_reply_to_thread, slack_add_reaction, slack_upload_files, slack_edit_message (restricted to allowed channels).",
|
|
460
603
|
}
|
|
461
604
|
);
|
|
462
605
|
|
|
@@ -465,6 +608,7 @@ async function main() {
|
|
|
465
608
|
const executeToolCall = createExecuteToolCall({
|
|
466
609
|
slackApi,
|
|
467
610
|
formatMessages,
|
|
611
|
+
uploadSlackFiles,
|
|
468
612
|
allowedWriteChannels: config.allowedWriteChannels,
|
|
469
613
|
});
|
|
470
614
|
|
|
@@ -509,4 +653,6 @@ export {
|
|
|
509
653
|
GetHistorySchema,
|
|
510
654
|
ReplyToThreadSchema,
|
|
511
655
|
AddReactionSchema,
|
|
656
|
+
UploadFilesSchema,
|
|
657
|
+
EditMessageSchema,
|
|
512
658
|
};
|
package/package.json
CHANGED
|
@@ -1,32 +1,32 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "yt-slack-mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "MCP server for Slack — read permalinks, threads, and channel history; reply to threads; add reactions.",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "index.js",
|
|
7
|
-
"bin": {
|
|
8
|
-
"yt-slack-mcp": "index.js"
|
|
9
|
-
},
|
|
10
|
-
"scripts": {
|
|
11
|
-
"test": "node --test --experimental-test-coverage"
|
|
12
|
-
},
|
|
13
|
-
"files": [
|
|
14
|
-
"index.js"
|
|
15
|
-
],
|
|
16
|
-
"keywords": [
|
|
17
|
-
"mcp",
|
|
18
|
-
"slack",
|
|
19
|
-
"model-context-protocol"
|
|
20
|
-
],
|
|
21
|
-
"license": "MIT",
|
|
22
|
-
"repository": {
|
|
23
|
-
"type": "git",
|
|
24
|
-
"url": "git+https://github.com/yentsun/slack-mcp.git"
|
|
25
|
-
},
|
|
26
|
-
"dependencies": {
|
|
27
|
-
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
28
|
-
"pdfjs-dist": "^4.0.0",
|
|
29
|
-
"zod": "^3.23.0",
|
|
30
|
-
"zod-to-json-schema": "^3.23.0"
|
|
31
|
-
}
|
|
32
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "yt-slack-mcp",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "MCP server for Slack — read permalinks, threads, and channel history; reply to threads; add reactions; edit messages.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"yt-slack-mcp": "index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"test": "node --test --experimental-test-coverage"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"index.js"
|
|
15
|
+
],
|
|
16
|
+
"keywords": [
|
|
17
|
+
"mcp",
|
|
18
|
+
"slack",
|
|
19
|
+
"model-context-protocol"
|
|
20
|
+
],
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "git+https://github.com/yentsun/slack-mcp.git"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
28
|
+
"pdfjs-dist": "^4.0.0",
|
|
29
|
+
"zod": "^3.23.0",
|
|
30
|
+
"zod-to-json-schema": "^3.23.0"
|
|
31
|
+
}
|
|
32
|
+
}
|