slack-workspace-mcp-server 0.0.3 → 0.0.4

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/README.md CHANGED
@@ -11,6 +11,7 @@ A Model Context Protocol (MCP) server for integrating with Slack workspaces. Thi
11
11
  - **Reply to Threads** - Continue threaded conversations
12
12
  - **Update Messages** - Edit previously posted messages
13
13
  - **React to Messages** - Add emoji reactions to messages
14
+ - **Upload Snippets** - Upload text content as file snippets (for long content that exceeds message limits)
14
15
 
15
16
  ## Setup
16
17
 
@@ -38,6 +39,7 @@ A Model Context Protocol (MCP) server for integrating with Slack workspaces. Thi
38
39
  - `groups:history` - View messages in private channels
39
40
  - `chat:write` - Send messages
40
41
  - `reactions:write` - Add reactions
42
+ - `files:write` - Upload files and snippets
41
43
  5. Install the app to your workspace
42
44
  6. Copy the **Bot User OAuth Token** (starts with `xoxb-`)
43
45
 
@@ -85,12 +87,13 @@ Restart Claude Desktop and you should be ready to go!
85
87
 
86
88
  ### Write Tools
87
89
 
88
- | Tool | Description |
89
- | ------------------------ | ---------------------------------- |
90
- | `slack_post_message` | Post a new message to a channel |
91
- | `slack_reply_to_thread` | Reply to an existing thread |
92
- | `slack_update_message` | Update a previously posted message |
93
- | `slack_react_to_message` | Add an emoji reaction to a message |
90
+ | Tool | Description |
91
+ | ------------------------ | -------------------------------------------------- |
92
+ | `slack_post_message` | Post a new message to a channel |
93
+ | `slack_reply_to_thread` | Reply to an existing thread |
94
+ | `slack_update_message` | Update a previously posted message |
95
+ | `slack_react_to_message` | Add an emoji reaction to a message |
96
+ | `slack_upload_snippet` | Upload text content as a file snippet to a channel |
94
97
 
95
98
  ## Tool Groups
96
99
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slack-workspace-mcp-server",
3
- "version": "0.0.3",
3
+ "version": "0.0.4",
4
4
  "description": "MCP server for Slack workspace integration",
5
5
  "main": "build/index.js",
6
6
  "type": "module",
@@ -63,6 +63,15 @@ export interface ISlackClient {
63
63
  * Download a file from Slack (requires authenticated URL)
64
64
  */
65
65
  downloadFile(fileUrl: string): Promise<Buffer>;
66
+ /**
67
+ * Upload text content as a snippet/file to a channel
68
+ */
69
+ uploadSnippet(content: string, options: {
70
+ channelId: string;
71
+ filename?: string;
72
+ title?: string;
73
+ threadTs?: string;
74
+ }): Promise<SlackFile>;
66
75
  }
67
76
  /**
68
77
  * Slack API client implementation
@@ -103,6 +112,12 @@ export declare class SlackClient implements ISlackClient {
103
112
  addReaction(channelId: string, timestamp: string, emoji: string): Promise<void>;
104
113
  getFileInfo(fileId: string): Promise<SlackFile>;
105
114
  downloadFile(fileUrl: string): Promise<Buffer>;
115
+ uploadSnippet(content: string, options: {
116
+ channelId: string;
117
+ filename?: string;
118
+ title?: string;
119
+ threadTs?: string;
120
+ }): Promise<SlackFile>;
106
121
  }
107
122
  export type ClientFactory = () => ISlackClient;
108
123
  export interface CreateMCPServerOptions {
package/shared/server.js CHANGED
@@ -49,6 +49,10 @@ export class SlackClient {
49
49
  const { downloadFile } = await import('./slack-client/lib/download-file.js');
50
50
  return downloadFile(this.headers, fileUrl);
51
51
  }
52
+ async uploadSnippet(content, options) {
53
+ const { uploadSnippet } = await import('./slack-client/lib/upload-snippet.js');
54
+ return uploadSnippet(this.baseUrl, this.headers, content, options);
55
+ }
52
56
  }
53
57
  export function createMCPServer(options) {
54
58
  const server = new Server({
@@ -0,0 +1,14 @@
1
+ import type { SlackFile } from '../../types.js';
2
+ /**
3
+ * Uploads text content as a snippet/file to Slack using the modern
4
+ * files.getUploadURLExternal + files.completeUploadExternal flow.
5
+ *
6
+ * The deprecated files.upload API stopped working for new apps in May 2024
7
+ * and was fully retired in November 2025.
8
+ */
9
+ export declare function uploadSnippet(baseUrl: string, headers: Record<string, string>, content: string, options: {
10
+ channelId: string;
11
+ filename?: string;
12
+ title?: string;
13
+ threadTs?: string;
14
+ }): Promise<SlackFile>;
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Uploads text content as a snippet/file to Slack using the modern
3
+ * files.getUploadURLExternal + files.completeUploadExternal flow.
4
+ *
5
+ * The deprecated files.upload API stopped working for new apps in May 2024
6
+ * and was fully retired in November 2025.
7
+ */
8
+ export async function uploadSnippet(baseUrl, headers, content, options) {
9
+ const filename = options.filename ?? 'snippet.txt';
10
+ const contentBuffer = Buffer.from(content, 'utf-8');
11
+ const length = contentBuffer.length;
12
+ // Step 1: Get an upload URL from Slack
13
+ const params = new URLSearchParams({
14
+ filename,
15
+ length: length.toString(),
16
+ });
17
+ const uploadUrlResponse = await fetch(`${baseUrl}/files.getUploadURLExternal?${params}`, {
18
+ method: 'GET',
19
+ headers,
20
+ });
21
+ if (!uploadUrlResponse.ok) {
22
+ throw new Error(`Failed to get upload URL: ${uploadUrlResponse.status} ${uploadUrlResponse.statusText}`);
23
+ }
24
+ const uploadUrlData = (await uploadUrlResponse.json());
25
+ if (!uploadUrlData.ok) {
26
+ throw new Error(`Slack API error (getUploadURLExternal): ${uploadUrlData.error}`);
27
+ }
28
+ if (!uploadUrlData.upload_url || !uploadUrlData.file_id) {
29
+ throw new Error('Missing upload_url or file_id in response');
30
+ }
31
+ // Step 2: Upload the file content to the presigned URL (no auth headers needed)
32
+ const uploadResponse = await fetch(uploadUrlData.upload_url, {
33
+ method: 'POST',
34
+ headers: {
35
+ 'Content-Type': 'application/octet-stream',
36
+ },
37
+ body: contentBuffer,
38
+ });
39
+ if (!uploadResponse.ok) {
40
+ throw new Error(`Failed to upload file content: ${uploadResponse.status} ${uploadResponse.statusText}`);
41
+ }
42
+ // Step 3: Complete the upload and share in channel
43
+ const completeBody = {
44
+ files: [
45
+ {
46
+ id: uploadUrlData.file_id,
47
+ title: options.title,
48
+ },
49
+ ],
50
+ channel_id: options.channelId,
51
+ };
52
+ if (options.threadTs) {
53
+ completeBody.thread_ts = options.threadTs;
54
+ }
55
+ const completeResponse = await fetch(`${baseUrl}/files.completeUploadExternal`, {
56
+ method: 'POST',
57
+ headers: {
58
+ ...headers,
59
+ 'Content-Type': 'application/json; charset=utf-8',
60
+ },
61
+ body: JSON.stringify(completeBody),
62
+ });
63
+ if (!completeResponse.ok) {
64
+ throw new Error(`Failed to complete upload: ${completeResponse.status} ${completeResponse.statusText}`);
65
+ }
66
+ const completeData = (await completeResponse.json());
67
+ if (!completeData.ok) {
68
+ throw new Error(`Slack API error (completeUploadExternal): ${completeData.error}`);
69
+ }
70
+ const file = completeData.files?.[0];
71
+ if (!file) {
72
+ // Return a minimal file object using the file_id we already know
73
+ return {
74
+ id: uploadUrlData.file_id,
75
+ name: filename,
76
+ title: options.title,
77
+ };
78
+ }
79
+ return file;
80
+ }
@@ -114,6 +114,16 @@ export function createIntegrationMockSlackClient(mockData = {}) {
114
114
  async downloadFile() {
115
115
  return Buffer.from('mock-file-content');
116
116
  },
117
+ async uploadSnippet(content, options) {
118
+ return {
119
+ id: `F${Date.now()}`,
120
+ name: options.filename ?? 'snippet.txt',
121
+ title: options.title,
122
+ mimetype: 'text/plain',
123
+ size: content.length,
124
+ permalink: `https://slack.com/files/mock/${options.filename ?? 'snippet.txt'}`,
125
+ };
126
+ },
117
127
  };
118
128
  return client;
119
129
  }
@@ -0,0 +1,65 @@
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 UploadSnippetSchema: z.ZodObject<{
5
+ channel_id: z.ZodString;
6
+ content: z.ZodString;
7
+ filename: z.ZodOptional<z.ZodString>;
8
+ title: z.ZodOptional<z.ZodString>;
9
+ thread_ts: z.ZodOptional<z.ZodString>;
10
+ }, "strip", z.ZodTypeAny, {
11
+ channel_id: string;
12
+ content: string;
13
+ thread_ts?: string | undefined;
14
+ filename?: string | undefined;
15
+ title?: string | undefined;
16
+ }, {
17
+ channel_id: string;
18
+ content: string;
19
+ thread_ts?: string | undefined;
20
+ filename?: string | undefined;
21
+ title?: string | undefined;
22
+ }>;
23
+ export declare function uploadSnippetTool(server: Server, clientFactory: ClientFactory): {
24
+ name: string;
25
+ description: string;
26
+ inputSchema: {
27
+ type: "object";
28
+ properties: {
29
+ channel_id: {
30
+ type: string;
31
+ description: string;
32
+ };
33
+ content: {
34
+ type: string;
35
+ description: string;
36
+ };
37
+ filename: {
38
+ type: string;
39
+ description: string;
40
+ };
41
+ title: {
42
+ type: string;
43
+ description: "Title displayed in Slack above the snippet.";
44
+ };
45
+ thread_ts: {
46
+ type: string;
47
+ description: string;
48
+ };
49
+ };
50
+ required: string[];
51
+ };
52
+ handler: (args: unknown) => Promise<{
53
+ content: {
54
+ type: string;
55
+ text: string;
56
+ }[];
57
+ isError?: undefined;
58
+ } | {
59
+ content: {
60
+ type: string;
61
+ text: string;
62
+ }[];
63
+ isError: boolean;
64
+ }>;
65
+ };
@@ -0,0 +1,116 @@
1
+ import { z } from 'zod';
2
+ const PARAM_DESCRIPTIONS = {
3
+ channel_id: 'The channel ID to share the snippet in (e.g., "C1234567890"). ' +
4
+ 'Get channel IDs using the slack_get_channels tool.',
5
+ content: 'The text content to upload as a snippet. Can be arbitrarily long — ' +
6
+ 'use this instead of slack_post_message when content exceeds message length limits.',
7
+ filename: 'Filename for the snippet (e.g., "output.txt", "error.log", "code.py"). ' +
8
+ 'Slack uses the file extension for syntax highlighting. Default: "snippet.txt".',
9
+ title: 'Title displayed in Slack above the snippet.',
10
+ thread_ts: 'Post the snippet as a thread reply to this message timestamp. ' +
11
+ 'If omitted, the snippet is posted as a new message in the channel.',
12
+ };
13
+ export const UploadSnippetSchema = z.object({
14
+ channel_id: z.string().min(1).describe(PARAM_DESCRIPTIONS.channel_id),
15
+ content: z.string().min(1).describe(PARAM_DESCRIPTIONS.content),
16
+ filename: z.string().optional().describe(PARAM_DESCRIPTIONS.filename),
17
+ title: z.string().optional().describe(PARAM_DESCRIPTIONS.title),
18
+ thread_ts: z.string().optional().describe(PARAM_DESCRIPTIONS.thread_ts),
19
+ });
20
+ const TOOL_DESCRIPTION = `Upload text content as a file snippet to a Slack channel.
21
+
22
+ Uploads content as a text file/snippet, bypassing Slack's message length limits. Use this when content is too long for slack_post_message (e.g., long URLs, logs, code, JSON payloads).
23
+
24
+ **Returns:**
25
+ - Confirmation of the uploaded snippet
26
+ - The file ID for reference
27
+
28
+ **Use cases:**
29
+ - Share very long URLs that exceed message length limits
30
+ - Post error logs, stack traces, or debug output
31
+ - Share code snippets or configuration files
32
+ - Upload large JSON or text payloads
33
+
34
+ **Note:** For short messages that fit within Slack's limits, use slack_post_message instead. Use the filename extension to control syntax highlighting (e.g., "code.py" for Python).`;
35
+ export function uploadSnippetTool(server, clientFactory) {
36
+ return {
37
+ name: 'slack_upload_snippet',
38
+ description: TOOL_DESCRIPTION,
39
+ inputSchema: {
40
+ type: 'object',
41
+ properties: {
42
+ channel_id: {
43
+ type: 'string',
44
+ description: PARAM_DESCRIPTIONS.channel_id,
45
+ },
46
+ content: {
47
+ type: 'string',
48
+ description: PARAM_DESCRIPTIONS.content,
49
+ },
50
+ filename: {
51
+ type: 'string',
52
+ description: PARAM_DESCRIPTIONS.filename,
53
+ },
54
+ title: {
55
+ type: 'string',
56
+ description: PARAM_DESCRIPTIONS.title,
57
+ },
58
+ thread_ts: {
59
+ type: 'string',
60
+ description: PARAM_DESCRIPTIONS.thread_ts,
61
+ },
62
+ },
63
+ required: ['channel_id', 'content'],
64
+ },
65
+ handler: async (args) => {
66
+ try {
67
+ const parsed = UploadSnippetSchema.parse(args);
68
+ const client = clientFactory();
69
+ const file = await client.uploadSnippet(parsed.content, {
70
+ channelId: parsed.channel_id,
71
+ filename: parsed.filename,
72
+ title: parsed.title,
73
+ threadTs: parsed.thread_ts,
74
+ });
75
+ const parts = [
76
+ `Snippet uploaded successfully!\n`,
77
+ `Channel: ${parsed.channel_id}`,
78
+ `File ID: ${file.id}`,
79
+ ];
80
+ if (file.name) {
81
+ parts.push(`Filename: ${file.name}`);
82
+ }
83
+ if (file.title) {
84
+ parts.push(`Title: ${file.title}`);
85
+ }
86
+ if (parsed.thread_ts) {
87
+ parts.push(`Thread: ${parsed.thread_ts}`);
88
+ }
89
+ if (file.permalink) {
90
+ parts.push(`Permalink: ${file.permalink}`);
91
+ }
92
+ const byteLength = Buffer.from(parsed.content, 'utf-8').length;
93
+ parts.push(`\nContent length: ${byteLength} bytes`);
94
+ return {
95
+ content: [
96
+ {
97
+ type: 'text',
98
+ text: parts.join('\n'),
99
+ },
100
+ ],
101
+ };
102
+ }
103
+ catch (error) {
104
+ return {
105
+ content: [
106
+ {
107
+ type: 'text',
108
+ text: `Error uploading snippet: ${error instanceof Error ? error.message : 'Unknown error'}`,
109
+ },
110
+ ],
111
+ isError: true,
112
+ };
113
+ }
114
+ },
115
+ };
116
+ }
package/shared/tools.js CHANGED
@@ -8,6 +8,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
10
  import { downloadFileTool } from './tools/download-file.js';
11
+ import { uploadSnippetTool } from './tools/upload-snippet.js';
11
12
  const ALL_TOOL_GROUPS = ['readonly', 'write'];
12
13
  /**
13
14
  * Parse enabled tool groups from environment variable
@@ -38,6 +39,7 @@ const ALL_TOOLS = [
38
39
  { factory: replyToThreadTool, groups: ['write'] },
39
40
  { factory: updateMessageTool, groups: ['write'] },
40
41
  { factory: reactToMessageTool, groups: ['write'] },
42
+ { factory: uploadSnippetTool, groups: ['write'] },
41
43
  ];
42
44
  /**
43
45
  * Creates a function to register all tools with the server