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

Commitbe7aa58

Browse files
authored
feat: add coder_workspace_ls MCP tool (#19652)
1 parent30330ab commitbe7aa58

File tree

6 files changed

+316
-113
lines changed

6 files changed

+316
-113
lines changed

‎agent/ls.go‎

Lines changed: 66 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -11,23 +11,39 @@ import (
1111
"strings"
1212

1313
"github.com/shirou/gopsutil/v4/disk"
14+
"github.com/spf13/afero"
1415
"golang.org/x/xerrors"
1516

1617
"github.com/coder/coder/v2/coderd/httpapi"
1718
"github.com/coder/coder/v2/codersdk"
19+
"github.com/coder/coder/v2/codersdk/workspacesdk"
1820
)
1921

2022
varWindowsDriveRegex=regexp.MustCompile(`^[a-zA-Z]:\\$`)
2123

22-
func (*agent)HandleLS(rw http.ResponseWriter,r*http.Request) {
24+
func (a*agent)HandleLS(rw http.ResponseWriter,r*http.Request) {
2325
ctx:=r.Context()
2426

25-
varqueryLSRequest
26-
if!httpapi.Read(ctx,rw,r,&query) {
27+
// An absolute path may be optionally provided, otherwise a path split into an
28+
// array must be provided in the body (which can be relative).
29+
query:=r.URL.Query()
30+
parser:=httpapi.NewQueryParamParser()
31+
path:=parser.String(query,"","path")
32+
parser.ErrorExcessParams(query)
33+
iflen(parser.Errors)>0 {
34+
httpapi.Write(ctx,rw,http.StatusBadRequest, codersdk.Response{
35+
Message:"Query parameters have invalid values.",
36+
Validations:parser.Errors,
37+
})
2738
return
2839
}
2940

30-
resp,err:=listFiles(query)
41+
varreq workspacesdk.LSRequest
42+
if!httpapi.Read(ctx,rw,r,&req) {
43+
return
44+
}
45+
46+
resp,err:=listFiles(a.filesystem,path,req)
3147
iferr!=nil {
3248
status:=http.StatusInternalServerError
3349
switch {
@@ -46,66 +62,74 @@ func (*agent) HandleLS(rw http.ResponseWriter, r *http.Request) {
4662
httpapi.Write(ctx,rw,http.StatusOK,resp)
4763
}
4864

49-
funclistFiles(queryLSRequest) (LSResponse,error) {
50-
varfullPath []string
51-
switchquery.Relativity {
52-
caseLSRelativityHome:
53-
home,err:=os.UserHomeDir()
54-
iferr!=nil {
55-
returnLSResponse{},xerrors.Errorf("failed to get user home directory: %w",err)
65+
funclistFiles(fs afero.Fs,pathstring,query workspacesdk.LSRequest) (workspacesdk.LSResponse,error) {
66+
absolutePathString:=path
67+
ifabsolutePathString!="" {
68+
if!filepath.IsAbs(path) {
69+
return workspacesdk.LSResponse{},xerrors.Errorf("path must be absolute: %q",path)
5670
}
57-
fullPath= []string{home}
58-
caseLSRelativityRoot:
59-
ifruntime.GOOS=="windows" {
60-
iflen(query.Path)==0 {
61-
returnlistDrives()
71+
}else {
72+
varfullPath []string
73+
switchquery.Relativity {
74+
caseworkspacesdk.LSRelativityHome:
75+
home,err:=os.UserHomeDir()
76+
iferr!=nil {
77+
return workspacesdk.LSResponse{},xerrors.Errorf("failed to get user home directory: %w",err)
6278
}
63-
if!WindowsDriveRegex.MatchString(query.Path[0]) {
64-
returnLSResponse{},xerrors.Errorf("invalid drive letter %q",query.Path[0])
79+
fullPath= []string{home}
80+
caseworkspacesdk.LSRelativityRoot:
81+
ifruntime.GOOS=="windows" {
82+
iflen(query.Path)==0 {
83+
returnlistDrives()
84+
}
85+
if!WindowsDriveRegex.MatchString(query.Path[0]) {
86+
return workspacesdk.LSResponse{},xerrors.Errorf("invalid drive letter %q",query.Path[0])
87+
}
88+
}else {
89+
fullPath= []string{"/"}
6590
}
66-
}else {
67-
fullPath= []string{"/"}
91+
default:
92+
return workspacesdk.LSResponse{},xerrors.Errorf("unsupported relativity type %q",query.Relativity)
6893
}
69-
default:
70-
returnLSResponse{},xerrors.Errorf("unsupported relativity type %q",query.Relativity)
71-
}
7294

73-
fullPath=append(fullPath,query.Path...)
74-
fullPathRelative:=filepath.Join(fullPath...)
75-
absolutePathString,err:=filepath.Abs(fullPathRelative)
76-
iferr!=nil {
77-
returnLSResponse{},xerrors.Errorf("failed to get absolute path of %q: %w",fullPathRelative,err)
95+
fullPath=append(fullPath,query.Path...)
96+
fullPathRelative:=filepath.Join(fullPath...)
97+
varerrerror
98+
absolutePathString,err=filepath.Abs(fullPathRelative)
99+
iferr!=nil {
100+
return workspacesdk.LSResponse{},xerrors.Errorf("failed to get absolute path of %q: %w",fullPathRelative,err)
101+
}
78102
}
79103

80104
// codeql[go/path-injection] - The intent is to allow the user to navigate to any directory in their workspace.
81-
f,err:=os.Open(absolutePathString)
105+
f,err:=fs.Open(absolutePathString)
82106
iferr!=nil {
83-
returnLSResponse{},xerrors.Errorf("failed to open directory %q: %w",absolutePathString,err)
107+
returnworkspacesdk.LSResponse{},xerrors.Errorf("failed to open directory %q: %w",absolutePathString,err)
84108
}
85109
deferf.Close()
86110

87111
stat,err:=f.Stat()
88112
iferr!=nil {
89-
returnLSResponse{},xerrors.Errorf("failed to stat directory %q: %w",absolutePathString,err)
113+
returnworkspacesdk.LSResponse{},xerrors.Errorf("failed to stat directory %q: %w",absolutePathString,err)
90114
}
91115

92116
if!stat.IsDir() {
93-
returnLSResponse{},xerrors.Errorf("path %q is not a directory",absolutePathString)
117+
returnworkspacesdk.LSResponse{},xerrors.Errorf("path %q is not a directory",absolutePathString)
94118
}
95119

96120
// `contents` may be partially populated even if the operation fails midway.
97-
contents,_:=f.ReadDir(-1)
98-
respContents:=make([]LSFile,0,len(contents))
121+
contents,_:=f.Readdir(-1)
122+
respContents:=make([]workspacesdk.LSFile,0,len(contents))
99123
for_,file:=rangecontents {
100-
respContents=append(respContents,LSFile{
124+
respContents=append(respContents,workspacesdk.LSFile{
101125
Name:file.Name(),
102126
AbsolutePathString:filepath.Join(absolutePathString,file.Name()),
103127
IsDir:file.IsDir(),
104128
})
105129
}
106130

107131
// Sort alphabetically: directories then files
108-
slices.SortFunc(respContents,func(a,bLSFile)int {
132+
slices.SortFunc(respContents,func(a,bworkspacesdk.LSFile)int {
109133
ifa.IsDir&&!b.IsDir {
110134
return-1
111135
}
@@ -117,35 +141,35 @@ func listFiles(query LSRequest) (LSResponse, error) {
117141

118142
absolutePath:=pathToArray(absolutePathString)
119143

120-
returnLSResponse{
144+
returnworkspacesdk.LSResponse{
121145
AbsolutePath:absolutePath,
122146
AbsolutePathString:absolutePathString,
123147
Contents:respContents,
124148
},nil
125149
}
126150

127-
funclistDrives() (LSResponse,error) {
151+
funclistDrives() (workspacesdk.LSResponse,error) {
128152
// disk.Partitions() will return partitions even if there was a failure to
129153
// get one. Any errored partitions will not be returned.
130154
partitionStats,err:=disk.Partitions(true)
131155
iferr!=nil&&len(partitionStats)==0 {
132156
// Only return the error if there were no partitions returned.
133-
returnLSResponse{},xerrors.Errorf("failed to get partitions: %w",err)
157+
returnworkspacesdk.LSResponse{},xerrors.Errorf("failed to get partitions: %w",err)
134158
}
135159

136-
contents:=make([]LSFile,0,len(partitionStats))
160+
contents:=make([]workspacesdk.LSFile,0,len(partitionStats))
137161
for_,a:=rangepartitionStats {
138162
// Drive letters on Windows have a trailing separator as part of their name.
139163
// i.e. `os.Open("C:")` does not work, but `os.Open("C:\\")` does.
140164
name:=a.Mountpoint+string(os.PathSeparator)
141-
contents=append(contents,LSFile{
165+
contents=append(contents,workspacesdk.LSFile{
142166
Name:name,
143167
AbsolutePathString:name,
144168
IsDir:true,
145169
})
146170
}
147171

148-
returnLSResponse{
172+
returnworkspacesdk.LSResponse{
149173
AbsolutePath: []string{},
150174
AbsolutePathString:"",
151175
Contents:contents,
@@ -163,36 +187,3 @@ func pathToArray(path string) []string {
163187
}
164188
returnout
165189
}
166-
167-
typeLSRequeststruct {
168-
// e.g. [], ["repos", "coder"],
169-
Path []string`json:"path"`
170-
// Whether the supplied path is relative to the user's home directory,
171-
// or the root directory.
172-
RelativityLSRelativity`json:"relativity"`
173-
}
174-
175-
typeLSResponsestruct {
176-
AbsolutePath []string`json:"absolute_path"`
177-
// Returned so clients can display the full path to the user, and
178-
// copy it to configure file sync
179-
// e.g. Windows: "C:\\Users\\coder"
180-
// Linux: "/home/coder"
181-
AbsolutePathStringstring`json:"absolute_path_string"`
182-
Contents []LSFile`json:"contents"`
183-
}
184-
185-
typeLSFilestruct {
186-
Namestring`json:"name"`
187-
// e.g. "C:\\Users\\coder\\hello.txt"
188-
// "/home/coder/hello.txt"
189-
AbsolutePathStringstring`json:"absolute_path_string"`
190-
IsDirbool`json:"is_dir"`
191-
}
192-
193-
typeLSRelativitystring
194-
195-
const (
196-
LSRelativityRootLSRelativity="root"
197-
LSRelativityHomeLSRelativity="home"
198-
)

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp