|
|
@@ -2,7 +2,9 @@ package storage
|
|
|
|
|
|
import (
|
|
|
"context"
|
|
|
+ "encoding/base64"
|
|
|
"fmt"
|
|
|
+ "path/filepath"
|
|
|
|
|
|
"github.com/mark3labs/mcp-go/mcp"
|
|
|
"github.com/mark3labs/mcp-go/server"
|
|
|
@@ -27,17 +29,25 @@ func GetFileStore() *filestore.FileStore {
|
|
|
func registerFileTools(s *server.MCPServer) {
|
|
|
s.AddTool(
|
|
|
mcp.NewTool("storage_file_read",
|
|
|
- mcp.WithDescription("读取用户上传的文件内容。用户通过前端上传的文件存储在 /uploads/ 路径下。"),
|
|
|
+ mcp.WithDescription("读取用户上传的文本文件内容(返回纯文本)。用户通过前端上传的文件存储在 /uploads/ 路径下。仅适用于文本类文件。"),
|
|
|
mcp.WithString("path", mcp.Required(), mcp.Description("文件路径,格式为 /uploads/{filename}")),
|
|
|
),
|
|
|
handleFileRead,
|
|
|
)
|
|
|
|
|
|
+ s.AddTool(
|
|
|
+ mcp.NewTool("storage_file_read_base64",
|
|
|
+ mcp.WithDescription("读取用户上传的文件内容并返回 base64 编码。支持任意文件类型(图片、PDF、压缩包等)。图片文件读取后可直接用于 AI 视觉分析。"),
|
|
|
+ mcp.WithString("path", mcp.Required(), mcp.Description("文件路径,格式为 /uploads/{filename}")),
|
|
|
+ ),
|
|
|
+ handleFileReadBase64,
|
|
|
+ )
|
|
|
+
|
|
|
s.AddTool(
|
|
|
mcp.NewTool("storage_file_write",
|
|
|
- mcp.WithDescription("将文本内容写入文件存储(MinIO)。可用于保存分析结果、生成报告等。文件路径会自动添加 /uploads/ 前缀。"),
|
|
|
+ mcp.WithDescription("将文本内容写入文件存储(MinIO)。可用于保存分析结果、生成报告等。最大 20MB。文件路径会自动添加 /uploads/ 前缀。如需上传二进制文件请使用 storage_file_upload。"),
|
|
|
mcp.WithString("path", mcp.Required(), mcp.Description("文件路径,格式为 /uploads/{filename}(如 /uploads/report.csv)")),
|
|
|
- mcp.WithString("content", mcp.Required(), mcp.Description("要写入的文本内容")),
|
|
|
+ mcp.WithString("content", mcp.Required(), mcp.Description("要写入的文本内容(最大 20MB)")),
|
|
|
mcp.WithString("userId", mcp.Description("用户标识,用于文件路径隔离(尽量填写,不传则使用默认值 mcp-agent)")),
|
|
|
mcp.WithString("sessionId", mcp.Description("会话标识,用于文件路径隔离(尽量填写,不传则使用默认值 mcp-session)")),
|
|
|
),
|
|
|
@@ -50,6 +60,17 @@ func registerFileTools(s *server.MCPServer) {
|
|
|
),
|
|
|
handleFileList,
|
|
|
)
|
|
|
+
|
|
|
+ s.AddTool(
|
|
|
+ mcp.NewTool("storage_file_upload",
|
|
|
+ mcp.WithDescription("将任意类型文件上传到文件存储(MinIO)。内容需使用 base64 编码传入。支持二进制文件(图片、PDF、压缩包、Excel 等)和文本文件。最大 100MB。"),
|
|
|
+ mcp.WithString("fileName", mcp.Required(), mcp.Description("目标文件名(如 data.zip、report.pdf、image.png)")),
|
|
|
+ mcp.WithString("content_base64", mcp.Required(), mcp.Description("文件内容的 base64 编码字符串(可通过读取本地文件后 base64 编码获得)")),
|
|
|
+ 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) {
|
|
|
@@ -74,6 +95,34 @@ func handleFileRead(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTool
|
|
|
return helper.TextResult(content), nil
|
|
|
}
|
|
|
|
|
|
+func handleFileReadBase64(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
|
+ path, err := req.RequireString("path")
|
|
|
+ if err != nil {
|
|
|
+ return helper.ParamError("path"), nil
|
|
|
+ }
|
|
|
+
|
|
|
+ if mcpFileStore == nil {
|
|
|
+ return helper.ServiceError("文件存储", fmt.Errorf("未初始化")), nil
|
|
|
+ }
|
|
|
+
|
|
|
+ if !mcpFileStore.HasFile(path) {
|
|
|
+ return helper.EmptyResult(fmt.Sprintf("文件不存在: %s", path)), nil
|
|
|
+ }
|
|
|
+
|
|
|
+ data, err := mcpFileStore.ReadFileBytes(path)
|
|
|
+ if err != nil {
|
|
|
+ return helper.ServiceError("读取文件", err), nil
|
|
|
+ }
|
|
|
+
|
|
|
+ encoded := base64.StdEncoding.EncodeToString(data)
|
|
|
+
|
|
|
+ return helper.JSONResult(map[string]any{
|
|
|
+ "path": path,
|
|
|
+ "size": len(data),
|
|
|
+ "content_base64": encoded,
|
|
|
+ }), nil
|
|
|
+}
|
|
|
+
|
|
|
func handleFileList(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
|
if mcpFileStore == nil {
|
|
|
return helper.ServiceError("文件存储", fmt.Errorf("未初始化")), nil
|
|
|
@@ -97,6 +146,12 @@ func handleFileWrite(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToo
|
|
|
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
|
|
|
+ }
|
|
|
+
|
|
|
if mcpFileStore == nil {
|
|
|
return helper.ServiceError("文件存储", fmt.Errorf("未初始化")), nil
|
|
|
}
|
|
|
@@ -117,3 +172,52 @@ func handleFileWrite(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToo
|
|
|
"key": record.Key,
|
|
|
}), nil
|
|
|
}
|
|
|
+
|
|
|
+func handleFileUpload(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
|
+ fileName, err := req.RequireString("fileName")
|
|
|
+ if err != nil {
|
|
|
+ return helper.ParamError("fileName"), nil
|
|
|
+ }
|
|
|
+ contentBase64, err := req.RequireString("content_base64")
|
|
|
+ if err != nil {
|
|
|
+ return helper.ParamError("content_base64"), nil
|
|
|
+ }
|
|
|
+
|
|
|
+ if mcpFileStore == nil {
|
|
|
+ return helper.ServiceError("文件存储", fmt.Errorf("未初始化")), nil
|
|
|
+ }
|
|
|
+
|
|
|
+ // base64 解码
|
|
|
+ data, err := base64.StdEncoding.DecodeString(contentBase64)
|
|
|
+ if err != nil {
|
|
|
+ // 尝试 RawStdEncoding(无 padding)
|
|
|
+ data, err = base64.RawStdEncoding.DecodeString(contentBase64)
|
|
|
+ if err != nil {
|
|
|
+ return helper.ParamInvalidError("content_base64", "base64 解码失败"), nil
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 限制文件大小(最大 100MB)
|
|
|
+ const maxSize = 100 * 1024 * 1024
|
|
|
+ if len(data) > maxSize {
|
|
|
+ return helper.ParamInvalidError("content_base64", fmt.Sprintf("文件大小 %d bytes 超过 100MB 限制", len(data))), nil
|
|
|
+ }
|
|
|
+
|
|
|
+ userId := req.GetString("userId", "mcp-agent")
|
|
|
+ sessionId := req.GetString("sessionId", "mcp-session")
|
|
|
+
|
|
|
+ virtualPath := fmt.Sprintf("/uploads/%s", filepath.Base(fileName))
|
|
|
+
|
|
|
+ record, err := mcpFileStore.WriteBytesWithSession(userId, sessionId, virtualPath, data)
|
|
|
+ if err != nil {
|
|
|
+ return helper.ServiceError("上传文件", err), nil
|
|
|
+ }
|
|
|
+
|
|
|
+ return helper.JSONResult(map[string]any{
|
|
|
+ "message": "文件上传成功",
|
|
|
+ "path": record.Path,
|
|
|
+ "fileName": record.FileName,
|
|
|
+ "size": record.Size,
|
|
|
+ "key": record.Key,
|
|
|
+ }), nil
|
|
|
+}
|