From 3a21fab9e2170a607c47c400621f0fb8f3116574 Mon Sep 17 00:00:00 2001 From: Acbox Date: Tue, 3 Feb 2026 23:46:44 +0800 Subject: [PATCH] feat: add fs.read_base64, fs.mkdir, fs.rename tools --- internal/mcp/tools.go | 123 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) diff --git a/internal/mcp/tools.go b/internal/mcp/tools.go index a7f85d9a..d45bbc8f 100644 --- a/internal/mcp/tools.go +++ b/internal/mcp/tools.go @@ -2,8 +2,10 @@ package mcp import ( "context" + "encoding/base64" "fmt" "io/fs" + "mime" "os" "path/filepath" "strconv" @@ -81,14 +83,43 @@ type FSApplyPatchOutput struct { OK bool `json:"ok" jsonschema:"apply result"` } +type FSMkdirInput struct { + Path string `json:"path" jsonschema:"relative directory path"` +} + +type FSMkdirOutput struct { + OK bool `json:"ok" jsonschema:"mkdir result"` +} + +type FSRenameInput struct { + Source string `json:"source" jsonschema:"relative source path"` + Destination string `json:"destination" jsonschema:"relative destination path"` +} + +type FSRenameOutput struct { + OK bool `json:"ok" jsonschema:"rename result"` +} + +type FSReadBase64Input struct { + Path string `json:"path" jsonschema:"relative file path"` +} + +type FSReadBase64Output struct { + Data string `json:"data" jsonschema:"base64-encoded file bytes"` + MimeType string `json:"mime_type" jsonschema:"detected mime type"` +} + func RegisterTools(server *sdkmcp.Server) { sdkmcp.AddTool(server, &sdkmcp.Tool{Name: "echo", Description: "echo input text"}, echoTool) sdkmcp.AddTool(server, &sdkmcp.Tool{Name: "fs.read", Description: "read file content"}, fsReadTool) + sdkmcp.AddTool(server, &sdkmcp.Tool{Name: "fs.read_base64", Description: "read file bytes as base64"}, fsReadBase64Tool) sdkmcp.AddTool(server, &sdkmcp.Tool{Name: "fs.write", Description: "write file content"}, fsWriteTool) sdkmcp.AddTool(server, &sdkmcp.Tool{Name: "fs.list", Description: "list directory entries"}, fsListTool) sdkmcp.AddTool(server, &sdkmcp.Tool{Name: "fs.stat", Description: "stat file or directory"}, fsStatTool) sdkmcp.AddTool(server, &sdkmcp.Tool{Name: "fs.delete", Description: "delete file or directory"}, fsDeleteTool) sdkmcp.AddTool(server, &sdkmcp.Tool{Name: "fs.apply_patch", Description: "apply unified diff patch"}, fsApplyPatchTool) + sdkmcp.AddTool(server, &sdkmcp.Tool{Name: "fs.mkdir", Description: "create directory (mkdir -p)"}, fsMkdirTool) + sdkmcp.AddTool(server, &sdkmcp.Tool{Name: "fs.rename", Description: "rename/move file or directory"}, fsRenameTool) } func echoTool(ctx context.Context, req *sdkmcp.CallToolRequest, input EchoInput) ( @@ -116,6 +147,55 @@ func fsReadTool(ctx context.Context, req *sdkmcp.CallToolRequest, input FSReadIn return nil, FSReadOutput{Content: string(data)}, nil } +func fsReadBase64Tool(ctx context.Context, req *sdkmcp.CallToolRequest, input FSReadBase64Input) ( + *sdkmcp.CallToolResult, + FSReadBase64Output, + error, +) { + root := dataRoot() + target, err := resolvePath(root, input.Path) + if err != nil { + return nil, FSReadBase64Output{}, err + } + data, err := os.ReadFile(target) + if err != nil { + return nil, FSReadBase64Output{}, err + } + ext := strings.ToLower(filepath.Ext(target)) + mimeType := mime.TypeByExtension(ext) + if strings.TrimSpace(mimeType) == "" { + // Fallback mapping for common image/audio extensions. + switch ext { + case ".png": + mimeType = "image/png" + case ".jpg", ".jpeg": + mimeType = "image/jpeg" + case ".gif": + mimeType = "image/gif" + case ".webp": + mimeType = "image/webp" + case ".bmp": + mimeType = "image/bmp" + case ".svg": + mimeType = "image/svg+xml" + case ".mp3": + mimeType = "audio/mpeg" + case ".wav": + mimeType = "audio/wav" + case ".ogg": + mimeType = "audio/ogg" + case ".flac": + mimeType = "audio/flac" + default: + mimeType = "application/octet-stream" + } + } + return nil, FSReadBase64Output{ + Data: base64.StdEncoding.EncodeToString(data), + MimeType: mimeType, + }, nil +} + func fsWriteTool(ctx context.Context, req *sdkmcp.CallToolRequest, input FSWriteInput) ( *sdkmcp.CallToolResult, FSWriteOutput, @@ -267,6 +347,49 @@ func fsApplyPatchTool(ctx context.Context, req *sdkmcp.CallToolRequest, input FS return nil, FSApplyPatchOutput{OK: true}, nil } +func fsMkdirTool(ctx context.Context, req *sdkmcp.CallToolRequest, input FSMkdirInput) ( + *sdkmcp.CallToolResult, + FSMkdirOutput, + error, +) { + root := dataRoot() + target, err := resolvePath(root, input.Path) + if err != nil { + return nil, FSMkdirOutput{}, err + } + if err := os.MkdirAll(target, 0o755); err != nil { + return nil, FSMkdirOutput{}, err + } + return nil, FSMkdirOutput{OK: true}, nil +} + +func fsRenameTool(ctx context.Context, req *sdkmcp.CallToolRequest, input FSRenameInput) ( + *sdkmcp.CallToolResult, + FSRenameOutput, + error, +) { + root := dataRoot() + source, err := resolvePath(root, input.Source) + if err != nil { + return nil, FSRenameOutput{}, err + } + destination, err := resolvePath(root, input.Destination) + if err != nil { + return nil, FSRenameOutput{}, err + } + + if _, err := os.Lstat(destination); err == nil { + return nil, FSRenameOutput{}, fmt.Errorf("destination already exists") + } else if !os.IsNotExist(err) { + return nil, FSRenameOutput{}, err + } + + if err := os.Rename(source, destination); err != nil { + return nil, FSRenameOutput{}, err + } + return nil, FSRenameOutput{OK: true}, nil +} + func dataRoot() string { root := strings.TrimSpace(os.Getenv("MCP_DATA_DIR")) if root == "" {