Skip to content

Commit 93158d5

Browse files
author
Dimitar Grigorov
committed
Add path validation helpers, update all handlers
1 parent 4c3420a commit 93158d5

File tree

11 files changed

+147
-241
lines changed

11 files changed

+147
-241
lines changed

filetoolsserver/handler/create_directory.go

Lines changed: 5 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -9,32 +9,14 @@ import (
99
)
1010

1111
// HandleCreateDirectory creates a new directory or ensures a directory exists.
12-
// Can create multiple nested directories in one operation (like mkdir -p).
1312
func (h *Handler) HandleCreateDirectory(ctx context.Context, req *mcp.CallToolRequest, input CreateDirectoryInput) (*mcp.CallToolResult, CreateDirectoryOutput, error) {
14-
// Validate path
15-
if input.Path == "" {
16-
return &mcp.CallToolResult{
17-
Content: []mcp.Content{&mcp.TextContent{Text: "path is required and must be a non-empty string"}},
18-
IsError: true,
19-
}, CreateDirectoryOutput{}, nil
13+
v := h.ValidatePath(input.Path)
14+
if !v.Ok() {
15+
return v.Result, CreateDirectoryOutput{}, nil
2016
}
2117

22-
// Validate path against allowed directories
23-
validatedPath, err := h.validatePath(input.Path)
24-
if err != nil {
25-
return &mcp.CallToolResult{
26-
Content: []mcp.Content{&mcp.TextContent{Text: err.Error()}},
27-
IsError: true,
28-
}, CreateDirectoryOutput{}, nil
29-
}
30-
31-
// Create directory (and any necessary parents)
32-
err = os.MkdirAll(validatedPath, 0755)
33-
if err != nil {
34-
return &mcp.CallToolResult{
35-
Content: []mcp.Content{&mcp.TextContent{Text: fmt.Sprintf("failed to create directory: %v", err)}},
36-
IsError: true,
37-
}, CreateDirectoryOutput{}, nil
18+
if err := os.MkdirAll(v.Path, 0755); err != nil {
19+
return errorResult(fmt.Sprintf("failed to create directory: %v", err)), CreateDirectoryOutput{}, nil
3820
}
3921

4022
message := fmt.Sprintf("Successfully created directory %s", input.Path)

filetoolsserver/handler/detect.go

Lines changed: 6 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -11,41 +11,19 @@ import (
1111

1212
// HandleDetectEncoding detects the encoding of a file
1313
func (h *Handler) HandleDetectEncoding(ctx context.Context, req *mcp.CallToolRequest, input DetectEncodingInput) (*mcp.CallToolResult, DetectEncodingOutput, error) {
14-
// Validate path
15-
if input.Path == "" {
16-
return &mcp.CallToolResult{
17-
Content: []mcp.Content{&mcp.TextContent{Text: "path is required and must be a non-empty string"}},
18-
IsError: true,
19-
}, DetectEncodingOutput{}, nil
14+
v := h.ValidatePath(input.Path)
15+
if !v.Ok() {
16+
return v.Result, DetectEncodingOutput{}, nil
2017
}
2118

22-
// Validate path against allowed directories
23-
validatedPath, err := h.validatePath(input.Path)
19+
data, err := os.ReadFile(v.Path)
2420
if err != nil {
25-
return &mcp.CallToolResult{
26-
Content: []mcp.Content{&mcp.TextContent{Text: err.Error()}},
27-
IsError: true,
28-
}, DetectEncodingOutput{}, nil
21+
return errorResult(fmt.Sprintf("failed to read file: %v", err)), DetectEncodingOutput{}, nil
2922
}
3023

31-
// Read file
32-
data, err := os.ReadFile(validatedPath)
33-
if err != nil {
34-
return &mcp.CallToolResult{
35-
Content: []mcp.Content{&mcp.TextContent{Text: fmt.Sprintf("failed to read file: %v", err)}},
36-
IsError: true,
37-
}, DetectEncodingOutput{}, nil
38-
}
39-
40-
// Detect encoding
4124
result := encoding.Detect(data)
42-
43-
// Handle unknown encoding
4425
if result.Charset == "" {
45-
return &mcp.CallToolResult{
46-
Content: []mcp.Content{&mcp.TextContent{Text: "Could not detect encoding"}},
47-
IsError: true,
48-
}, DetectEncodingOutput{}, nil
26+
return errorResult("Could not detect encoding"), DetectEncodingOutput{}, nil
4927
}
5028

5129
return &mcp.CallToolResult{}, DetectEncodingOutput{

filetoolsserver/handler/directory.go

Lines changed: 6 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -11,47 +11,26 @@ import (
1111

1212
// HandleListDirectory lists files in a directory with optional pattern filtering
1313
func (h *Handler) HandleListDirectory(ctx context.Context, req *mcp.CallToolRequest, input ListDirectoryInput) (*mcp.CallToolResult, ListDirectoryOutput, error) {
14-
// Validate path
15-
if input.Path == "" {
16-
return &mcp.CallToolResult{
17-
Content: []mcp.Content{&mcp.TextContent{Text: "path is required and must be a non-empty string"}},
18-
IsError: true,
19-
}, ListDirectoryOutput{}, nil
14+
v := h.ValidatePath(input.Path)
15+
if !v.Ok() {
16+
return v.Result, ListDirectoryOutput{}, nil
2017
}
2118

22-
// Validate path against allowed directories
23-
validatedPath, err := h.validatePath(input.Path)
24-
if err != nil {
25-
return &mcp.CallToolResult{
26-
Content: []mcp.Content{&mcp.TextContent{Text: err.Error()}},
27-
IsError: true,
28-
}, ListDirectoryOutput{}, nil
29-
}
30-
31-
// Default pattern
3219
pattern := "*"
3320
if input.Pattern != "" {
3421
pattern = input.Pattern
3522
}
3623

37-
// Read directory
38-
entries, err := os.ReadDir(validatedPath)
24+
entries, err := os.ReadDir(v.Path)
3925
if err != nil {
40-
return &mcp.CallToolResult{
41-
Content: []mcp.Content{&mcp.TextContent{Text: fmt.Sprintf("failed to read directory: %v", err)}},
42-
IsError: true,
43-
}, ListDirectoryOutput{}, nil
26+
return errorResult(fmt.Sprintf("failed to read directory: %v", err)), ListDirectoryOutput{}, nil
4427
}
4528

46-
// Filter by pattern and build result
4729
var files []string
4830
for _, entry := range entries {
4931
matched, err := filepath.Match(pattern, entry.Name())
5032
if err != nil {
51-
return &mcp.CallToolResult{
52-
Content: []mcp.Content{&mcp.TextContent{Text: fmt.Sprintf("invalid pattern: %v", err)}},
53-
IsError: true,
54-
}, ListDirectoryOutput{}, nil
33+
return errorResult(fmt.Sprintf("invalid pattern: %v", err)), ListDirectoryOutput{}, nil
5534
}
5635
if matched {
5736
prefix := ""

filetoolsserver/handler/directorytree.go

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,12 @@ import (
1313

1414
// HandleDirectoryTree returns a recursive tree view of files and directories as JSON.
1515
func (h *Handler) HandleDirectoryTree(ctx context.Context, req *mcp.CallToolRequest, input DirectoryTreeInput) (*mcp.CallToolResult, DirectoryTreeOutput, error) {
16-
// Validate input
17-
if input.Path == "" {
18-
return errorResult(ErrPathRequired.Error()), DirectoryTreeOutput{}, nil
16+
v := h.ValidatePath(input.Path)
17+
if !v.Ok() {
18+
return v.Result, DirectoryTreeOutput{}, nil
1919
}
2020

21-
// Validate path against allowed directories
22-
validatedPath, err := h.validatePath(input.Path)
23-
if err != nil {
24-
return errorResult(err.Error()), DirectoryTreeOutput{}, nil
25-
}
26-
27-
// Check that the path is a directory
28-
stat, err := os.Stat(validatedPath)
21+
stat, err := os.Stat(v.Path)
2922
if err != nil {
3023
return errorResult(fmt.Sprintf("failed to access path: %v", err)), DirectoryTreeOutput{}, nil
3124
}
@@ -34,7 +27,7 @@ func (h *Handler) HandleDirectoryTree(ctx context.Context, req *mcp.CallToolRequ
3427
}
3528

3629
// Build the tree
37-
tree, err := buildTree(validatedPath, input.ExcludePatterns)
30+
tree, err := buildTree(v.Path, input.ExcludePatterns)
3831
if err != nil {
3932
return errorResult(fmt.Sprintf("failed to build directory tree: %v", err)), DirectoryTreeOutput{}, nil
4033
}

filetoolsserver/handler/edit_file.go

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,25 +13,17 @@ import (
1313
)
1414

1515
// HandleEditFile applies line-based edits to a text file.
16-
// Each edit replaces exact line sequences with new content.
17-
// Returns a git-style unified diff showing the changes made.
1816
func (h *Handler) HandleEditFile(ctx context.Context, req *mcp.CallToolRequest, input EditFileInput) (*mcp.CallToolResult, EditFileOutput, error) {
19-
// Validate inputs
20-
if input.Path == "" {
21-
return errorResult(ErrPathRequired.Error()), EditFileOutput{}, nil
22-
}
2317
if len(input.Edits) == 0 {
2418
return errorResult(ErrEditsRequired.Error()), EditFileOutput{}, nil
2519
}
2620

27-
// Validate path against allowed directories
28-
validatedPath, err := h.validatePath(input.Path)
29-
if err != nil {
30-
return errorResult(err.Error()), EditFileOutput{}, nil
21+
v := h.ValidatePath(input.Path)
22+
if !v.Ok() {
23+
return v.Result, EditFileOutput{}, nil
3124
}
3225

33-
// Read file content
34-
data, err := os.ReadFile(validatedPath)
26+
data, err := os.ReadFile(v.Path)
3527
if err != nil {
3628
return errorResult(fmt.Sprintf("failed to read file: %v", err)), EditFileOutput{}, nil
3729
}
@@ -53,7 +45,7 @@ func (h *Handler) HandleEditFile(ctx context.Context, req *mcp.CallToolRequest,
5345

5446
// Write file if not dry run (atomic write)
5547
if !input.DryRun {
56-
if err := atomicWriteFile(validatedPath, modifiedContent); err != nil {
48+
if err := atomicWriteFile(v.Path, modifiedContent); err != nil {
5749
return errorResult(fmt.Sprintf("failed to write file: %v", err)), EditFileOutput{}, nil
5850
}
5951
}

filetoolsserver/handler/fileinfo.go

Lines changed: 5 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -11,36 +11,17 @@ import (
1111

1212
// HandleGetFileInfo retrieves detailed metadata about a file or directory
1313
func (h *Handler) HandleGetFileInfo(ctx context.Context, req *mcp.CallToolRequest, input GetFileInfoInput) (*mcp.CallToolResult, GetFileInfoOutput, error) {
14-
// Validate path
15-
if input.Path == "" {
16-
return &mcp.CallToolResult{
17-
Content: []mcp.Content{&mcp.TextContent{Text: "path is required and must be a non-empty string"}},
18-
IsError: true,
19-
}, GetFileInfoOutput{}, nil
14+
v := h.ValidatePath(input.Path)
15+
if !v.Ok() {
16+
return v.Result, GetFileInfoOutput{}, nil
2017
}
2118

22-
// Validate path against allowed directories
23-
validatedPath, err := h.validatePath(input.Path)
19+
stat, err := os.Stat(v.Path)
2420
if err != nil {
25-
return &mcp.CallToolResult{
26-
Content: []mcp.Content{&mcp.TextContent{Text: err.Error()}},
27-
IsError: true,
28-
}, GetFileInfoOutput{}, nil
21+
return errorResult(fmt.Sprintf("failed to get file info: %v", err)), GetFileInfoOutput{}, nil
2922
}
3023

31-
// Get file info
32-
stat, err := os.Stat(validatedPath)
33-
if err != nil {
34-
return &mcp.CallToolResult{
35-
Content: []mcp.Content{&mcp.TextContent{Text: fmt.Sprintf("failed to get file info: %v", err)}},
36-
IsError: true,
37-
}, GetFileInfoOutput{}, nil
38-
}
39-
40-
// Get file times (platform-specific)
4124
created, accessed, modified := getFileTimes(stat)
42-
43-
// Get permissions in octal format (last 3 digits)
4425
permissions := fmt.Sprintf("%03o", stat.Mode().Perm())
4526

4627
output := GetFileInfoOutput{
@@ -56,7 +37,6 @@ func (h *Handler) HandleGetFileInfo(ctx context.Context, req *mcp.CallToolReques
5637
return &mcp.CallToolResult{}, output, nil
5738
}
5839

59-
// formatTime formats a time.Time to ISO 8601 format
6040
func formatTime(t time.Time) string {
6141
return t.Format(time.RFC3339)
6242
}

filetoolsserver/handler/move_file.go

Lines changed: 11 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -9,66 +9,27 @@ import (
99
)
1010

1111
// HandleMoveFile moves or renames a file or directory.
12-
// Can move files between directories and rename them in a single operation.
13-
// Fails if the destination already exists.
1412
func (h *Handler) HandleMoveFile(ctx context.Context, req *mcp.CallToolRequest, input MoveFileInput) (*mcp.CallToolResult, MoveFileOutput, error) {
15-
// Validate source path
16-
if input.Source == "" {
17-
return &mcp.CallToolResult{
18-
Content: []mcp.Content{&mcp.TextContent{Text: "source is required and must be a non-empty string"}},
19-
IsError: true,
20-
}, MoveFileOutput{}, nil
13+
src, dst := h.ValidateSourceDest(input.Source, input.Destination)
14+
if !src.Ok() {
15+
return src.Result, MoveFileOutput{}, nil
2116
}
22-
23-
// Validate destination path
24-
if input.Destination == "" {
25-
return &mcp.CallToolResult{
26-
Content: []mcp.Content{&mcp.TextContent{Text: "destination is required and must be a non-empty string"}},
27-
IsError: true,
28-
}, MoveFileOutput{}, nil
29-
}
30-
31-
// Validate source path against allowed directories
32-
validatedSource, err := h.validatePath(input.Source)
33-
if err != nil {
34-
return &mcp.CallToolResult{
35-
Content: []mcp.Content{&mcp.TextContent{Text: err.Error()}},
36-
IsError: true,
37-
}, MoveFileOutput{}, nil
38-
}
39-
40-
// Validate destination path against allowed directories
41-
validatedDest, err := h.validatePath(input.Destination)
42-
if err != nil {
43-
return &mcp.CallToolResult{
44-
Content: []mcp.Content{&mcp.TextContent{Text: err.Error()}},
45-
IsError: true,
46-
}, MoveFileOutput{}, nil
17+
if !dst.Ok() {
18+
return dst.Result, MoveFileOutput{}, nil
4719
}
4820

4921
// Check if source exists
50-
if _, err := os.Stat(validatedSource); os.IsNotExist(err) {
51-
return &mcp.CallToolResult{
52-
Content: []mcp.Content{&mcp.TextContent{Text: fmt.Sprintf("source does not exist: %s", input.Source)}},
53-
IsError: true,
54-
}, MoveFileOutput{}, nil
22+
if _, err := os.Stat(src.Path); os.IsNotExist(err) {
23+
return errorResult(fmt.Sprintf("source does not exist: %s", input.Source)), MoveFileOutput{}, nil
5524
}
5625

5726
// Check if destination already exists
58-
if _, err := os.Stat(validatedDest); err == nil {
59-
return &mcp.CallToolResult{
60-
Content: []mcp.Content{&mcp.TextContent{Text: fmt.Sprintf("destination already exists: %s", input.Destination)}},
61-
IsError: true,
62-
}, MoveFileOutput{}, nil
27+
if _, err := os.Stat(dst.Path); err == nil {
28+
return errorResult(fmt.Sprintf("destination already exists: %s", input.Destination)), MoveFileOutput{}, nil
6329
}
6430

65-
// Perform the move/rename operation
66-
err = os.Rename(validatedSource, validatedDest)
67-
if err != nil {
68-
return &mcp.CallToolResult{
69-
Content: []mcp.Content{&mcp.TextContent{Text: fmt.Sprintf("failed to move file: %v", err)}},
70-
IsError: true,
71-
}, MoveFileOutput{}, nil
31+
if err := os.Rename(src.Path, dst.Path); err != nil {
32+
return errorResult(fmt.Sprintf("failed to move file: %v", err)), MoveFileOutput{}, nil
7233
}
7334

7435
message := fmt.Sprintf("Successfully moved %s to %s", input.Source, input.Destination)

filetoolsserver/handler/read.go

Lines changed: 7 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -23,19 +23,17 @@ type encodingResult struct {
2323
// HandleReadTextFile reads a file in the specified encoding and returns UTF-8 content.
2424
// If encoding is not specified, it auto-detects the encoding using chunked sampling.
2525
func (h *Handler) HandleReadTextFile(ctx context.Context, req *mcp.CallToolRequest, input ReadTextFileInput) (*mcp.CallToolResult, ReadTextFileOutput, error) {
26-
// Validate input
27-
if err := validateReadInput(input); err != nil {
28-
return errorResult(err.Error()), ReadTextFileOutput{}, nil
26+
// Validate head/tail conflict
27+
if input.Head != nil && input.Tail != nil {
28+
return errorResult(ErrHeadTailConflict.Error()), ReadTextFileOutput{}, nil
2929
}
3030

31-
// Validate path against allowed directories
32-
validatedPath, err := h.validatePath(input.Path)
33-
if err != nil {
34-
return errorResult(err.Error()), ReadTextFileOutput{}, nil
31+
v := h.ValidatePath(input.Path)
32+
if !v.Ok() {
33+
return v.Result, ReadTextFileOutput{}, nil
3534
}
3635

37-
// Read file
38-
data, err := os.ReadFile(validatedPath)
36+
data, err := os.ReadFile(v.Path)
3937
if err != nil {
4038
return errorResult(fmt.Sprintf("failed to read file: %v", err)), ReadTextFileOutput{}, nil
4139
}
@@ -65,17 +63,6 @@ func (h *Handler) HandleReadTextFile(ctx context.Context, req *mcp.CallToolReque
6563
return &mcp.CallToolResult{}, output, nil
6664
}
6765

68-
// validateReadInput validates the input parameters for reading a file
69-
func validateReadInput(input ReadTextFileInput) error {
70-
if input.Path == "" {
71-
return ErrPathRequired
72-
}
73-
if input.Head != nil && input.Tail != nil {
74-
return ErrHeadTailConflict
75-
}
76-
return nil
77-
}
78-
7966
// resolveEncoding determines the encoding to use, either from explicit input or auto-detection
8067
func resolveEncoding(inputEncoding string, data []byte) (encodingResult, error) {
8168
result := encodingResult{}

0 commit comments

Comments
 (0)