minio.go 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  1. package storage
  2. import (
  3. "context"
  4. "fmt"
  5. "io"
  6. "strings"
  7. "github.com/mark3labs/mcp-go/mcp"
  8. "github.com/mark3labs/mcp-go/server"
  9. "github.com/minio/minio-go/v7"
  10. "fwh-toolbox-server/cmd/mcp/tools/helper"
  11. minioUtil "fwh-toolbox-server/utils/minio"
  12. )
  13. // minioAllowedBuckets 允许访问的 bucket 白名单
  14. var minioAllowedBuckets = []string{
  15. "fwh-toolbox",
  16. "fwh-toolbox-ai-file",
  17. }
  18. func registerMinioTools(s *server.MCPServer) {
  19. s.AddTool(
  20. mcp.NewTool("storage_minio_list_objects",
  21. mcp.WithDescription("列出 MinIO bucket 中的对象(只读),用于查看已下载的文件"),
  22. mcp.WithString("bucket", mcp.Required(), mcp.Description(
  23. fmt.Sprintf("Bucket 名称,可选: %v", minioAllowedBuckets),
  24. )),
  25. mcp.WithString("prefix", mcp.Description("对象前缀过滤(如 SN 号或日期路径)")),
  26. mcp.WithNumber("limit", mcp.Description("返回数量限制,默认20,最大100")),
  27. ),
  28. handleMinioListObjects,
  29. )
  30. s.AddTool(
  31. mcp.NewTool("storage_minio_read_object",
  32. mcp.WithDescription("读取 MinIO 中的文本文件内容(只读,仅支持文本类型文件,最大 1MB)"),
  33. mcp.WithString("bucket", mcp.Required(), mcp.Description(
  34. fmt.Sprintf("Bucket 名称,可选: %v", minioAllowedBuckets),
  35. )),
  36. mcp.WithString("key", mcp.Required(), mcp.Description("对象 key(完整路径)")),
  37. ),
  38. handleMinioReadObject,
  39. )
  40. }
  41. func handleMinioListObjects(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
  42. bucket, err := req.RequireString("bucket")
  43. if err != nil {
  44. return helper.ParamError("bucket"), nil
  45. }
  46. if !isAllowedBucket(bucket) {
  47. return helper.ParamInvalidError("bucket", fmt.Sprintf("不允许访问此 bucket,可选: %v", minioAllowedBuckets)), nil
  48. }
  49. prefix := req.GetString("prefix", "")
  50. limit := int(req.GetFloat("limit", 20))
  51. if limit > 100 {
  52. limit = 100
  53. }
  54. opts := minio.ListObjectsOptions{
  55. Prefix: prefix,
  56. Recursive: true,
  57. }
  58. type ObjectInfo struct {
  59. Key string `json:"key"`
  60. Size int64 `json:"size"`
  61. LastModified string `json:"lastModified"`
  62. ContentType string `json:"contentType,omitempty"`
  63. }
  64. var objects []ObjectInfo
  65. for obj := range minioUtil.Client.ListObjects(ctx, bucket, opts) {
  66. if obj.Err != nil {
  67. return helper.ServiceError("列出MinIO对象", obj.Err), nil
  68. }
  69. objects = append(objects, ObjectInfo{
  70. Key: obj.Key,
  71. Size: obj.Size,
  72. LastModified: obj.LastModified.Format("2006-01-02 15:04:05"),
  73. ContentType: obj.ContentType,
  74. })
  75. if len(objects) >= limit {
  76. break
  77. }
  78. }
  79. if len(objects) == 0 {
  80. return helper.EmptyResult(fmt.Sprintf("bucket %s 中无匹配对象 (prefix: %s)", bucket, prefix)), nil
  81. }
  82. return helper.JSONResultOrFileAuto(map[string]any{
  83. "bucket": bucket,
  84. "prefix": prefix,
  85. "count": len(objects),
  86. "objects": objects,
  87. }), nil
  88. }
  89. func handleMinioReadObject(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
  90. bucket, err := req.RequireString("bucket")
  91. if err != nil {
  92. return helper.ParamError("bucket"), nil
  93. }
  94. key, err := req.RequireString("key")
  95. if err != nil {
  96. return helper.ParamError("key"), nil
  97. }
  98. if !isAllowedBucket(bucket) {
  99. return helper.ParamInvalidError("bucket", fmt.Sprintf("不允许访问此 bucket,可选: %v", minioAllowedBuckets)), nil
  100. }
  101. // 获取对象信息
  102. stat, err := minioUtil.Client.StatObject(ctx, bucket, key, minio.StatObjectOptions{})
  103. if err != nil {
  104. return helper.ServiceError("获取对象信息", err), nil
  105. }
  106. // 限制大小:仅允许读取 1MB 以内的文本文件
  107. const maxSize = 1 * 1024 * 1024
  108. if stat.Size > maxSize {
  109. return helper.ParamInvalidError("key", fmt.Sprintf("文件过大 (%d bytes),仅支持读取 1MB 以内的文本文件", stat.Size)), nil
  110. }
  111. // 检查是否为文本类型
  112. if !isTextContentType(stat.ContentType) {
  113. return helper.ParamInvalidError("key", fmt.Sprintf("非文本文件 (content-type: %s),不支持读取", stat.ContentType)), nil
  114. }
  115. obj, err := minioUtil.Client.GetObject(ctx, bucket, key, minio.GetObjectOptions{})
  116. if err != nil {
  117. return helper.ServiceError("读取MinIO对象", err), nil
  118. }
  119. defer obj.Close()
  120. data, err := io.ReadAll(obj)
  121. if err != nil {
  122. return helper.ServiceError("读取对象内容", err), nil
  123. }
  124. return helper.TextResultOrFile(string(data), "minio_object"), nil
  125. }
  126. func isAllowedBucket(name string) bool {
  127. for _, b := range minioAllowedBuckets {
  128. if b == name {
  129. return true
  130. }
  131. }
  132. return false
  133. }
  134. func isTextContentType(ct string) bool {
  135. textTypes := []string{
  136. "text/",
  137. "application/json",
  138. "application/xml",
  139. "application/csv",
  140. "application/javascript",
  141. }
  142. for _, t := range textTypes {
  143. if strings.HasPrefix(ct, t) || strings.Contains(ct, t) {
  144. return true
  145. }
  146. }
  147. return false
  148. }