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.
Files changed (4) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +49 -47
  3. package/index.js +180 -124
  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,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 writable. Each upload is limited to 10 MB. 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
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
- 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
- ];
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.2.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
+ }