| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247 |
- 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"
- )
- // mcpFileStore MCP 专用的文件存储实例
- var mcpFileStore *filestore.FileStore
- // InitFileStore 初始化 MCP FileStore(由 main.go 调用)
- func InitFileStore(fs *filestore.FileStore) {
- mcpFileStore = fs
- }
- // GetFileStore 获取 MCP FileStore 实例
- func GetFileStore() *filestore.FileStore {
- return mcpFileStore
- }
- // localTempDir 用于存放从 MinIO 下载的临时文件
- const localTempDir = "/tmp/fwh-toolbox-agent/downloads"
- func registerFileTools(s *server.MCPServer) {
- s.AddTool(
- mcp.NewTool("storage_file_read",
- mcp.WithDescription("读取用户上传的文本文件内容(返回纯文本)。用户通过前端上传的文件存储在 /uploads/ 路径下。仅适用于文本类文件。"),
- mcp.WithString("path", mcp.Required(), mcp.Description("文件路径,格式为 /uploads/{filename}")),
- ),
- handleFileRead,
- )
- s.AddTool(
- mcp.NewTool("storage_file_read_to_local",
- mcp.WithDescription("将用户上传的文件下载到本地临时路径。支持任意文件类型(图片、PDF、压缩包等)。返回本地文件路径,可直接用于后续处理或分析。"),
- mcp.WithString("path", mcp.Required(), mcp.Description("文件路径,格式为 /uploads/{filename}")),
- ),
- handleFileReadToLocal,
- )
- s.AddTool(
- mcp.NewTool("storage_file_write",
- 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("要写入的文本内容(最大 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("列出当前会话中用户上传的所有文件,返回文件名、路径、大小等信息。"),
- ),
- 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) {
- 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
- }
- content, err := mcpFileStore.ReadFile(path)
- if err != nil {
- return helper.ServiceError("读取文件", err), nil
- }
- return helper.TextResult(content), nil
- }
- func handleFileReadToLocal(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
- }
- // 确保临时目录存在
- if err := os.MkdirAll(localTempDir, 0755); err != nil {
- return helper.ServiceError("创建临时目录", err), nil
- }
- // 写入本地临时文件
- fileName := filepath.Base(path)
- localPath := filepath.Join(localTempDir, fileName)
- if err := os.WriteFile(localPath, data, 0644); err != nil {
- return helper.ServiceError("写入本地文件", err), nil
- }
- return helper.JSONResult(map[string]any{
- "message": "文件已下载到本地",
- "localPath": localPath,
- "fileName": fileName,
- "size": len(data),
- }), nil
- }
- func handleFileList(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
- if mcpFileStore == nil {
- return helper.ServiceError("文件存储", fmt.Errorf("未初始化")), nil
- }
- files := mcpFileStore.GetFiles()
- if len(files) == 0 {
- return helper.EmptyResult("当前没有上传的文件"), nil
- }
- return helper.JSONResult(files), nil
- }
- func handleFileWrite(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
- path, err := req.RequireString("path")
- if err != nil {
- return helper.ParamError("path"), 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
- }
- if mcpFileStore == nil {
- return helper.ServiceError("文件存储", fmt.Errorf("未初始化")), nil
- }
- userId := req.GetString("userId", "mcp-agent")
- sessionId := req.GetString("sessionId", "mcp-session")
- record, err := mcpFileStore.WriteFile(userId, sessionId, path, content)
- 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
- }
- func handleFileUpload(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
- localPath, err := req.RequireString("localPath")
- if err != nil {
- return helper.ParamError("localPath"), nil
- }
- if mcpFileStore == nil {
- return helper.ServiceError("文件存储", fmt.Errorf("未初始化")), 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")
- 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
- }
|