yt-slack-mcp 0.2.0 → 0.3.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/LICENSE +21 -21
- package/README.md +49 -47
- package/index.js +180 -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
|
|
|
@@ -413,37 +419,49 @@ const TOOLS = [
|
|
|
413
419
|
name: "slack_read_permalink",
|
|
414
420
|
description: "Read a Slack message or thread from a permalink, including inline text files, extracted PDFs, and images.",
|
|
415
421
|
inputSchema: zodToJsonSchema(ReadPermalinkSchema),
|
|
422
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
416
423
|
},
|
|
417
424
|
{
|
|
418
425
|
name: "slack_get_replies",
|
|
419
426
|
description: "Read replies for a Slack thread by channel id and thread timestamp.",
|
|
420
427
|
inputSchema: zodToJsonSchema(GetRepliesSchema),
|
|
428
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
421
429
|
},
|
|
422
430
|
{
|
|
423
431
|
name: "slack_get_history",
|
|
424
432
|
description: "Read recent Slack channel history.",
|
|
425
433
|
inputSchema: zodToJsonSchema(GetHistorySchema),
|
|
434
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
426
435
|
},
|
|
427
436
|
{
|
|
428
437
|
name: "slack_reply_to_thread",
|
|
429
438
|
description: "Reply to a Slack thread. Restricted to allowed channels.",
|
|
430
439
|
inputSchema: zodToJsonSchema(ReplyToThreadSchema),
|
|
440
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
431
441
|
},
|
|
432
|
-
{
|
|
433
|
-
name: "slack_add_reaction",
|
|
442
|
+
{
|
|
443
|
+
name: "slack_add_reaction",
|
|
434
444
|
description: "Add a reaction to a Slack message. Restricted to allowed channels.",
|
|
435
|
-
inputSchema: zodToJsonSchema(AddReactionSchema),
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
445
|
+
inputSchema: zodToJsonSchema(AddReactionSchema),
|
|
446
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
447
|
+
},
|
|
448
|
+
{
|
|
449
|
+
name: "slack_upload_files",
|
|
450
|
+
description: "Upload one or more local files to a channel or thread. Restricted to allowed channels.",
|
|
451
|
+
inputSchema: zodToJsonSchema(UploadFilesSchema),
|
|
452
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
453
|
+
},
|
|
454
|
+
{
|
|
455
|
+
name: "slack_edit_message",
|
|
456
|
+
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.",
|
|
457
|
+
inputSchema: zodToJsonSchema(EditMessageSchema),
|
|
458
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
459
|
+
},
|
|
460
|
+
];
|
|
443
461
|
|
|
444
462
|
// ── Execute factory (exported for tests) ─────────────────────────────────────
|
|
445
463
|
|
|
446
|
-
function createExecuteToolCall({ slackApi, formatMessages, uploadSlackFiles, allowedWriteChannels }) {
|
|
464
|
+
function createExecuteToolCall({ slackApi, formatMessages, uploadSlackFiles, allowedWriteChannels }) {
|
|
447
465
|
const allowed = new Set(allowedWriteChannels);
|
|
448
466
|
return async function executeToolCall(name, args) {
|
|
449
467
|
switch (name) {
|
|
@@ -489,7 +507,7 @@ function createExecuteToolCall({ slackApi, formatMessages, uploadSlackFiles, all
|
|
|
489
507
|
return { content: [{ type: "text", text: `posted reply at ${response.ts}` }] };
|
|
490
508
|
}
|
|
491
509
|
|
|
492
|
-
case "slack_add_reaction": {
|
|
510
|
+
case "slack_add_reaction": {
|
|
493
511
|
const a = AddReactionSchema.parse(args);
|
|
494
512
|
const channelId = requireString(a.channel_id, "channel_id");
|
|
495
513
|
assertAllowedWriteChannel(channelId, allowed);
|
|
@@ -498,26 +516,62 @@ function createExecuteToolCall({ slackApi, formatMessages, uploadSlackFiles, all
|
|
|
498
516
|
timestamp: requireString(a.timestamp, "timestamp"),
|
|
499
517
|
name: requireString(a.reaction, "reaction"),
|
|
500
518
|
}, "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
|
-
}
|
|
519
|
+
return { content: [{ type: "text", text: "added reaction" }] };
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
case "slack_upload_files": {
|
|
523
|
+
const a = UploadFilesSchema.parse(args);
|
|
524
|
+
const channelId = requireString(a.channel_id, "channel_id");
|
|
525
|
+
assertAllowedWriteChannel(channelId, allowed);
|
|
526
|
+
const files = await uploadSlackFiles({
|
|
527
|
+
filePaths: a.file_paths,
|
|
528
|
+
channelId,
|
|
529
|
+
threadTs: a.thread_ts && requireString(a.thread_ts, "thread_ts"),
|
|
530
|
+
initialComment: a.initial_comment,
|
|
531
|
+
});
|
|
532
|
+
return {
|
|
533
|
+
content: files.map((file) => ({
|
|
534
|
+
type: "text",
|
|
535
|
+
text: `uploaded file: ${file.title || file.name || file.id}\nfile permalink: ${file.permalink || "unavailable"}\nmessage permalink: ${file.message_permalink || "unavailable"}`,
|
|
536
|
+
})),
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
case "slack_edit_message": {
|
|
541
|
+
const a = EditMessageSchema.parse(args);
|
|
542
|
+
const channelId = requireString(a.channel_id, "channel_id");
|
|
543
|
+
assertAllowedWriteChannel(channelId, allowed);
|
|
544
|
+
const timestamp = requireString(a.timestamp, "timestamp");
|
|
545
|
+
const text = requireString(a.text, "text");
|
|
546
|
+
let updated;
|
|
547
|
+
try {
|
|
548
|
+
updated = await slackApi("chat.update", {
|
|
549
|
+
channel: channelId,
|
|
550
|
+
ts: timestamp,
|
|
551
|
+
text,
|
|
552
|
+
}, "POST");
|
|
553
|
+
} catch (error) {
|
|
554
|
+
const reason = String(error?.message ?? error).replace(/^slack chat\.update failed: /, "");
|
|
555
|
+
throw new Error(
|
|
556
|
+
`could not edit message ${timestamp} in ${channelId}: ${reason}. ` +
|
|
557
|
+
"Slack only allows editing messages authored by this account, within its edit window, and in a channel the caller can write to."
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
const messageTs = updated.ts || timestamp;
|
|
561
|
+
let permalink;
|
|
562
|
+
try {
|
|
563
|
+
const result = await slackApi("chat.getPermalink", {
|
|
564
|
+
channel: channelId,
|
|
565
|
+
message_ts: messageTs,
|
|
566
|
+
});
|
|
567
|
+
permalink = result.permalink;
|
|
568
|
+
} catch {
|
|
569
|
+
permalink = undefined;
|
|
570
|
+
}
|
|
571
|
+
const lines = [`edited message ${messageTs}`];
|
|
572
|
+
if (permalink) lines.push(`permalink: ${permalink}`);
|
|
573
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
574
|
+
}
|
|
521
575
|
|
|
522
576
|
default:
|
|
523
577
|
throw new Error(`Unknown tool: ${name}`);
|
|
@@ -545,23 +599,23 @@ async function main() {
|
|
|
545
599
|
process.exit(1);
|
|
546
600
|
}
|
|
547
601
|
|
|
548
|
-
const { slackApi, formatMessages, uploadSlackFiles } = createSlackClient({ xoxcToken, xoxdToken, userAgent: config.userAgent });
|
|
602
|
+
const { slackApi, formatMessages, uploadSlackFiles } = createSlackClient({ xoxcToken, xoxdToken, userAgent: config.userAgent });
|
|
549
603
|
|
|
550
604
|
const server = new Server(
|
|
551
605
|
{ name: "yt-slack-mcp", version: "0.1.0" },
|
|
552
606
|
{
|
|
553
607
|
capabilities: { tools: {} },
|
|
554
608
|
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).",
|
|
609
|
+
"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
610
|
}
|
|
557
611
|
);
|
|
558
612
|
|
|
559
613
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
|
|
560
614
|
|
|
561
615
|
const executeToolCall = createExecuteToolCall({
|
|
562
|
-
slackApi,
|
|
563
|
-
formatMessages,
|
|
564
|
-
uploadSlackFiles,
|
|
616
|
+
slackApi,
|
|
617
|
+
formatMessages,
|
|
618
|
+
uploadSlackFiles,
|
|
565
619
|
allowedWriteChannels: config.allowedWriteChannels,
|
|
566
620
|
});
|
|
567
621
|
|
|
@@ -593,6 +647,7 @@ if (isMain) {
|
|
|
593
647
|
}
|
|
594
648
|
|
|
595
649
|
export {
|
|
650
|
+
TOOLS,
|
|
596
651
|
parsePermalink,
|
|
597
652
|
requireString,
|
|
598
653
|
assertAllowedWriteChannel,
|
|
@@ -605,6 +660,7 @@ export {
|
|
|
605
660
|
GetRepliesSchema,
|
|
606
661
|
GetHistorySchema,
|
|
607
662
|
ReplyToThreadSchema,
|
|
608
|
-
AddReactionSchema,
|
|
609
|
-
UploadFilesSchema,
|
|
610
|
-
|
|
663
|
+
AddReactionSchema,
|
|
664
|
+
UploadFilesSchema,
|
|
665
|
+
EditMessageSchema,
|
|
666
|
+
};
|
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.1",
|
|
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
|
+
}
|