slack-workspace-mcp-server 0.0.2 → 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 +26 -1
- package/shared/server.js +12 -0
- package/shared/slack-client/lib/download-file.d.ts +5 -0
- package/shared/slack-client/lib/download-file.js +15 -0
- package/shared/slack-client/lib/get-file-info.d.ts +5 -0
- package/shared/slack-client/lib/get-file-info.js +23 -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 +23 -0
- package/shared/tools/download-file.d.ts +37 -0
- package/shared/tools/download-file.js +106 -0
- package/shared/tools/format-message-extras.js +2 -7
- package/shared/tools/upload-snippet.d.ts +65 -0
- package/shared/tools/upload-snippet.js +116 -0
- package/shared/tools.js +4 -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
|
@@ -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,23 @@ 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>;
|
|
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>;
|
|
58
75
|
}
|
|
59
76
|
/**
|
|
60
77
|
* Slack API client implementation
|
|
@@ -93,6 +110,14 @@ export declare class SlackClient implements ISlackClient {
|
|
|
93
110
|
}): Promise<Message>;
|
|
94
111
|
updateMessage(channelId: string, ts: string, text: string): Promise<Message>;
|
|
95
112
|
addReaction(channelId: string, timestamp: string, emoji: string): Promise<void>;
|
|
113
|
+
getFileInfo(fileId: string): Promise<SlackFile>;
|
|
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>;
|
|
96
121
|
}
|
|
97
122
|
export type ClientFactory = () => ISlackClient;
|
|
98
123
|
export interface CreateMCPServerOptions {
|
package/shared/server.js
CHANGED
|
@@ -41,6 +41,18 @@ 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
|
+
}
|
|
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
|
+
}
|
|
44
56
|
}
|
|
45
57
|
export function createMCPServer(options) {
|
|
46
58
|
const server = new Server({
|
|
@@ -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,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
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -101,6 +101,29 @@ 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
|
+
},
|
|
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
|
+
},
|
|
104
127
|
};
|
|
105
128
|
return client;
|
|
106
129
|
}
|
|
@@ -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
|
-
|
|
55
|
-
|
|
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;
|
|
@@ -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
|
@@ -7,6 +7,8 @@ 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';
|
|
11
|
+
import { uploadSnippetTool } from './tools/upload-snippet.js';
|
|
10
12
|
const ALL_TOOL_GROUPS = ['readonly', 'write'];
|
|
11
13
|
/**
|
|
12
14
|
* Parse enabled tool groups from environment variable
|
|
@@ -31,11 +33,13 @@ const ALL_TOOLS = [
|
|
|
31
33
|
{ factory: getChannelsTool, groups: ['readonly', 'write'] },
|
|
32
34
|
{ factory: getChannelTool, groups: ['readonly', 'write'] },
|
|
33
35
|
{ factory: getThreadTool, groups: ['readonly', 'write'] },
|
|
36
|
+
{ factory: downloadFileTool, groups: ['readonly', 'write'] },
|
|
34
37
|
// Write tools
|
|
35
38
|
{ factory: postMessageTool, groups: ['write'] },
|
|
36
39
|
{ factory: replyToThreadTool, groups: ['write'] },
|
|
37
40
|
{ factory: updateMessageTool, groups: ['write'] },
|
|
38
41
|
{ factory: reactToMessageTool, groups: ['write'] },
|
|
42
|
+
{ factory: uploadSnippetTool, groups: ['write'] },
|
|
39
43
|
];
|
|
40
44
|
/**
|
|
41
45
|
* Creates a function to register all tools with the server
|