package storage import ( "context" "fmt" "io" "strings" "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" "github.com/minio/minio-go/v7" "fwh-toolbox-server/cmd/mcp/tools/helper" minioUtil "fwh-toolbox-server/utils/minio" ) // minioAllowedBuckets 允许访问的 bucket 白名单 var minioAllowedBuckets = []string{ "fwh-toolbox", "fwh-toolbox-ai-file", } func registerMinioTools(s *server.MCPServer) { s.AddTool( mcp.NewTool("storage_minio_list_objects", mcp.WithDescription("列出 MinIO bucket 中的对象(只读),用于查看已下载的文件"), mcp.WithString("bucket", mcp.Required(), mcp.Description( fmt.Sprintf("Bucket 名称,可选: %v", minioAllowedBuckets), )), mcp.WithString("prefix", mcp.Description("对象前缀过滤(如 SN 号或日期路径)")), mcp.WithNumber("limit", mcp.Description("返回数量限制,默认20,最大100")), ), handleMinioListObjects, ) s.AddTool( mcp.NewTool("storage_minio_read_object", mcp.WithDescription("读取 MinIO 中的文本文件内容(只读,仅支持文本类型文件,最大 1MB)"), mcp.WithString("bucket", mcp.Required(), mcp.Description( fmt.Sprintf("Bucket 名称,可选: %v", minioAllowedBuckets), )), mcp.WithString("key", mcp.Required(), mcp.Description("对象 key(完整路径)")), ), handleMinioReadObject, ) } func handleMinioListObjects(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { bucket, err := req.RequireString("bucket") if err != nil { return helper.ParamError("bucket"), nil } if !isAllowedBucket(bucket) { return helper.ParamInvalidError("bucket", fmt.Sprintf("不允许访问此 bucket,可选: %v", minioAllowedBuckets)), nil } prefix := req.GetString("prefix", "") limit := int(req.GetFloat("limit", 20)) if limit > 100 { limit = 100 } opts := minio.ListObjectsOptions{ Prefix: prefix, Recursive: true, } type ObjectInfo struct { Key string `json:"key"` Size int64 `json:"size"` LastModified string `json:"lastModified"` ContentType string `json:"contentType,omitempty"` } var objects []ObjectInfo for obj := range minioUtil.Client.ListObjects(ctx, bucket, opts) { if obj.Err != nil { return helper.ServiceError("列出MinIO对象", obj.Err), nil } objects = append(objects, ObjectInfo{ Key: obj.Key, Size: obj.Size, LastModified: obj.LastModified.Format("2006-01-02 15:04:05"), ContentType: obj.ContentType, }) if len(objects) >= limit { break } } if len(objects) == 0 { return helper.EmptyResult(fmt.Sprintf("bucket %s 中无匹配对象 (prefix: %s)", bucket, prefix)), nil } return helper.JSONResult(map[string]any{ "bucket": bucket, "prefix": prefix, "count": len(objects), "objects": objects, }), nil } func handleMinioReadObject(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { bucket, err := req.RequireString("bucket") if err != nil { return helper.ParamError("bucket"), nil } key, err := req.RequireString("key") if err != nil { return helper.ParamError("key"), nil } if !isAllowedBucket(bucket) { return helper.ParamInvalidError("bucket", fmt.Sprintf("不允许访问此 bucket,可选: %v", minioAllowedBuckets)), nil } // 获取对象信息 stat, err := minioUtil.Client.StatObject(ctx, bucket, key, minio.StatObjectOptions{}) if err != nil { return helper.ServiceError("获取对象信息", err), nil } // 限制大小:仅允许读取 1MB 以内的文本文件 const maxSize = 1 * 1024 * 1024 if stat.Size > maxSize { return helper.ParamInvalidError("key", fmt.Sprintf("文件过大 (%d bytes),仅支持读取 1MB 以内的文本文件", stat.Size)), nil } // 检查是否为文本类型 if !isTextContentType(stat.ContentType) { return helper.ParamInvalidError("key", fmt.Sprintf("非文本文件 (content-type: %s),不支持读取", stat.ContentType)), nil } obj, err := minioUtil.Client.GetObject(ctx, bucket, key, minio.GetObjectOptions{}) if err != nil { return helper.ServiceError("读取MinIO对象", err), nil } defer obj.Close() data, err := io.ReadAll(obj) if err != nil { return helper.ServiceError("读取对象内容", err), nil } return helper.TextResult(string(data)), nil } func isAllowedBucket(name string) bool { for _, b := range minioAllowedBuckets { if b == name { return true } } return false } func isTextContentType(ct string) bool { textTypes := []string{ "text/", "application/json", "application/xml", "application/csv", "application/javascript", } for _, t := range textTypes { if strings.HasPrefix(ct, t) || strings.Contains(ct, t) { return true } } return false }