fix(mcp): resolve workspace paths for tools
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
45d439926f
commit
e62f4ab654
4 changed files with 62 additions and 3 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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})
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue