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