From b96b05ab0bb25cd4f40aa638f629e9afe3e6b1a8 Mon Sep 17 00:00:00 2001 From: Virgil Date: Thu, 2 Apr 2026 10:48:34 +0000 Subject: [PATCH] fix(mcp): stabilise file existence checks Use Stat() for file_exists and sort directory listings for deterministic output. Co-Authored-By: Virgil --- pkg/mcp/mcp.go | 26 ++++++++++++-------------- pkg/mcp/mcp_test.go | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 14 deletions(-) diff --git a/pkg/mcp/mcp.go b/pkg/mcp/mcp.go index d273eec..af99320 100644 --- a/pkg/mcp/mcp.go +++ b/pkg/mcp/mcp.go @@ -11,6 +11,7 @@ import ( "os" "path/filepath" "slices" + "sort" "sync" core "dappco.re/go/core" @@ -503,6 +504,9 @@ func (s *Service) listDirectory(ctx context.Context, req *mcp.CallToolRequest, i if err != nil { return nil, ListDirectoryOutput{}, log.E("mcp.listDirectory", "failed to list directory", err) } + sort.Slice(entries, func(i, j int) bool { + return entries[i].Name() < entries[j].Name() + }) result := make([]DirectoryEntry, 0, len(entries)) for _, e := range entries { info, _ := e.Info() @@ -545,21 +549,15 @@ func (s *Service) renameFile(ctx context.Context, req *mcp.CallToolRequest, inpu } func (s *Service) fileExists(ctx context.Context, req *mcp.CallToolRequest, input FileExistsInput) (*mcp.CallToolResult, FileExistsOutput, error) { - exists := s.medium.IsFile(input.Path) - if exists { - return nil, FileExistsOutput{Exists: true, IsDir: false, Path: input.Path}, nil + info, err := s.medium.Stat(input.Path) + if err != nil { + return nil, FileExistsOutput{Exists: false, IsDir: false, Path: input.Path}, nil } - // Check if it's a directory by attempting to list it - // List might fail if it's a file too (but we checked IsFile) or if doesn't exist. - _, err := s.medium.List(input.Path) - isDir := err == nil - - // If List failed, it might mean it doesn't exist OR it's a special file or permissions. - // Assuming if List works, it's a directory. - - // Refinement: If it doesn't exist, List returns error. - - return nil, FileExistsOutput{Exists: isDir, IsDir: isDir, Path: input.Path}, nil + return nil, FileExistsOutput{ + Exists: true, + IsDir: info.IsDir(), + Path: input.Path, + }, nil } func (s *Service) detectLanguage(ctx context.Context, req *mcp.CallToolRequest, input DetectLanguageInput) (*mcp.CallToolResult, DetectLanguageOutput, error) { diff --git a/pkg/mcp/mcp_test.go b/pkg/mcp/mcp_test.go index aeafed1..1138316 100644 --- a/pkg/mcp/mcp_test.go +++ b/pkg/mcp/mcp_test.go @@ -216,6 +216,43 @@ func TestMedium_Good_EnsureDir(t *testing.T) { } } +func TestFileExists_Good_FileAndDirectory(t *testing.T) { + tmpDir := t.TempDir() + s, err := New(Options{WorkspaceRoot: tmpDir}) + if err != nil { + t.Fatalf("Failed to create service: %v", err) + } + + if err := s.medium.EnsureDir("nested"); err != nil { + t.Fatalf("Failed to create directory: %v", err) + } + if err := s.medium.Write("nested/file.txt", "content"); err != nil { + t.Fatalf("Failed to write file: %v", err) + } + + _, fileOut, err := s.fileExists(nil, nil, FileExistsInput{Path: "nested/file.txt"}) + if err != nil { + t.Fatalf("fileExists(file) failed: %v", err) + } + if !fileOut.Exists { + t.Fatal("expected file to exist") + } + if fileOut.IsDir { + t.Fatal("expected file to not be reported as a directory") + } + + _, dirOut, err := s.fileExists(nil, nil, FileExistsInput{Path: "nested"}) + if err != nil { + t.Fatalf("fileExists(dir) failed: %v", err) + } + if !dirOut.Exists { + t.Fatal("expected directory to exist") + } + if !dirOut.IsDir { + t.Fatal("expected directory to be reported as a directory") + } +} + func TestMedium_Good_IsFile(t *testing.T) { tmpDir := t.TempDir() s, err := New(Options{WorkspaceRoot: tmpDir})