Skip to content

Commit 537831f

Browse files
author
cacapouh
committed
feat: add attachment support tools
- Add messages_with_attachments tool to search for file attachments in channels - Add get_attachment_details tool to get file metadata (thumbnails, dimensions, etc.) - Add get_attachment_content tool to retrieve text file content for RAG/AI - Add files:read scope to documentation - Add SlackToken() and ProvideHTTPClient() methods to ApiProvider Based on work started in korotovsky#130 by @FlorianOtel Closes korotovsky#88
1 parent a3d3fab commit 537831f

File tree

4 files changed

+153
-2
lines changed

4 files changed

+153
-2
lines changed

docs/01-authentication-setup.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,8 @@ Instead of using browser-based tokens (`xoxc`/`xoxd`), you can use a User OAuth
5656
- `mpim:write` - Start group direct messages with people on a user’s behalf (new since `v1.1.18`)
5757
- `users:read` - View people in a workspace.
5858
- `chat:write` - Send messages on a user’s behalf. (new since `v1.1.18`)
59-
- `search:read` - Search a workspace’s content. (new since `v1.1.18`)
59+
- `search:read` - Search a workspace's content. (new since `v1.1.18`)
60+
- `files:read` - View files shared in channels and conversations.
6061

6162
3. Install the app to your workspace
6263
4. Copy the "User OAuth Token" (starts with `xoxp-`)
@@ -84,7 +85,8 @@ To create the app from a manifest with permissions preconfigured, use the follow
8485
"mpim:write",
8586
"users:read",
8687
"chat:write",
87-
"search:read"
88+
"search:read",
89+
"files:read"
8890
]
8991
}
9092
},

pkg/handler/attachments.go

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import (
55
"encoding/json"
66
"errors"
77
"fmt"
8+
"io"
9+
"net/http"
810
"strings"
911

1012
"github.com/gocarina/gocsv"
@@ -246,3 +248,121 @@ func (ah *AttachmentsHandler) GetAttachmentDetailsHandler(ctx context.Context, r
246248

247249
return mcp.NewToolResultText(string(jsonBytes)), nil
248250
}
251+
252+
// textMimeTypes are MIME types considered as text for content retrieval
253+
var textMimeTypes = map[string]bool {
254+
"text/plain": true,
255+
"text/html": true,
256+
"text/css": true,
257+
"text/csv": true,
258+
"text/markdown": true,
259+
"text/xml": true,
260+
"application/json": true,
261+
"application/xml": true,
262+
"application/javascript": true,
263+
"application/x-yaml": true,
264+
"application/x-sh": true,
265+
}
266+
267+
// isTextFile checks if the file is a text file based on MIME type or file extension
268+
func isTextFile(mimeType, fileType string) bool {
269+
if textMimeTypes[mimeType] {
270+
return true
271+
}
272+
// Check common text file extensions
273+
textExtensions := []string{"txt", "md", "json", "xml", "yaml", "yml", "csv", "log", "sh", "py", "go", "js", "ts", "html", "css", "sql"}
274+
for _, ext := range textExtensions {
275+
if fileType == ext {
276+
return true
277+
}
278+
}
279+
return false
280+
}
281+
282+
// GetAttachmentContentHandler retrieves the content of a text file attachment
283+
func (ah *AttachmentsHandler) GetAttachmentContentHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
284+
ah.logger.Debug("GetAttachmentContentHandler called", zap.Any("params", request.Params))
285+
286+
if authenticated, err := auth.IsAuthenticated(ctx, ah.apiProvider.ServerTransport(), ah.logger); !authenticated {
287+
ah.logger.Error("Authentication failed", zap.Error(err))
288+
return nil, err
289+
}
290+
291+
fileID := request.GetString("file_id", "")
292+
if fileID == "" {
293+
return nil, errors.New("file_id is required")
294+
}
295+
296+
// Get file info first
297+
file, _, _, err := ah.apiProvider.Slack().GetFileInfoContext(ctx, fileID, 1, 1)
298+
if err != nil {
299+
ah.logger.Error("Failed to get file info", zap.String("file_id", fileID), zap.Error(err))
300+
return nil, err
301+
}
302+
303+
// Check if it's a text file
304+
if !isTextFile(file.Mimetype, file.Filetype) {
305+
return nil, fmt.Errorf("file %q is not a text file (type: %s). Use get_attachment_details for binary files", file.Name, file.Mimetype)
306+
}
307+
308+
// Download the file content
309+
content, err := ah.downloadFileContent(ctx, file.URLPrivate)
310+
if err != nil {
311+
ah.logger.Error("Failed to download file content", zap.String("file_id", fileID), zap.Error(err))
312+
return nil, err
313+
}
314+
315+
// Build response with metadata and content
316+
response := map[string]interface{}{
317+
"id": file.ID,
318+
"name": file.Name,
319+
"title": file.Title,
320+
"mimeType": file.Mimetype,
321+
"fileType": file.Filetype,
322+
"size": file.Size,
323+
"content": content,
324+
}
325+
326+
jsonBytes, err := json.MarshalIndent(response, "", " ")
327+
if err != nil {
328+
ah.logger.Error("Failed to marshal response to JSON", zap.Error(err))
329+
return nil, err
330+
}
331+
332+
return mcp.NewToolResultText(string(jsonBytes)), nil
333+
}
334+
335+
// downloadFileContent downloads the content of a file from Slack
336+
func (ah *AttachmentsHandler) downloadFileContent(ctx context.Context, url string) (string, error) {
337+
// Get HTTP client from provider
338+
httpClient := ah.apiProvider.ProvideHTTPClient()
339+
340+
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
341+
if err != nil {
342+
return "", fmt.Errorf("failed to create request: %w", err)
343+
}
344+
345+
// Add Authorization header for authenticated file download
346+
// This is required for bot tokens (xoxb-) and user tokens (xoxp-)
347+
token := ah.apiProvider.SlackToken()
348+
if token != "" {
349+
req.Header.Set("Authorization", "Bearer "+token)
350+
}
351+
352+
resp, err := httpClient.Do(req)
353+
if err != nil {
354+
return "", fmt.Errorf("failed to download file: %w", err)
355+
}
356+
defer resp.Body.Close()
357+
358+
if resp.StatusCode != http.StatusOK {
359+
return "", fmt.Errorf("failed to download file: HTTP %d", resp.StatusCode)
360+
}
361+
362+
body, err := io.ReadAll(resp.Body)
363+
if err != nil {
364+
return "", fmt.Errorf("failed to read file content: %w", err)
365+
}
366+
367+
return string(body), nil
368+
}

