slack-workspace-mcp-server 0.0.2 → 0.0.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slack-workspace-mcp-server",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "description": "MCP server for Slack workspace integration",
5
5
  "main": "build/index.js",
6
6
  "type": "module",
@@ -1,5 +1,5 @@
1
1
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
- import type { Channel, Message } from './types.js';
2
+ import type { Channel, Message, SlackFile } from './types.js';
3
3
  /**
4
4
  * Slack API client interface
5
5
  * Defines all methods for interacting with the Slack Web API
@@ -55,6 +55,14 @@ export interface ISlackClient {
55
55
  * Add a reaction to a message
56
56
  */
57
57
  addReaction(channelId: string, timestamp: string, emoji: string): Promise<void>;
58
+ /**
59
+ * Get file metadata from Slack
60
+ */
61
+ getFileInfo(fileId: string): Promise<SlackFile>;
62
+ /**
63
+ * Download a file from Slack (requires authenticated URL)
64
+ */
65
+ downloadFile(fileUrl: string): Promise<Buffer>;
58
66
  }
59
67
  /**
60
68
  * Slack API client implementation
@@ -93,6 +101,8 @@ export declare class SlackClient implements ISlackClient {
93
101
  }): Promise<Message>;
94
102
  updateMessage(channelId: string, ts: string, text: string): Promise<Message>;
95
103
  addReaction(channelId: string, timestamp: string, emoji: string): Promise<void>;
104
+ getFileInfo(fileId: string): Promise<SlackFile>;
105
+ downloadFile(fileUrl: string): Promise<Buffer>;
96
106
  }
97
107
  export type ClientFactory = () => ISlackClient;
98
108
  export interface CreateMCPServerOptions {
package/shared/server.js CHANGED
@@ -41,6 +41,14 @@ export class SlackClient {
41
41
  const { addReaction } = await import('./slack-client/lib/add-reaction.js');
42
42
  return addReaction(this.baseUrl, this.headers, channelId, timestamp, emoji);
43
43
  }
44
+ async getFileInfo(fileId) {
45
+ const { getFileInfo } = await import('./slack-client/lib/get-file-info.js');
46
+ return getFileInfo(this.baseUrl, this.headers, fileId);
47
+ }
48
+ async downloadFile(fileUrl) {
49
+ const { downloadFile } = await import('./slack-client/lib/download-file.js');
50
+ return downloadFile(this.headers, fileUrl);
51
+ }
44
52
  }
45
53
  export function createMCPServer(options) {
46
54
  const server = new Server({
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Downloads a file from Slack using an authenticated URL.
3
+ * Slack's url_private requires the bot token in the Authorization header.
4
+ */
5
+ export declare function downloadFile(headers: Record<string, string>, fileUrl: string): Promise<Buffer>;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Downloads a file from Slack using an authenticated URL.
3
+ * Slack's url_private requires the bot token in the Authorization header.
4
+ */
5
+ export async function downloadFile(headers, fileUrl) {
6
+ const response = await fetch(fileUrl, {
7
+ method: 'GET',
8
+ headers,
9
+ });
10
+ if (!response.ok) {
11
+ throw new Error(`Failed to download file: ${response.status} ${response.statusText}`);
12
+ }
13
+ const arrayBuffer = await response.arrayBuffer();
14
+ return Buffer.from(arrayBuffer);
15
+ }
@@ -0,0 +1,5 @@
1
+ import type { SlackFile } from '../../types.js';
2
+ /**
3
+ * Fetches information about a specific file using Slack's files.info API
4
+ */
5
+ export declare function getFileInfo(baseUrl: string, headers: Record<string, string>, fileId: string): Promise<SlackFile>;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Fetches information about a specific file using Slack's files.info API
3
+ */
4
+ export async function getFileInfo(baseUrl, headers, fileId) {
5
+ const params = new URLSearchParams({
6
+ file: fileId,
7
+ });
8
+ const response = await fetch(`${baseUrl}/files.info?${params}`, {
9
+ method: 'GET',
10
+ headers,
11
+ });
12
+ if (!response.ok) {
13
+ throw new Error(`Failed to fetch file info: ${response.status} ${response.statusText}`);
14
+ }
15
+ const data = (await response.json());
16
+ if (!data.ok) {
17
+ throw new Error(`Slack API error: ${data.error}`);
18
+ }
19
+ if (!data.file) {
20
+ throw new Error('File not found in response');
21
+ }
22
+ return data.file;
23
+ }
@@ -101,6 +101,19 @@ export function createIntegrationMockSlackClient(mockData = {}) {
101
101
  async addReaction() {
102
102
  // No-op for mock
103
103
  },
104
+ async getFileInfo(fileId) {
105
+ return {
106
+ id: fileId,
107
+ name: `mock-file-${fileId}.txt`,
108
+ mimetype: 'text/plain',
109
+ size: 1024,
110
+ url_private: `https://files.slack.com/files-pri/mock/${fileId}`,
111
+ url_private_download: `https://files.slack.com/files-pri/mock/${fileId}/download`,
112
+ };
113
+ },
114
+ async downloadFile() {
115
+ return Buffer.from('mock-file-content');
116
+ },
104
117
  };
