Skip to content

Commit 3637636

Browse files
committed
env: add new FileSystem-related tools such as Grep, Glob and Edit.
Also modifies List to add an `ignore` argument. Signed-off-by: Tibor Vass <teabee89@gmail.com>
1 parent e1bca34 commit 3637636

2 files changed

Lines changed: 227 additions & 10 deletions

File tree

environment/filesystem.go

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,14 @@ import (
44
"context"
55
"fmt"
66
"strings"
7+
8+
"dagger.io/dagger"
9+
"dagger.io/dagger/dag"
710
)
811

12+
// FIXME: See hack where it's used
13+
const fileEditBaseImage = "busybox"
14+
915
func (env *Environment) FileRead(ctx context.Context, targetFile string, shouldReadEntireFile bool, startLineOneIndexed int, endLineOneIndexedInclusive int) (string, error) {
1016
file, err := env.container().File(targetFile).Contents(ctx)
1117
if err != nil {
@@ -31,7 +37,7 @@ func (env *Environment) FileRead(ctx context.Context, targetFile string, shouldR
3137
return strings.Join(lines[start:end], "\n"), nil
3238
}
3339

34-
func (env *Environment) FileWrite(ctx context.Context, explanation, targetFile, contents string) error {
40+
func (env *Environment) FileWrite(ctx context.Context, targetFile, contents string) error {
3541
err := env.apply(ctx, env.container().WithNewFile(targetFile, contents))
3642
if err != nil {
3743
return fmt.Errorf("failed applying file write, skipping git propagation: %w", err)
@@ -40,7 +46,7 @@ func (env *Environment) FileWrite(ctx context.Context, explanation, targetFile,
4046
return nil
4147
}
4248

43-
func (env *Environment) FileDelete(ctx context.Context, explanation, targetFile string) error {
49+
func (env *Environment) FileDelete(ctx context.Context, targetFile string) error {
4450
err := env.apply(ctx, env.container().WithoutFile(targetFile))
4551
if err != nil {
4652
return fmt.Errorf("failed applying file delete, skipping git propagation: %w", err)
@@ -49,8 +55,18 @@ func (env *Environment) FileDelete(ctx context.Context, explanation, targetFile
4955
return nil
5056
}
5157

52-
func (env *Environment) FileList(ctx context.Context, path string) (string, error) {
53-
entries, err := env.container().Directory(path).Entries(ctx)
58+
func (env *Environment) FileList(ctx context.Context, path string, ignore []string) (string, error) {
59+
filter := dagger.DirectoryFilterOpts{Exclude: ignore}
60+
return env.ls(ctx, path, filter)
61+
}
62+
63+
func (env *Environment) FileGlob(ctx context.Context, path string, pattern string) (string, error) {
64+
filter := dagger.DirectoryFilterOpts{Include: []string{pattern}}
65+
return env.ls(ctx, path, filter)
66+
}
67+
68+
func (env *Environment) ls(ctx context.Context, path string, filter dagger.DirectoryFilterOpts) (string, error) {
69+
entries, err := env.container().Directory(path).Filter(filter).Entries(ctx)
5470
if err != nil {
5571
return "", err
5672
}
@@ -60,3 +76,28 @@ func (env *Environment) FileList(ctx context.Context, path string) (string, erro
6076
}
6177
return out.String(), nil
6278
}
79+
80+
func (env *Environment) FileGrep(ctx context.Context, path, pattern, include string) (string, error) {
81+
// Hack: use busybox to run `sed` since dagger doesn't have native file editing primitives.
82+
args := []string{"/bin/grep", "-E", "--", pattern, include}
83+
84+
dir := env.container().Rootfs().Directory(path)
85+
out, err := dag.Container().From(fileEditBaseImage).WithMountedDirectory("/mnt", dir).WithWorkdir("/mnt").WithExec(args).Stdout(ctx)
86+
if err != nil {
87+
return "", err
88+
}
89+
return out, nil
90+
}
91+
92+
func (env *Environment) FileEdit(ctx context.Context, targetFile string, edits []string) error {
93+
// Hack: use busybox to run `sed` since dagger doesn't have native file editing primitives.
94+
args := []string{"/bin/sh", "-c", fmt.Sprintf("sed -ri'' -- %s /target && cp /target /new", strings.Join(edits, " "))}
95+
96+
newFile := dag.Container().From(fileEditBaseImage).WithMountedFile("/target", env.container().File(targetFile)).WithExec(args).File("/new")
97+
err := env.apply(ctx, env.container().WithFile(targetFile, newFile))
98+
if err != nil {
99+
return fmt.Errorf("failed applying file edit, skipping git propagation: %w", err)
100+
}
101+
env.Notes.Add("Edit %s", targetFile)
102+
return nil
103+
}

mcpserver/tools.go

Lines changed: 182 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -115,9 +115,12 @@ func init() {
115115

116116
EnvironmentRunCmdTool,
117117

118-
EnvironmentFileReadTool,
119118
EnvironmentFileListTool,
119+
EnvironmentFileGlobTool,
120+
EnvironmentFileGrepTool,
121+
EnvironmentFileReadTool,
120122
EnvironmentFileWriteTool,
123+
EnvironmentFileEditTool,
121124
EnvironmentFileDeleteTool,
122125

123126
EnvironmentAddServiceTool,
@@ -575,7 +578,7 @@ var EnvironmentFileReadTool = &Tool{
575578

576579
var EnvironmentFileListTool = &Tool{
577580
Definition: mcp.NewTool("environment_file_list",
578-
mcp.WithDescription("List the contents of a directory"),
581+
mcp.WithDescription("List files and directories in a given path. You can optionally provide an array of glob patterns to ignore with the ignore parameter. You should generally prefer the environment_file_glob and environment_file_grep tools, if you know which directories to search."),
579582
mcp.WithString("explanation",
580583
mcp.Description("One sentence explanation for why this directory is being listed."),
581584
),
@@ -591,6 +594,63 @@ var EnvironmentFileListTool = &Tool{
591594
mcp.Description("Path of the directory to list contents of, absolute or relative to the workdir"),
592595
mcp.Required(),
593596
),
597+
mcp.WithArray("ignore",
598+
mcp.Description("List of glob patterns to ignore"),
599+
mcp.Items(map[string]any{"type": "string"}),
600+
),
601+
),
602+
Handler: func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
603+
_, env, err := openEnvironment(ctx, request)
604+
if err != nil {
605+
return mcp.NewToolResultErrorFromErr("unable to open the environment", err), nil
606+
}
607+
608+
path, err := request.RequireString("path")
609+
if err != nil {
610+
return nil, err
611+
}
612+
613+
args := request.GetArguments()
614+
v, ok := args["ignore"]
615+
var ignore []string
616+
if ok {
617+
ignore, ok = v.([]string)
618+
if !ok {
619+
return nil, fmt.Errorf("`ignore` arugment expects an array of strings")
620+
}
621+
}
622+
623+
out, err := env.FileList(ctx, path, ignore)
624+
if err != nil {
625+
return mcp.NewToolResultErrorFromErr("failed to list directory", err), nil
626+
}
627+
628+
return mcp.NewToolResultText(out), nil
629+
},
630+
}
631+
632+
var EnvironmentFileGlobTool = &Tool{
633+
Definition: mcp.NewTool("environment_file_glob",
634+
mcp.WithDescription("Fast file pattern matching tool.\nSupports glob syntax \"**/*.js\" or \"src/**/*.ts\".\nReturns matching file paths sorted by modification time."),
635+
mcp.WithString("explanation",
636+
mcp.Description("One sentence explanation for why file paths are being matched."),
637+
),
638+
mcp.WithString("environment_source",
639+
mcp.Description("Absolute path to the source git repository for the environment."),
640+
mcp.Required(),
641+
),
642+
mcp.WithString("environment_id",
643+
mcp.Description("The ID of the environment for this command. Must call `environment_create` first."),
644+
mcp.Required(),
645+
),
646+
mcp.WithString("path",
647+
mcp.Description("Path of the directory to search in, absolute or relative to the workdir"),
648+
mcp.Required(),
649+
),
650+
mcp.WithString("pattern",
651+
mcp.Description("The glob pattern to match file paths against."),
652+
mcp.Required(),
653+
),
594654
),
595655
Handler: func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
596656
_, env, err := openEnvironment(ctx, request)
@@ -603,7 +663,12 @@ var EnvironmentFileListTool = &Tool{
603663
return nil, err
604664
}
605665

606-
out, err := env.FileList(ctx, path)
666+
pattern, err := request.RequireString("pattern")
667+
if err != nil {
668+
return nil, err
669+
}
670+
671+
out, err := env.FileGlob(ctx, path, pattern)
607672
if err != nil {
608673
return mcp.NewToolResultErrorFromErr("failed to list directory", err), nil
609674
}
@@ -612,9 +677,120 @@ var EnvironmentFileListTool = &Tool{
612677
},
613678
}
614679

680+
var EnvironmentFileGrepTool = &Tool{
681+
Definition: mcp.NewTool("environment_file_grep",
682+
mcp.WithDescription("Fast file content search using regular expressions.\nSupports full regex syntax (eg. \"log.*Error\", \"function\\s+\\w+\", etc.).\nReturns matching file paths sorted by modification time."),
683+
mcp.WithString("explanation",
684+
mcp.Description("One sentence explanation for why file paths are being matched."),
685+
),
686+
mcp.WithString("environment_source",
687+
mcp.Description("Absolute path to the source git repository for the environment."),
688+
mcp.Required(),
689+
),
690+
mcp.WithString("environment_id",
691+
mcp.Description("The ID of the environment for this command. Must call `environment_create` first."),
692+
mcp.Required(),
693+
),
694+
mcp.WithString("path",
695+
mcp.Description("Path of the directory to search in, absolute or relative to the workdir"),
696+
mcp.Required(),
697+
),
698+
mcp.WithString("pattern",
699+
mcp.Description("The regular expression pattern to search for in file contents."),
700+
mcp.Required(),
701+
),
702+
mcp.WithString("include",
703+
mcp.Description("File pattern to include in the search (e.g. \"*.js\", \"*.{ts,tsx}\")."),
704+
),
705+
),
706+
Handler: func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
707+
_, env, err := openEnvironment(ctx, request)
708+
if err != nil {
709+
return mcp.NewToolResultErrorFromErr("unable to open the environment", err), nil
710+
}
711+
712+
path, err := request.RequireString("path")
713+
if err != nil {
714+
return nil, err
715+
}
716+
717+
pattern, err := request.RequireString("pattern")
718+
if err != nil {
719+
return nil, err
720+
}
721+
722+
include := request.GetString("include", "")
723+
724+
out, err := env.FileGrep(ctx, path, pattern, include)
725+
if err != nil {
726+
return mcp.NewToolResultErrorFromErr("failed to grep directory", err), nil
727+
}
728+
729+
return mcp.NewToolResultText(out), nil
730+
},
731+
}
732+
733+
var EnvironmentFileEditTool = &Tool{
734+
Definition: mcp.NewTool("environment_file_edit",
735+
mcp.WithDescription("Efficiently edit the contents of a file."),
736+
mcp.WithString("explanation",
737+
mcp.Description("One sentence explanation for why this file is being edited."),
738+
),
739+
mcp.WithString("environment_source",
740+
mcp.Description("Absolute path to the source git repository for the environment."),
741+
mcp.Required(),
742+
),
743+
mcp.WithString("environment_id",
744+
mcp.Description("The ID of the environment for this command. Must call `environment_create` first."),
745+
mcp.Required(),
746+
),
747+
mcp.WithString("target_file",
748+
mcp.Description("Path of the file to edit, absolute or relative to the workdir."),
749+
mcp.Required(),
750+
),
751+
mcp.WithArray("edits",
752+
mcp.Description("Array of sed search-replace operations to perform on the contents of target_file (e.g. \"s/old/new/g\").\nUses extended regex syntax."),
753+
mcp.Items(map[string]any{"type": "string"}),
754+
mcp.MinItems(1),
755+
mcp.Required(),
756+
),
757+
),
758+
Handler: func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
759+
repo, env, err := openEnvironment(ctx, request)
760+
if err != nil {
761+
return mcp.NewToolResultErrorFromErr("unable to open the environment", err), nil
762+
}
763+
764+
targetFile, err := request.RequireString("target_file")
765+
if err != nil {
766+
return nil, err
767+
}
768+
769+
args := request.GetArguments()
770+
v, ok := args["edits"]
771+
if !ok {
772+
return nil, fmt.Errorf("could not find `edits` argument")
773+
}
774+
edits, ok := v.([]string)
775+
if !ok {
776+
return nil, fmt.Errorf("`edits` argument is expected to be a []string")
777+
}
778+
779+
if err := env.FileEdit(ctx, targetFile, edits); err != nil {
780+
return mcp.NewToolResultErrorFromErr("failed to edit file", err), nil
781+
}
782+
783+
if err := repo.Update(ctx, env, request.GetString("explanation", "")); err != nil {
784+
return mcp.NewToolResultErrorFromErr("unable to update the environment", err), nil
785+
}
786+
787+
return mcp.NewToolResultText(fmt.Sprintf("file %s edited successfully and committed to container-use/ remote", targetFile)), nil
788+
},
789+
}
790+
615791
var EnvironmentFileWriteTool = &Tool{
616792
Definition: mcp.NewTool("environment_file_write",
617-
mcp.WithDescription("Write the contents of a file."),
793+
mcp.WithDescription("Write the full contents of a file."),
618794
mcp.WithString("explanation",
619795
mcp.Description("One sentence explanation for why this file is being written."),
620796
),
@@ -650,7 +826,7 @@ var EnvironmentFileWriteTool = &Tool{
650826
return nil, err
651827
}
652828

653-
if err := env.FileWrite(ctx, request.GetString("explanation", ""), targetFile, contents); err != nil {
829+
if err := env.FileWrite(ctx, targetFile, contents); err != nil {
654830
return mcp.NewToolResultErrorFromErr("failed to write file", err), nil
655831
}
656832

@@ -692,7 +868,7 @@ var EnvironmentFileDeleteTool = &Tool{
692868
return nil, err
693869
}
694870

695-
if err := env.FileDelete(ctx, request.GetString("explanation", ""), targetFile); err != nil {
871+
if err := env.FileDelete(ctx, targetFile); err != nil {
696872
return mcp.NewToolResultErrorFromErr("failed to delete file", err), nil
697873
}
698874

0 commit comments

Comments
 (0)