yt-slack-mcp 0.1.1 → 0.2.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 +4 -3
  2. package/index.js +123 -25
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -8,7 +8,7 @@ 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 or add a reaction only in explicitly allowed channel IDs.
11
+ - Reply to a thread, add a reaction, or upload local files only in explicitly allowed channel IDs.
12
12
 
13
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
14
 
@@ -21,6 +21,7 @@ Attachment limits: images and text files are limited to 10 MB; text attachments
21
21
  | `slack_get_history` | Read recent channel history. |
22
22
  | `slack_reply_to_thread` | Reply to a thread; restricted to allowed channels. |
23
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. |
24
25
 
25
26
  ## Setup
26
27
 
@@ -57,8 +58,8 @@ All configuration can live in a JSON config file at
57
58
  }
58
59
  ```
59
60
 
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
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
63
  missing file just means defaults. Env vars always override the config file.
63
64
 
64
65
  ## Configuration
package/index.js CHANGED
@@ -25,8 +25,9 @@ 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;
28
+ const maxTextFileBytes = 10 * 1024 * 1024;
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 });
@@ -191,7 +192,7 @@ function createSlackClient({ xoxcToken, xoxdToken, userAgent = "Mozilla/5.0" })
191
192
  return extractPdfText(pdfData);
192
193
  };
193
194
 
194
- const downloadSlackText = async (file) => {
195
+ const downloadSlackText = async (file) => {
195
196
  const fileData = await getSlackFileData(file);
196
197
  const textUrl = fileData.url_private_download || fileData.url_private;
197
198
  if (!textUrl) {
@@ -226,8 +227,73 @@ function createSlackClient({ xoxcToken, xoxdToken, userAgent = "Mozilla/5.0" })
226
227
  if (text.length > maxTextFileCharacters) {
227
228
  text = `${text.slice(0, maxTextFileCharacters)}\n\n[Text file truncated at ${maxTextFileCharacters} characters]`;
228
229
  }
229
- return text;
230
- };
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
+ };
231
297
 
232
298
  const formatMessages = async (messages) => {
233
299
  if (!messages.length) {
@@ -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) {
@@ -327,11 +393,18 @@ const ReplyToThreadSchema = z.object({
327
393
  text: z.string(),
328
394
  });
329
395
 
330
- const AddReactionSchema = z.object({
396
+ const AddReactionSchema = z.object({
331
397
  channel_id: z.string(),
332
398
  timestamp: z.string(),
333
399
  reaction: z.string().describe("Emoji name without colons."),
334
- });
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
+ });
335
408
 
336
409
  // ── Tool definitions ─────────────────────────────────────────────────────────
337
410
 
@@ -356,16 +429,21 @@ const TOOLS = [
356
429
  description: "Reply to a Slack thread. Restricted to allowed channels.",
357
430
  inputSchema: zodToJsonSchema(ReplyToThreadSchema),
358
431
  },
359
- {
360
- name: "slack_add_reaction",
432
+ {
433
+ name: "slack_add_reaction",
361
434
  description: "Add a reaction to a Slack message. Restricted to allowed channels.",
362
- inputSchema: zodToJsonSchema(AddReactionSchema),
363
- },
364
- ];
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
+ ];
365
443
 
366
444
  // ── Execute factory (exported for tests) ─────────────────────────────────────
367
445
 
368
- function createExecuteToolCall({ slackApi, formatMessages, allowedWriteChannels }) {
446
+ function createExecuteToolCall({ slackApi, formatMessages, uploadSlackFiles, allowedWriteChannels }) {
369
447
  const allowed = new Set(allowedWriteChannels);
370
448
  return async function executeToolCall(name, args) {
371
449
  switch (name) {
@@ -411,7 +489,7 @@ function createExecuteToolCall({ slackApi, formatMessages, allowedWriteChannels
411
489
  return { content: [{ type: "text", text: `posted reply at ${response.ts}` }] };
412
490
  }
413
491
 
414
- case "slack_add_reaction": {
492
+ case "slack_add_reaction": {
415
493
  const a = AddReactionSchema.parse(args);
416
494
  const channelId = requireString(a.channel_id, "channel_id");
417
495
  assertAllowedWriteChannel(channelId, allowed);
@@ -420,8 +498,26 @@ function createExecuteToolCall({ slackApi, formatMessages, allowedWriteChannels
420
498
  timestamp: requireString(a.timestamp, "timestamp"),
421
499
  name: requireString(a.reaction, "reaction"),
422
500
  }, "POST");
423
- return { content: [{ type: "text", text: "added reaction" }] };
424
- }
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
+ }
425
521
 
426
522
  default:
427
523
  throw new Error(`Unknown tool: ${name}`);
@@ -449,22 +545,23 @@ async function main() {
449
545
  process.exit(1);
450
546
  }
451
547
 
452
- const { slackApi, formatMessages } = createSlackClient({ xoxcToken, xoxdToken, userAgent: config.userAgent });
548
+ const { slackApi, formatMessages, uploadSlackFiles } = createSlackClient({ xoxcToken, xoxdToken, userAgent: config.userAgent });
453
549
 
454
550
  const server = new Server(
455
551
  { name: "yt-slack-mcp", version: "0.1.0" },
456
552
  {
457
553
  capabilities: { tools: {} },
458
554
  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).",
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).",
460
556
  }
461
557
  );
462
558
 
463
559
  server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
464
560
 
465
561
  const executeToolCall = createExecuteToolCall({
466
- slackApi,
467
- formatMessages,
562
+ slackApi,
563
+ formatMessages,
564
+ uploadSlackFiles,
468
565
  allowedWriteChannels: config.allowedWriteChannels,
469
566
  });
470
567
 
@@ -508,5 +605,6 @@ export {
508
605
  GetRepliesSchema,
509
606
  GetHistorySchema,
510
607
  ReplyToThreadSchema,
511
- AddReactionSchema,
512
- };
608
+ AddReactionSchema,
609
+ UploadFilesSchema,
610
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yt-slack-mcp",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "MCP server for Slack — read permalinks, threads, and channel history; reply to threads; add reactions.",
5
5
  "type": "module",
6
6
  "main": "index.js",