Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

feat: add coder_workspace_ls MCP tool#19652

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Merged
code-asher merged 1 commit intomainfromasher/mcp-file-list
Sep 12, 2025
Merged
Show file tree
Hide file tree
Changes fromall commits
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
141 changes: 66 additions & 75 deletionsagent/ls.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,23 +11,39 @@ import (
"strings"

"github.com/shirou/gopsutil/v4/disk"
"github.com/spf13/afero"
"golang.org/x/xerrors"

"github.com/coder/coder/v2/coderd/httpapi"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/codersdk/workspacesdk"
)

var WindowsDriveRegex = regexp.MustCompile(`^[a-zA-Z]:\\$`)

func (*agent) HandleLS(rw http.ResponseWriter, r *http.Request) {
func (a*agent) HandleLS(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()

var query LSRequest
if !httpapi.Read(ctx, rw, r, &query) {
// An absolute path may be optionally provided, otherwise a path split into an
// array must be provided in the body (which can be relative).
query := r.URL.Query()
parser := httpapi.NewQueryParamParser()
path := parser.String(query, "", "path")
parser.ErrorExcessParams(query)
if len(parser.Errors) > 0 {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "Query parameters have invalid values.",
Validations: parser.Errors,
})
return
}

resp, err := listFiles(query)
var req workspacesdk.LSRequest
if !httpapi.Read(ctx, rw, r, &req) {
return
}

resp, err := listFiles(a.filesystem, path, req)
if err != nil {
status := http.StatusInternalServerError
switch {
Expand All@@ -46,66 +62,74 @@ func (*agent) HandleLS(rw http.ResponseWriter, r *http.Request) {
httpapi.Write(ctx, rw, http.StatusOK, resp)
}

func listFiles(query LSRequest) (LSResponse, error) {
var fullPath []string
switch query.Relativity {
case LSRelativityHome:
home, err := os.UserHomeDir()
if err != nil {
return LSResponse{}, xerrors.Errorf("failed to get user home directory: %w", err)
func listFiles(fs afero.Fs, path string, query workspacesdk.LSRequest) (workspacesdk.LSResponse, error) {
absolutePathString := path
if absolutePathString != "" {
if !filepath.IsAbs(path) {
return workspacesdk.LSResponse{}, xerrors.Errorf("path must be absolute: %q", path)
}
fullPath = []string{home}
case LSRelativityRoot:
if runtime.GOOS == "windows" {
if len(query.Path) == 0 {
return listDrives()
} else {
var fullPath []string
switch query.Relativity {
case workspacesdk.LSRelativityHome:
home, err := os.UserHomeDir()
if err != nil {
return workspacesdk.LSResponse{}, xerrors.Errorf("failed to get user home directory: %w", err)
}
if !WindowsDriveRegex.MatchString(query.Path[0]) {
return LSResponse{}, xerrors.Errorf("invalid drive letter %q", query.Path[0])
fullPath = []string{home}
case workspacesdk.LSRelativityRoot:
if runtime.GOOS == "windows" {
if len(query.Path) == 0 {
return listDrives()
}
if !WindowsDriveRegex.MatchString(query.Path[0]) {
return workspacesdk.LSResponse{}, xerrors.Errorf("invalid drive letter %q", query.Path[0])
}
} else {
fullPath = []string{"/"}
}
} else {
fullPath = []string{"/"}
default:
return workspacesdk.LSResponse{}, xerrors.Errorf("unsupported relativity type %q", query.Relativity)
}
default:
return LSResponse{}, xerrors.Errorf("unsupported relativity type %q", query.Relativity)
}

fullPath = append(fullPath, query.Path...)
fullPathRelative := filepath.Join(fullPath...)
absolutePathString, err := filepath.Abs(fullPathRelative)
if err != nil {
return LSResponse{}, xerrors.Errorf("failed to get absolute path of %q: %w", fullPathRelative, err)
fullPath = append(fullPath, query.Path...)
fullPathRelative := filepath.Join(fullPath...)
var err error
absolutePathString, err = filepath.Abs(fullPathRelative)
if err != nil {
return workspacesdk.LSResponse{}, xerrors.Errorf("failed to get absolute path of %q: %w", fullPathRelative, err)
}
}

// codeql[go/path-injection] - The intent is to allow the user to navigate to any directory in their workspace.
f, err :=os.Open(absolutePathString)
f, err :=fs.Open(absolutePathString)
if err != nil {
return LSResponse{}, xerrors.Errorf("failed to open directory %q: %w", absolutePathString, err)
returnworkspacesdk.LSResponse{}, xerrors.Errorf("failed to open directory %q: %w", absolutePathString, err)
}
defer f.Close()

stat, err := f.Stat()
if err != nil {
return LSResponse{}, xerrors.Errorf("failed to stat directory %q: %w", absolutePathString, err)
returnworkspacesdk.LSResponse{}, xerrors.Errorf("failed to stat directory %q: %w", absolutePathString, err)
}

if !stat.IsDir() {
return LSResponse{}, xerrors.Errorf("path %q is not a directory", absolutePathString)
returnworkspacesdk.LSResponse{}, xerrors.Errorf("path %q is not a directory", absolutePathString)
}

// `contents` may be partially populated even if the operation fails midway.
contents, _ := f.ReadDir(-1)
respContents := make([]LSFile, 0, len(contents))
contents, _ := f.Readdir(-1)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

It's a shame Afero doesn't exposeReadDir here :(

Copy link
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Too true 😢

respContents := make([]workspacesdk.LSFile, 0, len(contents))
for _, file := range contents {
respContents = append(respContents, LSFile{
respContents = append(respContents,workspacesdk.LSFile{
Name: file.Name(),
AbsolutePathString: filepath.Join(absolutePathString, file.Name()),
IsDir: file.IsDir(),
})
}

// Sort alphabetically: directories then files
slices.SortFunc(respContents, func(a, b LSFile) int {
slices.SortFunc(respContents, func(a, bworkspacesdk.LSFile) int {
if a.IsDir && !b.IsDir {
return -1
}
Expand All@@ -117,35 +141,35 @@ func listFiles(query LSRequest) (LSResponse, error) {

absolutePath := pathToArray(absolutePathString)

return LSResponse{
returnworkspacesdk.LSResponse{
AbsolutePath: absolutePath,
AbsolutePathString: absolutePathString,
Contents: respContents,
}, nil
}

func listDrives() (LSResponse, error) {
func listDrives() (workspacesdk.LSResponse, error) {
// disk.Partitions() will return partitions even if there was a failure to
// get one. Any errored partitions will not be returned.
partitionStats, err := disk.Partitions(true)
if err != nil && len(partitionStats) == 0 {
// Only return the error if there were no partitions returned.
return LSResponse{}, xerrors.Errorf("failed to get partitions: %w", err)
returnworkspacesdk.LSResponse{}, xerrors.Errorf("failed to get partitions: %w", err)
}

contents := make([]LSFile, 0, len(partitionStats))
contents := make([]workspacesdk.LSFile, 0, len(partitionStats))
for _, a := range partitionStats {
// Drive letters on Windows have a trailing separator as part of their name.
// i.e. `os.Open("C:")` does not work, but `os.Open("C:\\")` does.
name := a.Mountpoint + string(os.PathSeparator)
contents = append(contents, LSFile{
contents = append(contents,workspacesdk.LSFile{
Name: name,
AbsolutePathString: name,
IsDir: true,
})
}

return LSResponse{
returnworkspacesdk.LSResponse{
AbsolutePath: []string{},
AbsolutePathString: "",
Contents: contents,
Expand All@@ -163,36 +187,3 @@ func pathToArray(path string) []string {
}
return out
}

type LSRequest struct {
// e.g. [], ["repos", "coder"],
Path []string `json:"path"`
// Whether the supplied path is relative to the user's home directory,
// or the root directory.
Relativity LSRelativity `json:"relativity"`
}

type LSResponse struct {
AbsolutePath []string `json:"absolute_path"`
// Returned so clients can display the full path to the user, and
// copy it to configure file sync
// e.g. Windows: "C:\\Users\\coder"
// Linux: "/home/coder"
AbsolutePathString string `json:"absolute_path_string"`
Contents []LSFile `json:"contents"`
}

type LSFile struct {
Name string `json:"name"`
// e.g. "C:\\Users\\coder\\hello.txt"
// "/home/coder/hello.txt"
AbsolutePathString string `json:"absolute_path_string"`
IsDir bool `json:"is_dir"`
}

type LSRelativity string

const (
LSRelativityRoot LSRelativity = "root"
LSRelativityHome LSRelativity = "home"
)
Loading
Loading

[8]ページ先頭

©2009-2025 Movatter.jp