fix(mcp): resolve workspace paths for tools

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-04-02 11:01:11 +00:00
parent 45d439926f
commit e62f4ab654
4 changed files with 62 additions and 3 deletions

View file

@ -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

View file

@ -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})

View file

@ -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,
}

View file

@ -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)