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 +9 -6
- package/package.json +1 -1
- package/shared/server.d.ts +15 -0
- package/shared/server.js +4 -0
- package/shared/slack-client/lib/upload-snippet.d.ts +14 -0
- package/shared/slack-client/lib/upload-snippet.js +80 -0
- package/shared/slack-client/slack-client.integration-mock.js +10 -0
- package/shared/tools/upload-snippet.d.ts +65 -0
- package/shared/tools/upload-snippet.js +116 -0
- package/shared/tools.js +2 -0
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
package/shared/server.d.ts
CHANGED
|
@@ -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
|