yt-slack-mcp 0.2.0 → 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 -47
- package/index.js +172 -124
- 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,27 +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, add a reaction, 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. |
|
|
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. |
|
|
25
26
|
|
|
26
27
|
## Setup
|
|
27
28
|
|
|
@@ -32,16 +33,16 @@ Attachment limits: images and text files are limited to 10 MB; text attachments
|
|
|
32
33
|
|
|
33
34
|
These may need refreshing when Slack rotates/invalidates the session.
|
|
34
35
|
|
|
35
|
-
### 2. Store the secrets
|
|
36
|
+
### 2. Store the secrets
|
|
36
37
|
|
|
37
38
|
Save each token as a single-line file:
|
|
38
39
|
|
|
39
|
-
```
|
|
40
|
-
~/.config/yt-slack-mcp/secrets/slack-xoxc.txt
|
|
41
|
-
~/.config/yt-slack-mcp/secrets/slack-xoxd.txt
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
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.
|
|
45
46
|
|
|
46
47
|
### 3. (Optional) Configure allowed write channels and paths
|
|
47
48
|
|
|
@@ -49,17 +50,18 @@ All configuration can live in a JSON config file at
|
|
|
49
50
|
`~/.config/yt-slack-mcp/config.json` (override with `SLACK_MCP_CONFIG`):
|
|
50
51
|
|
|
51
52
|
```json
|
|
52
|
-
{
|
|
53
|
-
"secretsDir": "~/.config/yt-slack-mcp/secrets",
|
|
54
|
-
"xoxcPath": "~/.config/yt-slack-mcp/secrets/slack-xoxc.txt",
|
|
55
|
-
"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",
|
|
56
57
|
"allowedWriteChannels": ["C0123456789", "C0987654321"],
|
|
57
58
|
"userAgent": "Mozilla/5.0"
|
|
58
59
|
}
|
|
59
60
|
```
|
|
60
61
|
|
|
61
|
-
By default the write tools (`slack_reply_to_thread`, `slack_add_reaction
|
|
62
|
-
and `slack_upload_files` 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
|
|
63
65
|
missing file just means defaults. Env vars always override the config file.
|
|
64
66
|
|
|
65
67
|
## Configuration
|
|
@@ -68,22 +70,22 @@ Config resolution order: built-in defaults → config file → env vars.
|
|
|
68
70
|
|
|
69
71
|
| Config file key | Env var | Default | Description |
|
|
70
72
|
|-----------------|---------|---------|-------------|
|
|
71
|
-
| `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 |
|
|
72
74
|
| `xoxcPath` | `SLACK_MCP_XOXC_PATH` | `$SECRETS_DIR/slack-xoxc.txt` | Path to the `xoxc` token |
|
|
73
75
|
| `xoxdPath` | `SLACK_MCP_XOXD_PATH` | `$SECRETS_DIR/slack-xoxd.txt` | Path to the `xoxd` token |
|
|
74
76
|
| `allowedWriteChannels` | `SLACK_MCP_ALLOWED_WRITE_CHANNELS` | *(none)* | Channel IDs allowed for write tools (comma-separated in env) |
|
|
75
77
|
| `userAgent` | `SLACK_MCP_USER_AGENT` | `Mozilla/5.0` | HTTP `User-Agent` header |
|
|
76
78
|
| *(config file path)* | `SLACK_MCP_CONFIG` | `~/.config/yt-slack-mcp/config.json` | Path to the JSON config file |
|
|
77
79
|
|
|
78
|
-
## MCP host configuration
|
|
79
|
-
|
|
80
|
-
Configure your MCP host to run `yt-slack-mcp` over stdio. The host-specific configuration varies, but the command is:
|
|
81
|
-
|
|
82
|
-
```
|
|
83
|
-
npx yt-slack-mcp
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
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:
|
|
87
89
|
|
|
88
90
|
```json
|
|
89
91
|
{
|
package/index.js
CHANGED
|
@@ -18,16 +18,16 @@ 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;
|
|
25
25
|
const maxPdfBytes = 20 * 1024 * 1024;
|
|
26
26
|
const maxPdfPages = 50;
|
|
27
27
|
const maxPdfTextCharacters = 100_000;
|
|
28
|
-
const maxTextFileBytes = 10 * 1024 * 1024;
|
|
29
|
-
const maxTextFileCharacters = 100_000;
|
|
30
|
-
const maxUploadBytes = 10 * 1024 * 1024;
|
|
28
|
+
const maxTextFileBytes = 10 * 1024 * 1024;
|
|
29
|
+
const maxTextFileCharacters = 100_000;
|
|
30
|
+
const maxUploadBytes = 10 * 1024 * 1024;
|
|
31
31
|
|
|
32
32
|
function expandHome(filePath) {
|
|
33
33
|
if (!filePath) {
|
|
@@ -111,8 +111,8 @@ function createSlackClient({ xoxcToken, xoxdToken, userAgent = "Mozilla/5.0" })
|
|
|
111
111
|
return data;
|
|
112
112
|
};
|
|
113
113
|
|
|
114
|
-
const getSlackFileData = async (file, { refresh = false } = {}) => {
|
|
115
|
-
if (!refresh && (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)) {
|
|
116
116
|
return file;
|
|
117
117
|
}
|
|
118
118
|
const response = await slackApi("files.info", { file: file.id });
|
|
@@ -192,7 +192,7 @@ function createSlackClient({ xoxcToken, xoxdToken, userAgent = "Mozilla/5.0" })
|
|
|
192
192
|
return extractPdfText(pdfData);
|
|
193
193
|
};
|
|
194
194
|
|
|
195
|
-
const downloadSlackText = async (file) => {
|
|
195
|
+
const downloadSlackText = async (file) => {
|
|
196
196
|
const fileData = await getSlackFileData(file);
|
|
197
197
|
const textUrl = fileData.url_private_download || fileData.url_private;
|
|
198
198
|
if (!textUrl) {
|
|
@@ -227,73 +227,73 @@ function createSlackClient({ xoxcToken, xoxdToken, userAgent = "Mozilla/5.0" })
|
|
|
227
227
|
if (text.length > maxTextFileCharacters) {
|
|
228
228
|
text = `${text.slice(0, maxTextFileCharacters)}\n\n[Text file truncated at ${maxTextFileCharacters} characters]`;
|
|
229
229
|
}
|
|
230
|
-
return text;
|
|
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
|
-
};
|
|
230
|
+
return text;
|
|
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
297
|
|
|
298
298
|
const formatMessages = async (messages) => {
|
|
299
299
|
if (!messages.length) {
|
|
@@ -346,7 +346,7 @@ function createSlackClient({ xoxcToken, xoxdToken, userAgent = "Mozilla/5.0" })
|
|
|
346
346
|
return content;
|
|
347
347
|
};
|
|
348
348
|
|
|
349
|
-
return { slackApi, formatMessages, uploadSlackFiles };
|
|
349
|
+
return { slackApi, formatMessages, uploadSlackFiles };
|
|
350
350
|
}
|
|
351
351
|
|
|
352
352
|
function parsePermalink(url) {
|
|
@@ -393,18 +393,24 @@ const ReplyToThreadSchema = z.object({
|
|
|
393
393
|
text: z.string(),
|
|
394
394
|
});
|
|
395
395
|
|
|
396
|
-
const AddReactionSchema = z.object({
|
|
396
|
+
const AddReactionSchema = z.object({
|
|
397
397
|
channel_id: z.string(),
|
|
398
398
|
timestamp: z.string(),
|
|
399
399
|
reaction: z.string().describe("Emoji name without colons."),
|
|
400
|
-
});
|
|
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
|
-
});
|
|
400
|
+
});
|
|
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
|
+
});
|
|
408
414
|
|
|
409
415
|
// ── Tool definitions ─────────────────────────────────────────────────────────
|
|
410
416
|
|
|
@@ -429,21 +435,26 @@ const TOOLS = [
|
|
|
429
435
|
description: "Reply to a Slack thread. Restricted to allowed channels.",
|
|
430
436
|
inputSchema: zodToJsonSchema(ReplyToThreadSchema),
|
|
431
437
|
},
|
|
432
|
-
{
|
|
433
|
-
name: "slack_add_reaction",
|
|
438
|
+
{
|
|
439
|
+
name: "slack_add_reaction",
|
|
434
440
|
description: "Add a reaction to a Slack message. Restricted to allowed channels.",
|
|
435
|
-
inputSchema: zodToJsonSchema(AddReactionSchema),
|
|
436
|
-
},
|
|
437
|
-
{
|
|
438
|
-
name: "slack_upload_files",
|
|
439
|
-
description: "Upload one or more local files to a channel or thread. Restricted to allowed channels.",
|
|
440
|
-
inputSchema: zodToJsonSchema(UploadFilesSchema),
|
|
441
|
-
},
|
|
442
|
-
|
|
441
|
+
inputSchema: zodToJsonSchema(AddReactionSchema),
|
|
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
|
+
},
|
|
453
|
+
];
|
|
443
454
|
|
|
444
455
|
// ── Execute factory (exported for tests) ─────────────────────────────────────
|
|
445
456
|
|
|
446
|
-
function createExecuteToolCall({ slackApi, formatMessages, uploadSlackFiles, allowedWriteChannels }) {
|
|
457
|
+
function createExecuteToolCall({ slackApi, formatMessages, uploadSlackFiles, allowedWriteChannels }) {
|
|
447
458
|
const allowed = new Set(allowedWriteChannels);
|
|
448
459
|
return async function executeToolCall(name, args) {
|
|
449
460
|
switch (name) {
|
|
@@ -489,7 +500,7 @@ function createExecuteToolCall({ slackApi, formatMessages, uploadSlackFiles, all
|
|
|
489
500
|
return { content: [{ type: "text", text: `posted reply at ${response.ts}` }] };
|
|
490
501
|
}
|
|
491
502
|
|
|
492
|
-
case "slack_add_reaction": {
|
|
503
|
+
case "slack_add_reaction": {
|
|
493
504
|
const a = AddReactionSchema.parse(args);
|
|
494
505
|
const channelId = requireString(a.channel_id, "channel_id");
|
|
495
506
|
assertAllowedWriteChannel(channelId, allowed);
|
|
@@ -498,26 +509,62 @@ function createExecuteToolCall({ slackApi, formatMessages, uploadSlackFiles, all
|
|
|
498
509
|
timestamp: requireString(a.timestamp, "timestamp"),
|
|
499
510
|
name: requireString(a.reaction, "reaction"),
|
|
500
511
|
}, "POST");
|
|
501
|
-
return { content: [{ type: "text", text: "added reaction" }] };
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
case "slack_upload_files": {
|
|
505
|
-
const a = UploadFilesSchema.parse(args);
|
|
506
|
-
const channelId = requireString(a.channel_id, "channel_id");
|
|
507
|
-
assertAllowedWriteChannel(channelId, allowed);
|
|
508
|
-
const files = await uploadSlackFiles({
|
|
509
|
-
filePaths: a.file_paths,
|
|
510
|
-
channelId,
|
|
511
|
-
threadTs: a.thread_ts && requireString(a.thread_ts, "thread_ts"),
|
|
512
|
-
initialComment: a.initial_comment,
|
|
513
|
-
});
|
|
514
|
-
return {
|
|
515
|
-
content: files.map((file) => ({
|
|
516
|
-
type: "text",
|
|
517
|
-
text: `uploaded file: ${file.title || file.name || file.id}\nfile permalink: ${file.permalink || "unavailable"}\nmessage permalink: ${file.message_permalink || "unavailable"}`,
|
|
518
|
-
})),
|
|
519
|
-
};
|
|
520
|
-
}
|
|
512
|
+
return { content: [{ type: "text", text: "added reaction" }] };
|
|
513
|
+
}
|
|
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
|
+
}
|
|
521
568
|
|
|
522
569
|
default:
|
|
523
570
|
throw new Error(`Unknown tool: ${name}`);
|
|
@@ -545,23 +592,23 @@ async function main() {
|
|
|
545
592
|
process.exit(1);
|
|
546
593
|
}
|
|
547
594
|
|
|
548
|
-
const { slackApi, formatMessages, uploadSlackFiles } = createSlackClient({ xoxcToken, xoxdToken, userAgent: config.userAgent });
|
|
595
|
+
const { slackApi, formatMessages, uploadSlackFiles } = createSlackClient({ xoxcToken, xoxdToken, userAgent: config.userAgent });
|
|
549
596
|
|
|
550
597
|
const server = new Server(
|
|
551
598
|
{ name: "yt-slack-mcp", version: "0.1.0" },
|
|
552
599
|
{
|
|
553
600
|
capabilities: { tools: {} },
|
|
554
601
|
instructions:
|
|
555
|
-
"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 (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).",
|
|
556
603
|
}
|
|
557
604
|
);
|
|
558
605
|
|
|
559
606
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
|
|
560
607
|
|
|
561
608
|
const executeToolCall = createExecuteToolCall({
|
|
562
|
-
slackApi,
|
|
563
|
-
formatMessages,
|
|
564
|
-
uploadSlackFiles,
|
|
609
|
+
slackApi,
|
|
610
|
+
formatMessages,
|
|
611
|
+
uploadSlackFiles,
|
|
565
612
|
allowedWriteChannels: config.allowedWriteChannels,
|
|
566
613
|
});
|
|
567
614
|
|
|
@@ -605,6 +652,7 @@ export {
|
|
|
605
652
|
GetRepliesSchema,
|
|
606
653
|
GetHistorySchema,
|
|
607
654
|
ReplyToThreadSchema,
|
|
608
|
-
AddReactionSchema,
|
|
609
|
-
UploadFilesSchema,
|
|
610
|
-
|
|
655
|
+
AddReactionSchema,
|
|
656
|
+
UploadFilesSchema,
|
|
657
|
+
EditMessageSchema,
|
|
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
|
+
}
|