yt-slack-mcp 0.3.2 → 0.5.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 (3) hide show
  1. package/README.md +37 -7
  2. package/index.js +58 -7
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -8,7 +8,8 @@ A host-agnostic [Model Context Protocol](https://modelcontextprotocol.io) server
8
8
  - Read message permalinks from channels, DMs, and group DMs.
9
9
  - Return image attachments inline, extract text from PDFs, and return `text/*` attachment contents inline.
10
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.
11
+ - Reply to a thread, add or remove a reaction, edit an authored message or thread reply, or upload local files only in explicitly allowed channel IDs.
12
+ - Prefix every message the agent posts with `🤖:` — or `🤖 <model>:` when the call names the model — so agent replies stand apart from the account's own messages; an edit can opt out with `agent_authored: false`.
12
13
 
13
14
  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
 
@@ -19,10 +20,38 @@ Attachment limits: images and text files are limited to 10 MB; text attachments
19
20
  | `slack_read_permalink` | Read a message or thread from a channel, DM, or group-DM permalink, including supported attachments. |
20
21
  | `slack_get_replies` | Read replies for a thread by channel ID and timestamp. |
21
22
  | `slack_get_history` | Read recent channel history. |
22
- | `slack_reply_to_thread` | Reply to a thread; restricted to allowed channels. |
23
+ | `slack_reply_to_thread` | Reply to a thread, with optional `model`, in a `🤖:`-prefixed message; restricted to allowed channels. |
23
24
  | `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
+ | `slack_remove_reaction` | Remove a reaction added by this account; restricted to allowed channels. |
26
+ | `slack_upload_files` | Upload up to 10 local files to a channel or thread, with an optional `initial_comment` prefixed with `🤖:` and an optional `model` (only used with a comment); returns file and message permalinks; restricted to allowed channels. |
27
+ | `slack_edit_message` | Edit an authored message or thread reply by channel ID and timestamp; replaces the message text with `🤖:`-prefixed text and an optional `model`, or with the text as given when `agent_authored` is `false` (Slack removes any existing Block Kit blocks) and returns the updated timestamp and permalink; restricted to allowed channels. |
28
+
29
+ ## Agent message prefix
30
+
31
+ The write tools post through the account's own browser session, so Slack shows
32
+ the account name on them. To keep agent-written messages distinguishable, the
33
+ text of every posted message is prefixed with `🤖:` by default:
34
+
35
+ ```text
36
+ 🤖: deployed revision 4f2c1a9
37
+ ```
38
+
39
+ The MCP transport does not report which model is calling, so a write tool call
40
+ can pass `model` with the model it is running; the prefix then names it:
41
+
42
+ ```text
43
+ 🤖 claude-sonnet-4-5: deployed revision 4f2c1a9
44
+ ```
45
+
46
+ The prefix is added once: text that already starts with `🤖` is sent unchanged,
47
+ so editing a prefixed message does not stack prefixes. A file upload without an
48
+ `initial_comment` — or with only whitespace — gets no comment rather than a bare
49
+ prefix, and `model` has no effect then.
50
+
51
+ `slack_edit_message` can also replace a message with text that is not
52
+ agent-written, such as a correction to the user's own message: pass
53
+ `agent_authored: false` and the text is stored as given, without the prefix.
54
+ `model` has no effect when `agent_authored` is `false`.
26
55
 
27
56
  ## Setup
28
57
 
@@ -60,9 +89,10 @@ All configuration can live in a JSON config file at
60
89
  ```
61
90
 
62
91
  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
65
- missing file just means defaults. Env vars always override the config file.
92
+ `slack_remove_reaction`, `slack_edit_message`) and `slack_upload_files` are
93
+ locked down — no channel is writable. Each upload is limited to 10 MB. Every
94
+ field is optional; a missing file just means defaults. Env vars always override
95
+ the config file.
66
96
 
67
97
  ## Configuration
68
98
 
package/index.js CHANGED
@@ -370,8 +370,33 @@ function assertAllowedWriteChannel(channelId, allowed) {
370
370
  }
371
371
  }
372
372
 
