diff --git a/pkg/mcp/mcp.go b/pkg/mcp/mcp.go index af99320..ad1cf2c 100644 --- a/pkg/mcp/mcp.go +++ b/pkg/mcp/mcp.go @@ -12,6 +12,7 @@ import ( "path/filepath" "slices" "sort" + "strings" "sync" core "dappco.re/go/core" @@ -229,6 +230,29 @@ func (s *Service) ProcessService() *process.Service { return s.processService } +// resolveWorkspacePath converts a tool path into the filesystem path the +// service actually operates on. +// +// Sandboxed services keep paths anchored under workspaceRoot. Unrestricted +// services preserve absolute paths and clean relative ones against the current +// working directory. +func (s *Service) resolveWorkspacePath(path string) string { + if path == "" { + return "" + } + + if s.workspaceRoot == "" { + return filepath.Clean(path) + } + + clean := filepath.Clean(string(filepath.Separator) + path) + clean = strings.TrimPrefix(clean, string(filepath.Separator)) + if clean == "." || clean == "" { + return s.workspaceRoot + } + return filepath.Join(s.workspaceRoot, clean) +} + // registerTools adds file operation tools to the MCP server. func (s *Service) registerTools(server *mcp.Server) { // File operations diff --git a/pkg/mcp/mcp_test.go b/pkg/mcp/mcp_test.go index 1138316..fb1635a 100644 --- a/pkg/mcp/mcp_test.go +++ b/pkg/mcp/mcp_test.go @@ -274,6 +274,40 @@ func TestMedium_Good_IsFile(t *testing.T) { } } +func TestResolveWorkspacePath_Good(t *testing.T) { + tmpDir := t.TempDir() + s, err := New(Options{WorkspaceRoot: tmpDir}) + if err != nil { + t.Fatalf("Failed to create service: %v", err) + } + + cases := map[string]string{ + "docs/readme.md": filepath.Join(tmpDir, "docs", "readme.md"), + "/docs/readme.md": filepath.Join(tmpDir, "docs", "readme.md"), + "../escape/notes.md": filepath.Join(tmpDir, "escape", "notes.md"), + "": "", + } + for input, want := range cases { + if got := s.resolveWorkspacePath(input); got != want { + t.Fatalf("resolveWorkspacePath(%q) = %q, want %q", input, got, want) + } + } +} + +func TestResolveWorkspacePath_Good_Unrestricted(t *testing.T) { + s, err := New(Options{Unrestricted: true}) + if err != nil { + t.Fatalf("Failed to create service: %v", err) + } + + if got, want := s.resolveWorkspacePath("docs/readme.md"), filepath.Clean("docs/readme.md"); got != want { + t.Fatalf("resolveWorkspacePath(relative) = %q, want %q", got, want) + } + if got, want := s.resolveWorkspacePath("/tmp/readme.md"), filepath.Clean("/tmp/readme.md"); got != want { + t.Fatalf("resolveWorkspacePath(absolute) = %q, want %q", got, want) + } +} + func TestSandboxing_Traversal_Sanitized(t *testing.T) { tmpDir := t.TempDir() s, err := New(Options{WorkspaceRoot: tmpDir}) diff --git a/pkg/mcp/tools_process.go b/pkg/mcp/tools_process.go index 1c9abb8..4f74ba2 100644 --- a/pkg/mcp/tools_process.go +++ b/pkg/mcp/tools_process.go @@ -183,7 +183,7 @@ func (s *Service) processStart(ctx context.Context, req *mcp.CallToolRequest, in opts := process.RunOptions{ Command: input.Command, Args: input.Args, - Dir: input.Dir, + Dir: s.resolveWorkspacePath(input.Dir), Env: input.Env, } diff --git a/pkg/mcp/tools_rag.go b/pkg/mcp/tools_rag.go index ceeaf29..5669d74 100644 --- a/pkg/mcp/tools_rag.go +++ b/pkg/mcp/tools_rag.go @@ -183,12 +183,13 @@ func (s *Service) ragIngest(ctx context.Context, req *mcp.CallToolRequest, input log.Error("mcp: rag ingest stat failed", "path", input.Path, "err", err) return nil, RAGIngestOutput{}, log.E("ragIngest", "failed to access path", err) } + resolvedPath := s.resolveWorkspacePath(input.Path) var message string var chunks int if info.IsDir() { // Ingest directory - err = rag.IngestDirectory(ctx, input.Path, collection, input.Recreate) + err = rag.IngestDirectory(ctx, resolvedPath, collection, input.Recreate) if err != nil { log.Error("mcp: rag ingest directory failed", "path", input.Path, "collection", collection, "err", err) return nil, RAGIngestOutput{}, log.E("ragIngest", "failed to ingest directory", err) @@ -196,7 +197,7 @@ func (s *Service) ragIngest(ctx context.Context, req *mcp.CallToolRequest, input message = core.Sprintf("Successfully ingested directory %s into collection %s", input.Path, collection) } else { // Ingest single file - chunks, err = rag.IngestSingleFile(ctx, input.Path, collection) + chunks, err = rag.IngestSingleFile(ctx, resolvedPath, collection) if err != nil { log.Error("mcp: rag ingest file failed", "path", input.Path, "collection", collection, "err", err) return nil, RAGIngestOutput{}, log.E("ragIngest", "failed to ingest file", err)