package storage import ( "context" "fmt" "os" "path/filepath" "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" "fwh-toolbox-server/cmd/mcp/tools/helper" "fwh-toolbox-server/utils/filestore" ) // MinIO 文件前缀约定: // - uploadPrefix:用户上传、供 Agent 读取的文件 // - downloadPrefix:Agent 生成、供用户下载的文件 const ( uploadPrefix = "upload" downloadPrefix = "download" ) // localTempDir 用于存放从 MinIO 下载的临时文件 const localTempDir = "/tmp/fwh-toolbox-agent/downloads" func registerFileTools(s *server.MCPServer) { s.AddTool( mcp.NewTool("storage_file_read", mcp.WithDescription("读取文件文本内容(返回纯文本)。仅适用于文本类文件。key 为写入/上传/列表返回的对象 key。"), mcp.WithString("key", mcp.Required(), mcp.Description("文件的对象 key(写入/上传成功或 storage_file_list 返回的 key)")), ), handleFileRead, ) s.AddTool( mcp.NewTool("storage_file_read_to_local", mcp.WithDescription("将文件下载到本地临时路径。支持任意文件类型(图片、PDF、压缩包等)。返回本地文件路径,可直接用于后续处理或分析。key 为写入/上传/列表返回的对象 key。"), mcp.WithString("key", mcp.Required(), mcp.Description("文件的对象 key(写入/上传成功或 storage_file_list 返回的 key)")), ), handleFileReadToLocal, ) s.AddTool( mcp.NewTool("storage_file_write", mcp.WithDescription("将文本内容写入文件存储(MinIO)。可用于保存分析结果、生成报告等。最大 20MB。如需上传本地文件请使用 storage_file_upload。"), mcp.WithString("fileName", mcp.Required(), mcp.Description("文件名(如 report.csv)")), mcp.WithString("content", mcp.Required(), mcp.Description("要写入的文本内容(最大 20MB)")), mcp.WithString("userId", mcp.Description("用户标识,用于文件路径隔离(尽量填写,不传则使用默认值 mcp-agent)")), mcp.WithString("sessionId", mcp.Description("会话标识,用于文件路径隔离(尽量填写,不传则使用默认值 mcp-session)")), ), handleFileWrite, ) s.AddTool( mcp.NewTool("storage_file_list", mcp.WithDescription("列出当前会话中用户上传的所有文件,返回文件名、key、大小等信息。"), mcp.WithString("userId", mcp.Description("用户标识,用于定位文件(尽量填写,不传则使用默认值 mcp-agent)")), mcp.WithString("sessionId", mcp.Description("会话标识,用于定位文件(尽量填写,不传则使用默认值 mcp-session)")), ), handleFileList, ) s.AddTool( mcp.NewTool("storage_file_upload", mcp.WithDescription("将本地文件上传到文件存储(MinIO)。支持任意文件类型(图片、PDF、压缩包、Excel 等)。最大 100MB。直接读取本地文件路径,无需 base64 编码。"), mcp.WithString("localPath", mcp.Required(), mcp.Description("本地文件的绝对路径(如 /tmp/data.zip、/home/user/report.pdf)")), mcp.WithString("fileName", mcp.Description("上传后的文件名(不传则使用本地文件名)")), mcp.WithString("userId", mcp.Description("用户标识,用于文件路径隔离(尽量填写,不传则使用默认值 mcp-agent)")), mcp.WithString("sessionId", mcp.Description("会话标识,用于文件路径隔离(尽量填写,不传则使用默认值 mcp-session)")), ), handleFileUpload, ) } func handleFileRead(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { key, err := req.RequireString("key") if err != nil { return helper.ParamError("key"), nil } if !filestore.HasFile(key) { return helper.EmptyResult(fmt.Sprintf("文件不存在: %s", key)), nil } content, err := filestore.ReadFile(key) if err != nil { return helper.ServiceError("读取文件", err), nil } return helper.TextResultOrFile(content, "file_read"), nil } func handleFileReadToLocal(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { key, err := req.RequireString("key") if err != nil { return helper.ParamError("key"), nil } if !filestore.HasFile(key) { return helper.EmptyResult(fmt.Sprintf("文件不存在: %s", key)), nil } data, err := filestore.ReadFileBytes(key) if err != nil { return helper.ServiceError("读取文件", err), nil } // 确保临时目录存在 if err := os.MkdirAll(localTempDir, 0755); err != nil { return helper.ServiceError("创建临时目录", err), nil } // 从 key 还原文件名:去掉 "{uuid}_" 前缀(8位uuid + 下划线 = 9字符) fileName := filepath.Base(key) if len(fileName) > 9 { fileName = fileName[9:] } localPath := filepath.Join(localTempDir, fileName) if err := os.WriteFile(localPath, data, 0644); err != nil { return helper.ServiceError("写入本地文件", err), nil } return helper.JSONResultOrFileAuto(map[string]any{ "message": "文件已下载到本地", "localPath": localPath, "fileName": fileName, "size": len(data), }), nil } func handleFileList(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { userId := req.GetString("userId", "mcp-agent") sessionId := req.GetString("sessionId", "mcp-session") files, err := filestore.GetFiles(uploadPrefix, userId, sessionId) if err != nil { return helper.ServiceError("列举文件", err), nil } if len(files) == 0 { return helper.EmptyResult("当前没有上传的文件"), nil } return helper.JSONResultOrFileAuto(files), nil } func handleFileWrite(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { fileName, err := req.RequireString("fileName") if err != nil { return helper.ParamError("fileName"), nil } content, err := req.RequireString("content") if err != nil { return helper.ParamError("content"), nil } // 限制文本内容大小(最大 20MB) const maxSize = 20 * 1024 * 1024 if len(content) > maxSize { return helper.ParamInvalidError("content", fmt.Sprintf("内容大小 %d bytes 超过 20MB 限制", len(content))), nil } userId := req.GetString("userId", "mcp-agent") sessionId := req.GetString("sessionId", "mcp-session") // 仅取文件名部分,上传到 "download" 前缀(供用户下载) record, err := filestore.DownloadFile(userId, sessionId, filepath.Base(fileName), content) if err != nil { return helper.ServiceError("写入文件", err), nil } return helper.JSONResultOrFileAuto(map[string]any{ "message": "文件写入成功", "fileName": record.FileName, "size": record.Size, "key": record.Key, }), nil } func handleFileUpload(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { localPath, err := req.RequireString("localPath") if err != nil { return helper.ParamError("localPath"), nil } // 检查本地文件是否存在 info, err := os.Stat(localPath) if err != nil { if os.IsNotExist(err) { return helper.ParamInvalidError("localPath", fmt.Sprintf("文件不存在: %s", localPath)), nil } return helper.ServiceError("检查文件", err), nil } if info.IsDir() { return helper.ParamInvalidError("localPath", "路径是目录,不是文件"), nil } // 限制文件大小(最大 100MB) const maxSize = 100 * 1024 * 1024 if info.Size() > maxSize { return helper.ParamInvalidError("localPath", fmt.Sprintf("文件大小 %d bytes 超过 100MB 限制", info.Size())), nil } // 读取本地文件 data, err := os.ReadFile(localPath) if err != nil { return helper.ServiceError("读取本地文件", err), nil } // 确定文件名 fileName := req.GetString("fileName", "") if fileName == "" { fileName = filepath.Base(localPath) } userId := req.GetString("userId", "mcp-agent") sessionId := req.GetString("sessionId", "mcp-session") // 上传到 "download" 前缀(供用户下载),仅取文件名部分 record, err := filestore.DownloadBytes(userId, sessionId, filepath.Base(fileName), data) if err != nil { return helper.ServiceError("上传文件", err), nil } return helper.JSONResultOrFileAuto(map[string]any{ "message": "文件上传成功", "fileName": record.FileName, "size": record.Size, "key": record.Key, }), nil }