373
+ const AGENT_PREFIX_EMOJI = "🤖";
374
+
375
+ // Slack shows the account's own name on everything this server posts, so mark
376
+ // agent-written text with a robot emoji and name the model that wrote it when
377
+ // the caller says which one it is running. Text is trimmed and blank text has no
378
+ // prefix to carry, so an optional comment stays absent. Text that already
379
+ // carries the prefix is returned unchanged, so editing a prefixed message does
380
+ // not stack prefixes.
381
+ function withAgentPrefix(text, model) {
382
+ const value = typeof text === "string" ? text.trim() : "";
383
+ if (!value) {
384
+ return undefined;
385
+ }
386
+ if (value.startsWith(AGENT_PREFIX_EMOJI)) {
387
+ return value;
388
+ }
389
+ const name = typeof model === "string" ? model.trim() : "";
390
+ return `${AGENT_PREFIX_EMOJI}${name ? ` ${name}` : ""}: ${value}`;
391
+ }
392
+
373
393
  // ── Schemas ─────────────────────────────────────────────────────────────────
374
394
 
395
+ const AgentModelField = z
396
+ .string()
397
+ .optional()
398
+ .describe("Model you are running, e.g. gpt-5.1, shown in the 🤖 prefix. Omit when unknown.");
399
+
375
400
  const ReadPermalinkSchema = z.object({
376
401
  url: z.string().describe("Slack permalink, e.g. https://<workspace>.slack.com/archives/C.../p..."),
377
402
  });
@@ -391,6 +416,7 @@ const ReplyToThreadSchema = z.object({
391
416
  channel_id: z.string(),
392
417
  thread_ts: z.string(),
393
418
  text: z.string(),
419
+ model: AgentModelField,
394
420
  });
395
421
 
396
422
  const AddReactionSchema = z.object({
@@ -404,12 +430,19 @@ const UploadFilesSchema = z.object({
404
430
  file_paths: z.array(z.string().min(1)).min(1).max(10),
405
431
  thread_ts: z.string().optional(),
406
432
  initial_comment: z.string().optional(),
433
+ model: AgentModelField,
407
434
  });
408
435
 
409
436
  const EditMessageSchema = z.object({
410
437
  channel_id: z.string(),
411
438
  timestamp: z.string().describe("Timestamp of the message or thread reply to edit."),
412
439
  text: z.string(),
440
+ agent_authored: z
441
+ .boolean()
442
+ .optional()
443
+ .default(true)
444
+ .describe("Set false when this text is not agent-written, for example correcting the user's own message, so it is stored without the 🤖 prefix; model is ignored when this is false."),
445
+ model: AgentModelField,
413
446
  });
414
447
 
415
448
  // ── Tool definitions ─────────────────────────────────────────────────────────
@@ -432,7 +465,7 @@ const TOOLS = [
432
465
  },
433
466
  {
434
467
  name: "slack_reply_to_thread",
435
- description: "Reply to a Slack thread. Restricted to allowed channels.",
468
+ description: "Reply to a Slack thread. The posted text is prefixed with `🤖:` — or `🤖 <model>:` when model is given — so readers can tell it came from an agent; pass model to name the model writing it. Restricted to allowed channels.",
436
469
  inputSchema: zodToJsonSchema(ReplyToThreadSchema),
437
470
  },
438
471
  {
@@ -440,14 +473,19 @@ const TOOLS = [
440
473
  description: "Add a reaction to a Slack message. Restricted to allowed channels.",
441
474
  inputSchema: zodToJsonSchema(AddReactionSchema),
442
475
  },
476
+ {
477
+ name: "slack_remove_reaction",
478
+ description: "Remove a reaction added by this account from a Slack message. Restricted to allowed channels.",
479
+ inputSchema: zodToJsonSchema(AddReactionSchema),
480
+ },
443
481
  {
444
482
  name: "slack_upload_files",
445
- description: "Upload one or more local files to a channel or thread. Restricted to allowed channels.",
483
+ description: "Upload one or more local files to a channel or thread. A given initial_comment is prefixed with `🤖:` — or `🤖 <model>:` when model is given — so readers can tell it came from an agent; pass model to name the model writing it, and note that model has no effect without initial_comment. Restricted to allowed channels.",
446
484
  inputSchema: zodToJsonSchema(UploadFilesSchema),
447
485
  },
448
486
  {
449
487
  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.",
488
+ description: "Edit an authored Slack message or thread reply by channel id and timestamp. Replaces the message text, prefixed with `🤖:` — or `🤖 <model>:` when model is given — unless it already carries the prefix; pass model to name the model writing it, and set agent_authored to false when the new text is not agent-written. Any existing Block Kit blocks are removed by Slack. Restricted to allowed channels.",
451
489
  inputSchema: zodToJsonSchema(EditMessageSchema),
452
490
  },
453
491
  ];
@@ -495,7 +533,7 @@ function createExecuteToolCall({ slackApi, formatMessages, uploadSlackFiles, all
495
533
  const response = await slackApi("chat.postMessage", {
496
534
  channel: channelId,
497
535
  thread_ts: requireString(a.thread_ts, "thread_ts"),
498
- text: requireString(a.text, "text"),
536
+ text: withAgentPrefix(requireString(a.text, "text"), a.model),
499
537
  }, "POST");
500
538
  return { content: [{ type: "text", text: `posted reply at ${response.ts}` }] };
501
539
  }
@@ -512,6 +550,18 @@ function createExecuteToolCall({ slackApi, formatMessages, uploadSlackFiles, all
512
550
  return { content: [{ type: "text", text: "added reaction" }] };
513
551
  }
514
552
 
553
+ case "slack_remove_reaction": {
554
+ const a = AddReactionSchema.parse(args);
555
+ const channelId = requireString(a.channel_id, "channel_id");
556
+ assertAllowedWriteChannel(channelId, allowed);
557
+ await slackApi("reactions.remove", {
558
+ channel: channelId,
559
+ timestamp: requireString(a.timestamp, "timestamp"),
560
+ name: requireString(a.reaction, "reaction"),
561
+ }, "POST");
562
+ return { content: [{ type: "text", text: "removed reaction" }] };
563
+ }
564
+
515
565
  case "slack_upload_files": {
516
566
  const a = UploadFilesSchema.parse(args);
517
567
  const channelId = requireString(a.channel_id, "channel_id");
@@ -520,7 +570,7 @@ function createExecuteToolCall({ slackApi, formatMessages, uploadSlackFiles, all
520
570
  filePaths: a.file_paths,
521
571
  channelId,
522
572
  threadTs: a.thread_ts && requireString(a.thread_ts, "thread_ts"),
523
- initialComment: a.initial_comment,
573
+ initialComment: withAgentPrefix(a.initial_comment, a.model),
524
574
  });
525
575
  return {
526
576
  content: files.map((file) => ({
@@ -541,7 +591,7 @@ function createExecuteToolCall({ slackApi, formatMessages, uploadSlackFiles, all
541
591
  updated = await slackApi("chat.update", {
542
592
  channel: channelId,
543
593
  ts: timestamp,
544
- text,
594
+ text: a.agent_authored ? withAgentPrefix(text, a.model) : text,
545
595
  }, "POST");
546
596
  } catch (error) {
547
597
  const reason = String(error?.message ?? error).replace(/^slack chat\.update failed: /, "");
@@ -599,7 +649,7 @@ async function main() {
599
649
  {
600
650
  capabilities: { tools: {} },
601
651
  instructions:
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).",
652
+ "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_remove_reaction, slack_upload_files, slack_edit_message (restricted to allowed channels). Messages sent by the write tools are prefixed with `🤖:` by default, and slack_edit_message can opt out with agent_authored false; pass model with the model you are running to include it in the prefix.",
603
653
  }
604
654
  );
605
655
 
@@ -644,6 +694,7 @@ export {
644
694
  parsePermalink,
645
695
  requireString,
646
696
  assertAllowedWriteChannel,
697
+ withAgentPrefix,
647
698
  createExecuteToolCall,
648
699
  createSlackClient,
649
700
  loadConfig,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "yt-slack-mcp",
3
- "version": "0.3.2",
4
- "description": "MCP server for Slack — read permalinks, threads, and channel history; reply to threads; add reactions; edit messages.",
3
+ "version": "0.5.0",
4
+ "description": "MCP server for Slack — read permalinks, threads, and channel history; reply to threads; add or remove reactions; edit messages.",
5
5
  "type": "module",
6
6
  "main": "index.js",
7
7
  "bin": {