pkg/provider/api.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"encoding/json"
66
"errors"
77
"io/ioutil"
8+
"net/http"
89
"os"
910
"path/filepath"
1011
"strings"
@@ -774,6 +775,24 @@ func (ap *ApiProvider) IsBotToken() bool {
774775
return ok && client != nil && client.IsBotToken()
775776
}
776777

778+
// ProvideHTTPClient returns the HTTP client used for Slack API requests
779+
func (ap *ApiProvider) ProvideHTTPClient() *http.Client {
780+
client, ok := ap.client.(*MCPSlackClient)
781+
if !ok || client == nil || client.authProvider == nil {
782+
return http.DefaultClient
783+
}
784+
return transport.ProvideHTTPClient(client.authProvider.Cookies(), ap.logger)
785+
}
786+
787+
// SlackToken returns the Slack API token for authenticated requests
788+
func (ap *ApiProvider) SlackToken() string {
789+
client, ok := ap.client.(*MCPSlackClient)
790+
if !ok || client == nil || client.authProvider == nil {
791+
return ""
792+
}
793+
return client.authProvider.SlackToken()
794+
}
795+
777796
func mapChannel(
778797
id, name, nameNormalized, topic, purpose, user string,
779798
members []string,

pkg/server/server.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,16 @@ func NewMCPServer(provider *provider.ApiProvider, logger *zap.Logger) *MCPServer
177177
),
178178
), attachmentsHandler.GetAttachmentDetailsHandler)
179179

180+
s.AddTool(mcp.NewTool("get_attachment_content",
181+
mcp.WithDescription("Get the content of a text file attachment. Only works for text files (txt, json, md, yaml, csv, etc.). Returns the file content directly for RAG/AI processing."),
182+
mcp.WithTitleAnnotation("Get Attachment Content"),
183+
mcp.WithReadOnlyHintAnnotation(true),
184+
mcp.WithString("file_id",
185+
mcp.Required(),
186+
mcp.Description("The ID of the file attachment to get content for (format: Fxxxxxxxxxx)."),
187+
),
188+
), attachmentsHandler.GetAttachmentContentHandler)
189+
180190
s.AddTool(mcp.NewTool("channels_list",
181191
mcp.WithDescription("Get list of channels"),
182192
mcp.WithTitleAnnotation("List Channels"),

0 commit comments

Comments
 (0)