file.go 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. package storage
  2. import (
  3. "context"
  4. "encoding/base64"
  5. "fmt"
  6. "path/filepath"
  7. "github.com/mark3labs/mcp-go/mcp"
  8. "github.com/mark3labs/mcp-go/server"
  9. "fwh-toolbox-server/cmd/mcp/tools/helper"
  10. "fwh-toolbox-server/utils/filestore"
  11. )
  12. // mcpFileStore MCP 专用的文件存储实例
  13. var mcpFileStore *filestore.FileStore
  14. // InitFileStore 初始化 MCP FileStore(由 main.go 调用)
  15. func InitFileStore(fs *filestore.FileStore) {
  16. mcpFileStore = fs
  17. }
  18. // GetFileStore 获取 MCP FileStore 实例
  19. func GetFileStore() *filestore.FileStore {
  20. return mcpFileStore
  21. }
  22. func registerFileTools(s *server.MCPServer) {
  23. s.AddTool(
  24. mcp.NewTool("storage_file_read",
  25. mcp.WithDescription("读取用户上传的文本文件内容(返回纯文本)。用户通过前端上传的文件存储在 /uploads/ 路径下。仅适用于文本类文件。"),
  26. mcp.WithString("path", mcp.Required(), mcp.Description("文件路径,格式为 /uploads/{filename}")),
  27. ),
  28. handleFileRead,
  29. )
  30. s.AddTool(
  31. mcp.NewTool("storage_file_read_base64",
  32. mcp.WithDescription("读取用户上传的文件内容并返回 base64 编码。支持任意文件类型(图片、PDF、压缩包等)。图片文件读取后可直接用于 AI 视觉分析。"),
  33. mcp.WithString("path", mcp.Required(), mcp.Description("文件路径,格式为 /uploads/{filename}")),
  34. ),
  35. handleFileReadBase64,
  36. )
  37. s.AddTool(
  38. mcp.NewTool("storage_file_write",
  39. mcp.WithDescription("将文本内容写入文件存储(MinIO)。可用于保存分析结果、生成报告等。最大 20MB。文件路径会自动添加 /uploads/ 前缀。如需上传二进制文件请使用 storage_file_upload。"),
  40. mcp.WithString("path", mcp.Required(), mcp.Description("文件路径,格式为 /uploads/{filename}(如 /uploads/report.csv)")),
  41. mcp.WithString("content", mcp.Required(), mcp.Description("要写入的文本内容(最大 20MB)")),
  42. mcp.WithString("userId", mcp.Description("用户标识,用于文件路径隔离(尽量填写,不传则使用默认值 mcp-agent)")),
  43. mcp.WithString("sessionId", mcp.Description("会话标识,用于文件路径隔离(尽量填写,不传则使用默认值 mcp-session)")),
  44. ),
  45. handleFileWrite,
  46. )
  47. s.AddTool(
  48. mcp.NewTool("storage_file_list",
  49. mcp.WithDescription("列出当前会话中用户上传的所有文件,返回文件名、路径、大小等信息。"),
  50. ),
  51. handleFileList,
  52. )
  53. s.AddTool(
  54. mcp.NewTool("storage_file_upload",
  55. mcp.WithDescription("将任意类型文件上传到文件存储(MinIO)。内容需使用 base64 编码传入。支持二进制文件(图片、PDF、压缩包、Excel 等)和文本文件。最大 100MB。"),
  56. mcp.WithString("fileName", mcp.Required(), mcp.Description("目标文件名(如 data.zip、report.pdf、image.png)")),
  57. mcp.WithString("content_base64", mcp.Required(), mcp.Description("文件内容的 base64 编码字符串(可通过读取本地文件后 base64 编码获得)")),
  58. mcp.WithString("userId", mcp.Description("用户标识,用于文件路径隔离(尽量填写,不传则使用默认值 mcp-agent)")),
  59. mcp.WithString("sessionId", mcp.Description("会话标识,用于文件路径隔离(尽量填写,不传则使用默认值 mcp-session)")),
  60. ),
  61. handleFileUpload,
  62. )
  63. }
  64. func handleFileRead(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
  65. path, err := req.RequireString("path")
  66. if err != nil {
  67. return helper.ParamError("path"), nil
  68. }
  69. if mcpFileStore == nil {
  70. return helper.ServiceError("文件存储", fmt.Errorf("未初始化")), nil
  71. }
  72. if !mcpFileStore.HasFile(path) {
  73. return helper.EmptyResult(fmt.Sprintf("文件不存在: %s", path)), nil
  74. }
  75. content, err := mcpFileStore.ReadFile(path)
  76. if err != nil {
  77. return helper.ServiceError("读取文件", err), nil
  78. }
  79. return helper.TextResult(content), nil
  80. }
  81. func handleFileReadBase64(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
  82. path, err := req.RequireString("path")
  83. if err != nil {
  84. return helper.ParamError("path"), nil
  85. }
  86. if mcpFileStore == nil {
  87. return helper.ServiceError("文件存储", fmt.Errorf("未初始化")), nil
  88. }
  89. if !mcpFileStore.HasFile(path) {
  90. return helper.EmptyResult(fmt.Sprintf("文件不存在: %s", path)), nil
  91. }
  92. data, err := mcpFileStore.ReadFileBytes(path)
  93. if err != nil {
  94. return helper.ServiceError("读取文件", err), nil
  95. }
  96. encoded := base64.StdEncoding.EncodeToString(data)
  97. return helper.JSONResult(map[string]any{
  98. "path": path,
  99. "size": len(data),
  100. "content_base64": encoded,
  101. }), nil
  102. }
  103. func handleFileList(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
  104. if mcpFileStore == nil {
  105. return helper.ServiceError("文件存储", fmt.Errorf("未初始化")), nil
  106. }
  107. files := mcpFileStore.GetFiles()
  108. if len(files) == 0 {
  109. return helper.EmptyResult("当前没有上传的文件"), nil
  110. }
  111. return helper.JSONResult(files), nil
  112. }
  113. func handleFileWrite(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
  114. path, err := req.RequireString("path")
  115. if err != nil {
  116. return helper.ParamError("path"), nil
  117. }
  118. content, err := req.RequireString("content")
  119. if err != nil {
  120. return helper.ParamError("content"), nil
  121. }
  122. // 限制文本内容大小(最大 20MB)
  123. const maxSize = 20 * 1024 * 1024
  124. if len(content) > maxSize {
  125. return helper.ParamInvalidError("content", fmt.Sprintf("内容大小 %d bytes 超过 20MB 限制", len(content))), nil
  126. }
  127. if mcpFileStore == nil {
  128. return helper.ServiceError("文件存储", fmt.Errorf("未初始化")), nil
  129. }
  130. userId := req.GetString("userId", "mcp-agent")
  131. sessionId := req.GetString("sessionId", "mcp-session")
  132. record, err := mcpFileStore.WriteFile(userId, sessionId, path, content)
  133. if err != nil {
  134. return helper.ServiceError("写入文件", err), nil
  135. }
  136. return helper.JSONResult(map[string]any{
  137. "message": "文件写入成功",
  138. "path": record.Path,
  139. "fileName": record.FileName,
  140. "size": record.Size,
  141. "key": record.Key,
  142. }), nil
  143. }
  144. func handleFileUpload(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
  145. fileName, err := req.RequireString("fileName")
  146. if err != nil {
  147. return helper.ParamError("fileName"), nil
  148. }
  149. contentBase64, err := req.RequireString("content_base64")
  150. if err != nil {
  151. return helper.ParamError("content_base64"), nil
  152. }
  153. if mcpFileStore == nil {
  154. return helper.ServiceError("文件存储", fmt.Errorf("未初始化")), nil
  155. }
  156. // base64 解码
  157. data, err := base64.StdEncoding.DecodeString(contentBase64)
  158. if err != nil {
  159. // 尝试 RawStdEncoding(无 padding)
  160. data, err = base64.RawStdEncoding.DecodeString(contentBase64)
  161. if err != nil {
  162. return helper.ParamInvalidError("content_base64", "base64 解码失败"), nil
  163. }
  164. }
  165. // 限制文件大小(最大 100MB)
  166. const maxSize = 100 * 1024 * 1024
  167. if len(data) > maxSize {
  168. return helper.ParamInvalidError("content_base64", fmt.Sprintf("文件大小 %d bytes 超过 100MB 限制", len(data))), nil
  169. }
  170. userId := req.GetString("userId", "mcp-agent")
  171. sessionId := req.GetString("sessionId", "mcp-session")
  172. virtualPath := fmt.Sprintf("/uploads/%s", filepath.Base(fileName))
  173. record, err := mcpFileStore.WriteBytesWithSession(userId, sessionId, virtualPath, data)
  174. if err != nil {
  175. return helper.ServiceError("上传文件", err), nil
  176. }
  177. return helper.JSONResult(map[string]any{
  178. "message": "文件上传成功",
  179. "path": record.Path,
  180. "fileName": record.FileName,
  181. "size": record.Size,
  182. "key": record.Key,
  183. }), nil
  184. }