105
118
  return client;
106
119
  }
@@ -0,0 +1,37 @@
1
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
+ import { z } from 'zod';
3
+ import type { ClientFactory } from '../server.js';
4
+ export declare const DownloadFileSchema: z.ZodObject<{
5
+ file_id: z.ZodString;
6
+ }, "strip", z.ZodTypeAny, {
7
+ file_id: string;
8
+ }, {
9
+ file_id: string;
10
+ }>;
11
+ export declare function downloadFileTool(server: Server, clientFactory: ClientFactory): {
12
+ name: string;
13
+ description: string;
14
+ inputSchema: {
15
+ type: "object";
16
+ properties: {
17
+ file_id: {
18
+ type: string;
19
+ description: string;
20
+ };
21
+ };
22
+ required: string[];
23
+ };
24
+ handler: (args: unknown) => Promise<{
25
+ content: {
26
+ type: string;
27
+ text: string;
28
+ }[];
29
+ isError: boolean;
30
+ } | {
31
+ content: {
32
+ type: string;
33
+ text: string;
34
+ }[];
35
+ isError?: undefined;
36
+ }>;
37
+ };
@@ -0,0 +1,106 @@
1
+ import { z } from 'zod';
2
+ import { writeFile, mkdir } from 'fs/promises';
3
+ import { join } from 'path';
4
+ import { tmpdir } from 'os';
5
+ const PARAM_DESCRIPTIONS = {
6
+ file_id: 'The Slack file ID (e.g., "F1234567890"). ' +
7
+ 'File IDs are shown in message outputs next to uploaded files.',
8
+ };
9
+ export const DownloadFileSchema = z.object({
10
+ file_id: z.string().min(1).describe(PARAM_DESCRIPTIONS.file_id),
11
+ });
12
+ const TOOL_DESCRIPTION = `Download a file from Slack to a local temporary path.
13
+
14
+ Slack files require authentication to access, so this tool handles the download using the bot token. It saves the file to a temporary directory and returns the local file path.
15
+
16
+ **Returns:**
17
+ - Local file path (file:// URI) where the downloaded file was saved
18
+ - File metadata: name, type, size
19
+
20
+ **Use cases:**
21
+ - Download images shared in Slack to view them
22
+ - Download documents, PDFs, or other files shared in channels
23
+ - Access any file attachment from a Slack message
24
+
25
+ **Note:** File IDs (e.g., "F1234567890") are shown in message outputs from slack_get_channel and slack_get_thread.`;
26
+ export function downloadFileTool(server, clientFactory) {
27
+ return {
28
+ name: 'slack_download_file',
29
+ description: TOOL_DESCRIPTION,
30
+ inputSchema: {
31
+ type: 'object',
32
+ properties: {
33
+ file_id: {
34
+ type: 'string',
35
+ description: PARAM_DESCRIPTIONS.file_id,
36
+ },
37
+ },
38
+ required: ['file_id'],
39
+ },
40
+ handler: async (args) => {
41
+ try {
42
+ const parsed = DownloadFileSchema.parse(args);
43
+ const client = clientFactory();
44
+ // Get file metadata
45
+ const fileInfo = await client.getFileInfo(parsed.file_id);
46
+ const downloadUrl = fileInfo.url_private_download || fileInfo.url_private;
47
+ if (!downloadUrl) {
48
+ return {
49
+ content: [
50
+ {
51
+ type: 'text',
52
+ text: `File ${parsed.file_id} has no downloadable URL. It may have been deleted or is not accessible.`,
53
+ },
54
+ ],
55
+ isError: true,
56
+ };
57
+ }
58
+ // Download the file
59
+ const fileBuffer = await client.downloadFile(downloadUrl);
60
+ // Write to temp directory
61
+ const slackTmpDir = join(tmpdir(), 'slack-files');
62
+ await mkdir(slackTmpDir, { recursive: true });
63
+ const fileName = fileInfo.name || fileInfo.title || parsed.file_id;
64
+ const safeName = fileName.replace(/[^a-zA-Z0-9._-]/g, '_');
65
+ const localPath = join(slackTmpDir, `${parsed.file_id}-${safeName}`);
66
+ await writeFile(localPath, fileBuffer);
67
+ const sizeParts = [];
68
+ if (fileInfo.mimetype)
69
+ sizeParts.push(fileInfo.mimetype);
70
+ if (fileInfo.size)
71
+ sizeParts.push(formatFileSize(fileInfo.size));
72
+ let output = `File downloaded successfully.\n`;
73
+ output += `Name: ${fileName}\n`;
74
+ if (sizeParts.length > 0)
75
+ output += `Type: ${sizeParts.join(', ')}\n`;
76
+ output += `Path: file://${localPath}\n`;
77
+ return {
78
+ content: [
79
+ {
80
+ type: 'text',
81
+ text: output,
82
+ },
83
+ ],
84
+ };
85
+ }
86
+ catch (error) {
87
+ return {
88
+ content: [
89
+ {
90
+ type: 'text',
91
+ text: `Error downloading file: ${error instanceof Error ? error.message : 'Unknown error'}`,
92
+ },
93
+ ],
94
+ isError: true,
95
+ };
96
+ }
97
+ },
98
+ };
99
+ }
100
+ function formatFileSize(bytes) {
101
+ if (bytes < 1024)
102
+ return `${bytes} B`;
103
+ if (bytes < 1024 * 1024)
104
+ return `${(bytes / 1024).toFixed(1)} KB`;
105
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
106
+ }
@@ -51,13 +51,8 @@ export function formatMessageExtras(msg) {
51
51
  if (file.size) {
52
52
  parts.push(formatFileSize(file.size));
53
53
  }
54
- if (file.permalink) {
55
- parts.push(`Link: ${file.permalink}`);
56
- }
57
- else if (file.url_private) {
58
- parts.push(`Link: ${file.url_private}`);
59
- }
60
- output += ` 📄 File: ${parts.join(' | ')}\n`;
54
+ parts.push(`id: ${file.id}`);
55
+ output += ` 📄 File: ${parts.join(' | ')} — use slack_download_file to download\n`;
61
56
  }
62
57
  }
63
58
  return output;
package/shared/tools.js CHANGED
@@ -7,6 +7,7 @@ import { postMessageTool } from './tools/post-message.js';
7
7
  import { replyToThreadTool } from './tools/reply-to-thread.js';
8
8
  import { updateMessageTool } from './tools/update-message.js';
9
9
  import { reactToMessageTool } from './tools/react-to-message.js';
10
+ import { downloadFileTool } from './tools/download-file.js';
10
11
  const ALL_TOOL_GROUPS = ['readonly', 'write'];
11
12
  /**
12
13
  * Parse enabled tool groups from environment variable
@@ -31,6 +32,7 @@ const ALL_TOOLS = [
31
32
  { factory: getChannelsTool, groups: ['readonly', 'write'] },
32
33
  { factory: getChannelTool, groups: ['readonly', 'write'] },
33
34
  { factory: getThreadTool, groups: ['readonly', 'write'] },
35
+ { factory: downloadFileTool, groups: ['readonly', 'write'] },
34
36
  // Write tools
35
37
  { factory: postMessageTool, groups: ['write'] },
36
38
  { factory: replyToThreadTool, groups: ['write'] },