Files
mattermost-mcp-server/operation/file/file.go
T
karti 35a0b2b715 feat: initial release with 48 Mattermost MCP tools
- 27 base tools: channels, messaging, users, reactions, files, DMs
- Team management: invite/remove users, get stats, list members
- Slash commands: execute /remind, /poll, etc.
- Webhook management: incoming and outgoing webhooks
- System tools: server config, logs, bulk status updates
- Channel admin: create, invite, leave, delete channels
- Read-only mode for safe exploration
- Dual token support (bot + PAT) for enhanced security
- Apache 2.0 licensed
2026-04-15 21:26:19 -07:00

192 lines
5.5 KiB
Go

package file
import (
"context"
"fmt"
"os"
"path/filepath"
"github.com/karti-ai/mattermost-mcp-server/pkg/file"
"github.com/karti-ai/mattermost-mcp-server/pkg/log"
"github.com/karti-ai/mattermost-mcp-server/pkg/mattermost"
"github.com/karti-ai/mattermost-mcp-server/pkg/params"
"github.com/karti-ai/mattermost-mcp-server/pkg/to"
"github.com/karti-ai/mattermost-mcp-server/pkg/tool"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
"github.com/mattermost/mattermost-server/v6/model"
)
var Tool = tool.New()
const (
UploadFileToolName = "mattermost_upload_file"
DownloadFileToolName = "mattermost_download_file"
)
var (
UploadFileTool = mcp.NewTool(
UploadFileToolName,
mcp.WithDescription("Upload file to channel"),
mcp.WithString("channel_id", mcp.Required(), mcp.Description("Channel ID to upload file to")),
mcp.WithString("file_path", mcp.Required(), mcp.Description("Local file path to upload")),
mcp.WithString("message", mcp.Description("Message to post with file (optional)")),
)
DownloadFileTool = mcp.NewTool(
DownloadFileToolName,
mcp.WithDescription("Download file from Mattermost"),
mcp.WithString("file_id", mcp.Required(), mcp.Description("File ID to download")),
mcp.WithString("download_path", mcp.Required(), mcp.Description("Local path where file should be saved")),
)
)
func init() {
registerTools()
}
func registerTools() {
tools := []server.ServerTool{
{Tool: UploadFileTool, Handler: UploadFileFn},
{Tool: DownloadFileTool, Handler: DownloadFileFn},
}
for _, t := range tools {
Tool.RegisterWrite(t)
}
}
func UploadFileFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("[File] Called UploadFileFn")
args := req.GetArguments()
channelId, err := params.GetString(args, "channel_id")
if err != nil {
return to.Error(fmt.Errorf("[channel_id] %v", err)), nil
}
filePath, err := params.GetString(args, "file_path")
if err != nil {
return to.Error(fmt.Errorf("[file_path] %v", err)), nil
}
message := params.GetOptionalString(args, "message", "")
if !file.IsValidPath(filePath) {
return to.Error(fmt.Errorf("[file_path] path traversal detected: %s", filePath)), nil
}
fileInfo, err := file.GetFileInfo(filePath)
if err != nil {
return to.Error(fmt.Errorf("[file_path] failed to access file: %v", err)), nil
}
if fileInfo.IsDir() {
return to.Error(fmt.Errorf("[file_path] path is a directory, not a file: %s", filePath)), nil
}
if err := file.ValidateFileSize(fileInfo.Size()); err != nil {
return to.Error(fmt.Errorf("[file_path] %v", err)), nil
}
if err := file.ValidateFilename(fileInfo.Name()); err != nil {
return to.Error(fmt.Errorf("[file_path] %v", err)), nil
}
data, err := os.ReadFile(filePath)
if err != nil {
return to.Error(fmt.Errorf("[file_path] failed to read file: %v", err)), nil
}
if _, err := file.ValidateMimeType(data); err != nil {
return to.Error(fmt.Errorf("[file_path] %v", err)), nil
}
client := mattermost.GetGlobalClient()
if client == nil {
return to.Error(fmt.Errorf("[internal] client not initialized")), nil
}
filename := filepath.Base(filePath)
uploadResp, err := client.UploadFile(ctx, data, channelId, filename)
if err != nil {
return to.Error(fmt.Errorf("[upload] failed to upload file: %v", err)), nil
}
if message != "" && len(uploadResp.FileInfos) > 0 {
post := &model.Post{
ChannelId: channelId,
Message: message,
FileIds: []string{uploadResp.FileInfos[0].Id},
}
_, err := client.CreatePost(ctx, post)
if err != nil {
return to.Result(map[string]interface{}{
"success": true,
"file_id": uploadResp.FileInfos[0].Id,
"file_infos": SlimFileInfos(uploadResp.FileInfos),
"warning": fmt.Sprintf("File uploaded but message failed to post: %v", err),
}), nil
}
}
return to.Result(map[string]interface{}{
"success": true,
"file_id": uploadResp.FileInfos[0].Id,
"file_infos": SlimFileInfos(uploadResp.FileInfos),
}), nil
}
func DownloadFileFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("[File] Called DownloadFileFn")
args := req.GetArguments()
fileId, err := params.GetString(args, "file_id")
if err != nil {
return to.Error(fmt.Errorf("[file_id] %v", err)), nil
}
downloadPath, err := params.GetString(args, "download_path")
if err != nil {
return to.Error(fmt.Errorf("[download_path] %v", err)), nil
}
if !file.IsValidPath(downloadPath) {
return to.Error(fmt.Errorf("[download_path] path traversal detected: %s", downloadPath)), nil
}
if file.Exists(downloadPath) {
return to.Error(fmt.Errorf("[download_path] file already exists: %s", downloadPath)), nil
}
client := mattermost.GetGlobalClient()
if client == nil {
return to.Error(fmt.Errorf("[internal] client not initialized")), nil
}
data, _, err := client.GetFile(ctx, fileId)
if err != nil {
return to.Error(fmt.Errorf("[download] failed to download file: %v", err)), nil
}
if int64(len(data)) > file.MaxFileSize {
return to.Error(fmt.Errorf("[download] downloaded file size %d exceeds maximum allowed %d", len(data), file.MaxFileSize)), nil
}
if err := file.CheckDiskSpace(downloadPath, int64(len(data))); err != nil {
return to.Error(fmt.Errorf("[download_path] %v", err)), nil
}
if err := os.WriteFile(downloadPath, data, 0644); err != nil {
return to.Error(fmt.Errorf("[download_path] failed to save file: %v", err)), nil
}
return to.Result(map[string]interface{}{
"success": true,
"file_id": fileId,
"download_path": downloadPath,
"size": len(data),
}), nil
}