feat: add coder_workspace_ls MCP tool (#19652)

This commit is contained in:
Asher
2025-09-12 15:57:15 -08:00
committed by GitHub
parent 30330abaea
commit be7aa58075
6 changed files with 316 additions and 113 deletions
+58
View File
@@ -42,6 +42,7 @@ const (
ToolNameWorkspaceBash = "coder_workspace_bash"
ToolNameChatGPTSearch = "search"
ToolNameChatGPTFetch = "fetch"
ToolNameWorkspaceLS = "coder_workspace_ls"
ToolNameWorkspaceReadFile = "coder_workspace_read_file"
ToolNameWorkspaceWriteFile = "coder_workspace_write_file"
ToolNameWorkspaceEditFile = "coder_workspace_edit_file"
@@ -213,6 +214,7 @@ var All = []GenericTool{
WorkspaceBash.Generic(),
ChatGPTSearch.Generic(),
ChatGPTFetch.Generic(),
WorkspaceLS.Generic(),
WorkspaceReadFile.Generic(),
WorkspaceWriteFile.Generic(),
WorkspaceEditFile.Generic(),
@@ -1373,6 +1375,62 @@ type MinimalTemplate struct {
ActiveUserCount int `json:"active_user_count"`
}
type WorkspaceLSArgs struct {
Workspace string `json:"workspace"`
Path string `json:"path"`
}
type WorkspaceLSFile struct {
Path string `json:"path"`
IsDir bool `json:"is_dir"`
}
type WorkspaceLSResponse struct {
Contents []WorkspaceLSFile `json:"contents"`
}
var WorkspaceLS = Tool[WorkspaceLSArgs, WorkspaceLSResponse]{
Tool: aisdk.Tool{
Name: ToolNameWorkspaceLS,
Description: `List directories 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.",
},
"path": map[string]any{
"type": "string",
"description": "The absolute path of the directory in the workspace to list.",
},
},
Required: []string{"path", "workspace"},
},
},
UserClientOptional: true,
Handler: func(ctx context.Context, deps Deps, args WorkspaceLSArgs) (WorkspaceLSResponse, error) {
conn, err := newAgentConn(ctx, deps.coderClient, args.Workspace)
if err != nil {
return WorkspaceLSResponse{}, err
}
defer conn.Close()
res, err := conn.LS(ctx, args.Path, workspacesdk.LSRequest{})
if err != nil {
return WorkspaceLSResponse{}, err
}
contents := make([]WorkspaceLSFile, len(res.Contents))
for i, f := range res.Contents {
contents[i] = WorkspaceLSFile{
Path: f.AbsolutePathString,
IsDir: f.IsDir,
}
}
return WorkspaceLSResponse{Contents: contents}, nil
},
}
type WorkspaceReadFileArgs struct {
Workspace string `json:"workspace"`
Path string `json:"path"`
+46
View File
@@ -454,6 +454,52 @@ func TestTools(t *testing.T) {
require.Equal(t, "owner format works", result.Output)
})
t.Run("WorkspaceLS", 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()
dirPath := filepath.Join(tmpdir, "dir1/dir2")
err = fs.MkdirAll(dirPath, 0o755)
require.NoError(t, err)
filePath := filepath.Join(tmpdir, "dir1", "foo")
err = afero.WriteFile(fs, filePath, []byte("foo bar"), 0o644)
require.NoError(t, err)
_, err = testTool(t, toolsdk.WorkspaceLS, tb, toolsdk.WorkspaceLSArgs{
Workspace: workspace.Name,
Path: "relative",
})
require.Error(t, err)
require.Contains(t, err.Error(), "path must be absolute")
res, err := testTool(t, toolsdk.WorkspaceLS, tb, toolsdk.WorkspaceLSArgs{
Workspace: workspace.Name,
Path: filepath.Dir(dirPath),
})
require.NoError(t, err)
require.Equal(t, []toolsdk.WorkspaceLSFile{
{
Path: dirPath,
IsDir: true,
},
{
Path: filePath,
IsDir: false,
},
}, res.Contents)
})
t.Run("WorkspaceReadFile", func(t *testing.T) {
t.Parallel()