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.
Files changed (4) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +49 -46
  3. package/index.js +153 -7
  4. 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 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.
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 writable. Every field is optional; a
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.1.1",
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
+ }