Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Add coder_workspace_edit_files MCP tool
  • Loading branch information
code-asher committed Sep 12, 2025
commit 40a84954df8627968469dd6d3b5f2533f2462d95
76 changes: 76 additions & 0 deletions codersdk/toolsdk/toolsdk.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ const (
ToolNameWorkspaceReadFile = "coder_workspace_read_file"
ToolNameWorkspaceWriteFile = "coder_workspace_write_file"
ToolNameWorkspaceEditFile = "coder_workspace_edit_file"
ToolNameWorkspaceEditFiles = "coder_workspace_edit_files"
)

func NewDeps(client *codersdk.Client, opts ...func(*Deps)) (Deps, error) {
Expand Down Expand Up @@ -215,6 +216,7 @@ var All = []GenericTool{
WorkspaceReadFile.Generic(),
WorkspaceWriteFile.Generic(),
WorkspaceEditFile.Generic(),
WorkspaceEditFiles.Generic(),
}

type ReportTaskArgs struct {
Expand Down Expand Up @@ -1563,6 +1565,80 @@ var WorkspaceEditFile = Tool[WorkspaceEditFileArgs, codersdk.Response]{
},
}

type WorkspaceEditFilesArgs struct {
Workspace string `json:"workspace"`
Files []workspacesdk.FileEdits `json:"files"`
}

var WorkspaceEditFiles = Tool[WorkspaceEditFilesArgs, codersdk.Response]{
Tool: aisdk.Tool{
Name: ToolNameWorkspaceEditFiles,
Description: `Edit one or more files in a workspace.`,
Schema: aisdk.Schema{
Properties: map[string]any{
"workspace": map[string]any{
"type": "string",
"description": "The workspace name in the format [owner/]workspace[.agent]. If an owner is not specified, the authenticated user is used.",
},
"files": map[string]any{
"type": "array",
"description": "An array of files to edit.",
"items": []any{
map[string]any{
"type": "object",
"properties": map[string]any{
"path": map[string]any{
"type": "string",
"description": "The absolute path of the file to write in the workspace.",
},
"edits": map[string]any{
"type": "array",
"description": "An array of edit operations.",
"items": []any{
map[string]any{
"type": "object",
"properties": map[string]any{
"search": map[string]any{
"type": "string",
"description": "The old string to replace.",
},
"replace": map[string]any{
"type": "string",
"description": "The new string that replaces the old string.",
},
},
"required": []string{"search", "replace"},
},
},
},
"required": []string{"path", "edits"},
},
},
},
},
},
Required: []string{"workspace", "files"},
},
},
UserClientOptional: true,
Handler: func(ctx context.Context, deps Deps, args WorkspaceEditFilesArgs) (codersdk.Response, error) {
conn, err := newAgentConn(ctx, deps.coderClient, args.Workspace)
if err != nil {
return codersdk.Response{}, err
}
defer conn.Close()

err = conn.EditFiles(ctx, workspacesdk.FileEditRequest{Files: args.Files})
if err != nil {
return codersdk.Response{}, err
}

return codersdk.Response{
Message: "File(s) edited successfully.",
}, nil
},
}

// NormalizeWorkspaceInput converts workspace name input to standard format.
// Handles the following input formats:
// - workspace → workspace
Expand Down
61 changes: 61 additions & 0 deletions codersdk/toolsdk/toolsdk_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -620,6 +620,67 @@ func TestTools(t *testing.T) {
require.NoError(t, err)
require.Equal(t, "bar bar", string(b))
})

t.Run("WorkspaceEditFiles", func(t *testing.T) {
t.Parallel()

client, workspace, agentToken := setupWorkspaceForAgent(t)
fs := afero.NewMemMapFs()
_ = agenttest.New(t, client.URL, agentToken, func(opts *agent.Options) {
opts.Filesystem = fs
})
coderdtest.NewWorkspaceAgentWaiter(t, client, workspace.ID).Wait()
tb, err := toolsdk.NewDeps(client)
require.NoError(t, err)

tmpdir := os.TempDir()
filePath1 := filepath.Join(tmpdir, "edit1")
err = afero.WriteFile(fs, filePath1, []byte("foo1 bar1"), 0o644)
require.NoError(t, err)

filePath2 := filepath.Join(tmpdir, "edit2")
err = afero.WriteFile(fs, filePath2, []byte("foo2 bar2"), 0o644)
require.NoError(t, err)

_, err = testTool(t, toolsdk.WorkspaceEditFiles, tb, toolsdk.WorkspaceEditFilesArgs{
Workspace: workspace.Name,
})
require.Error(t, err)
require.Contains(t, err.Error(), "must specify at least one file")

_, err = testTool(t, toolsdk.WorkspaceEditFiles, tb, toolsdk.WorkspaceEditFilesArgs{
Workspace: workspace.Name,
Files: []workspacesdk.FileEdits{
{
Path: filePath1,
Edits: []workspacesdk.FileEdit{
{
Search: "foo1",
Replace: "bar1",
},
},
},
{
Path: filePath2,
Edits: []workspacesdk.FileEdit{
{
Search: "foo2",
Replace: "bar2",
},
},
},
},
})
require.NoError(t, err)

b, err := afero.ReadFile(fs, filePath1)
require.NoError(t, err)
require.Equal(t, "bar1 bar1", string(b))

b, err = afero.ReadFile(fs, filePath2)
require.NoError(t, err)
require.Equal(t, "bar2 bar2", string(b))
})
}

// TestedTools keeps track of which tools have been tested.
Expand Down
Loading