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

Commit057cbd4

Browse files
authored
feat(cli): addcoder exp mcp command (#17066)
Adds a `coder exp mcp` command which will start a local MCP serverlistening on stdio with the following capabilities:* Show logged in user (`coder whoami`)* List workspaces (`coder list`)* List templates (`coder templates list`)* Start a workspace (`coder start`)* Stop a workspace (`coder stop`)* Fetch a single workspace (no direct CLI analogue)* Execute a command inside a workspace (`coder exp rpty`)* Report the status of a task (currently a no-op, pending task support)This can be tested as follows:```# Start a local Coder server../scripts/develop.sh# Start a workspace. Currently, creating workspaces is not supported../scripts/coder-dev.sh create -t docker --yes# Add the MCP to your Claude config.claude mcp add coder ./scripts/coder-dev.sh exp mcp# Tell Claude to do something Coder-related. You may need to nudge it to use the tools.claude 'start a docker workspace and tell me what version of python is installed'```
1 parent8ea956f commit057cbd4

File tree

9 files changed

+1469
-5
lines changed

9 files changed

+1469
-5
lines changed

‎cli/clitest/golden.go

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ import (
1111
"strings"
1212
"testing"
1313

14+
"github.com/google/go-cmp/cmp"
1415
"github.com/google/uuid"
16+
"github.com/stretchr/testify/assert"
1517
"github.com/stretchr/testify/require"
1618

1719
"github.com/coder/coder/v2/cli/config"
@@ -117,11 +119,7 @@ func TestGoldenFile(t *testing.T, fileName string, actual []byte, replacements m
117119
require.NoError(t,err,"read golden file, run\"make gen/golden-files\" and commit the changes")
118120

119121
expected=normalizeGoldenFile(t,expected)
120-
require.Equal(
121-
t,string(expected),string(actual),
122-
"golden file mismatch: %s, run\"make gen/golden-files\", verify and commit the changes",
123-
goldenPath,
124-
)
122+
assert.Empty(t,cmp.Diff(string(expected),string(actual)),"golden file mismatch (-want +got): %s, run\"make gen/golden-files\", verify and commit the changes",goldenPath)
125123
}
126124

127125
// normalizeGoldenFile replaces any strings that are system or timing dependent

‎cli/exp.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ func (r *RootCmd) expCmd() *serpent.Command {
1313
Children: []*serpent.Command{
1414
r.scaletestCmd(),
1515
r.errorExample(),
16+
r.mcpCommand(),
1617
r.promptExample(),
1718
r.rptyCommand(),
1819
},

‎cli/exp_mcp.go

Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
1+
package cli
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"errors"
7+
"log"
8+
"os"
9+
"path/filepath"
10+
11+
"cdr.dev/slog"
12+
"cdr.dev/slog/sloggers/sloghuman"
13+
"github.com/coder/coder/v2/cli/cliui"
14+
"github.com/coder/coder/v2/codersdk"
15+
codermcp"github.com/coder/coder/v2/mcp"
16+
"github.com/coder/serpent"
17+
)
18+
19+
func (r*RootCmd)mcpCommand()*serpent.Command {
20+
cmd:=&serpent.Command{
21+
Use:"mcp",
22+
Short:"Run the Coder MCP server and configure it to work with AI tools.",
23+
Long:"The Coder MCP server allows you to automatically create workspaces with parameters.",
24+
Handler:func(i*serpent.Invocation)error {
25+
returni.Command.HelpHandler(i)
26+
},
27+
Children: []*serpent.Command{
28+
r.mcpConfigure(),
29+
r.mcpServer(),
30+
},
31+
}
32+
returncmd
33+
}
34+
35+
func (r*RootCmd)mcpConfigure()*serpent.Command {
36+
cmd:=&serpent.Command{
37+
Use:"configure",
38+
Short:"Automatically configure the MCP server.",
39+
Handler:func(i*serpent.Invocation)error {
40+
returni.Command.HelpHandler(i)
41+
},
42+
Children: []*serpent.Command{
43+
r.mcpConfigureClaudeDesktop(),
44+
r.mcpConfigureClaudeCode(),
45+
r.mcpConfigureCursor(),
46+
},
47+
}
48+
returncmd
49+
}
50+
51+
func (*RootCmd)mcpConfigureClaudeDesktop()*serpent.Command {
52+
cmd:=&serpent.Command{
53+
Use:"claude-desktop",
54+
Short:"Configure the Claude Desktop server.",
55+
Handler:func(_*serpent.Invocation)error {
56+
configPath,err:=os.UserConfigDir()
57+
iferr!=nil {
58+
returnerr
59+
}
60+
configPath=filepath.Join(configPath,"Claude")
61+
err=os.MkdirAll(configPath,0o755)
62+
iferr!=nil {
63+
returnerr
64+
}
65+
configPath=filepath.Join(configPath,"claude_desktop_config.json")
66+
_,err=os.Stat(configPath)
67+
iferr!=nil {
68+
if!os.IsNotExist(err) {
69+
returnerr
70+
}
71+
}
72+
contents:=map[string]any{}
73+
data,err:=os.ReadFile(configPath)
74+
iferr!=nil {
75+
if!os.IsNotExist(err) {
76+
returnerr
77+
}
78+
}else {
79+
err=json.Unmarshal(data,&contents)
80+
iferr!=nil {
81+
returnerr
82+
}
83+
}
84+
binPath,err:=os.Executable()
85+
iferr!=nil {
86+
returnerr
87+
}
88+
contents["mcpServers"]=map[string]any{
89+
"coder":map[string]any{"command":binPath,"args": []string{"exp","mcp","server"}},
90+
}
91+
data,err=json.MarshalIndent(contents,""," ")
92+
iferr!=nil {
93+
returnerr
94+
}
95+
err=os.WriteFile(configPath,data,0o600)
96+
iferr!=nil {
97+
returnerr
98+
}
99+
returnnil
100+
},
101+
}
102+
returncmd
103+
}
104+
105+
func (*RootCmd)mcpConfigureClaudeCode()*serpent.Command {
106+
cmd:=&serpent.Command{
107+
Use:"claude-code",
108+
Short:"Configure the Claude Code server.",
109+
Handler:func(_*serpent.Invocation)error {
110+
returnnil
111+
},
112+
}
113+
returncmd
114+
}
115+
116+
func (*RootCmd)mcpConfigureCursor()*serpent.Command {
117+
varprojectbool
118+
cmd:=&serpent.Command{
119+
Use:"cursor",
120+
Short:"Configure Cursor to use Coder MCP.",
121+
Options: serpent.OptionSet{
122+
serpent.Option{
123+
Flag:"project",
124+
Env:"CODER_MCP_CURSOR_PROJECT",
125+
Description:"Use to configure a local project to use the Cursor MCP.",
126+
Value:serpent.BoolOf(&project),
127+
},
128+
},
129+
Handler:func(_*serpent.Invocation)error {
130+
dir,err:=os.Getwd()
131+
iferr!=nil {
132+
returnerr
133+
}
134+
if!project {
135+
dir,err=os.UserHomeDir()
136+
iferr!=nil {
137+
returnerr
138+
}
139+
}
140+
cursorDir:=filepath.Join(dir,".cursor")
141+
err=os.MkdirAll(cursorDir,0o755)
142+
iferr!=nil {
143+
returnerr
144+
}
145+
mcpConfig:=filepath.Join(cursorDir,"mcp.json")
146+
_,err=os.Stat(mcpConfig)
147+
contents:=map[string]any{}
148+
iferr!=nil {
149+
if!os.IsNotExist(err) {
150+
returnerr
151+
}
152+
}else {
153+
data,err:=os.ReadFile(mcpConfig)
154+
iferr!=nil {
155+
returnerr
156+
}
157+
// The config can be empty, so we don't want to return an error if it is.
158+
iflen(data)>0 {
159+
err=json.Unmarshal(data,&contents)
160+
iferr!=nil {
161+
returnerr
162+
}
163+
}
164+
}
165+
mcpServers,ok:=contents["mcpServers"].(map[string]any)
166+
if!ok {
167+
mcpServers=map[string]any{}
168+
}
169+
binPath,err:=os.Executable()
170+
iferr!=nil {
171+
returnerr
172+
}
173+
mcpServers["coder"]=map[string]any{
174+
"command":binPath,
175+
"args": []string{"exp","mcp","server"},
176+
}
177+
contents["mcpServers"]=mcpServers
178+
data,err:=json.MarshalIndent(contents,""," ")
179+
iferr!=nil {
180+
returnerr
181+
}
182+
err=os.WriteFile(mcpConfig,data,0o600)
183+
iferr!=nil {
184+
returnerr
185+
}
186+
returnnil
187+
},
188+
}
189+
returncmd
190+
}
191+
192+
func (r*RootCmd)mcpServer()*serpent.Command {
193+
var (
194+
client=new(codersdk.Client)
195+
instructionsstring
196+
allowedTools []string
197+
)
198+
return&serpent.Command{
199+
Use:"server",
200+
Handler:func(inv*serpent.Invocation)error {
201+
returnmcpServerHandler(inv,client,instructions,allowedTools)
202+
},
203+
Short:"Start the Coder MCP server.",
204+
Middleware:serpent.Chain(
205+
r.InitClient(client),
206+
),
207+
Options: []serpent.Option{
208+
{
209+
Name:"instructions",
210+
Description:"The instructions to pass to the MCP server.",
211+
Flag:"instructions",
212+
Value:serpent.StringOf(&instructions),
213+
},
214+
{
215+
Name:"allowed-tools",
216+
Description:"Comma-separated list of allowed tools. If not specified, all tools are allowed.",
217+
Flag:"allowed-tools",
218+
Value:serpent.StringArrayOf(&allowedTools),
219+
},
220+
},
221+
}
222+
}
223+
224+
funcmcpServerHandler(inv*serpent.Invocation,client*codersdk.Client,instructionsstring,allowedTools []string)error {
225+
ctx,cancel:=context.WithCancel(inv.Context())
226+
defercancel()
227+
228+
logger:=slog.Make(sloghuman.Sink(inv.Stdout))
229+
230+
me,err:=client.User(ctx,codersdk.Me)
231+
iferr!=nil {
232+
cliui.Errorf(inv.Stderr,"Failed to log in to the Coder deployment.")
233+
cliui.Errorf(inv.Stderr,"Please check your URL and credentials.")
234+
cliui.Errorf(inv.Stderr,"Tip: Run `coder whoami` to check your credentials.")
235+
returnerr
236+
}
237+
cliui.Infof(inv.Stderr,"Starting MCP server")
238+
cliui.Infof(inv.Stderr,"User : %s",me.Username)
239+
cliui.Infof(inv.Stderr,"URL : %s",client.URL)
240+
cliui.Infof(inv.Stderr,"Instructions : %q",instructions)
241+
iflen(allowedTools)>0 {
242+
cliui.Infof(inv.Stderr,"Allowed Tools : %v",allowedTools)
243+
}
244+
cliui.Infof(inv.Stderr,"Press Ctrl+C to stop the server")
245+
246+
// Capture the original stdin, stdout, and stderr.
247+
invStdin:=inv.Stdin
248+
invStdout:=inv.Stdout
249+
invStderr:=inv.Stderr
250+
deferfunc() {
251+
inv.Stdin=invStdin
252+
inv.Stdout=invStdout
253+
inv.Stderr=invStderr
254+
}()
255+
256+
options:= []codermcp.Option{
257+
codermcp.WithInstructions(instructions),
258+
codermcp.WithLogger(&logger),
259+
}
260+
261+
// Add allowed tools option if specified
262+
iflen(allowedTools)>0 {
263+
options=append(options,codermcp.WithAllowedTools(allowedTools))
264+
}
265+
266+
srv:=codermcp.NewStdio(client,options...)
267+
srv.SetErrorLogger(log.New(invStderr,"",log.LstdFlags))
268+
269+
done:=make(chanerror)
270+
gofunc() {
271+
deferclose(done)
272+
srvErr:=srv.Listen(ctx,invStdin,invStdout)
273+
done<-srvErr
274+
}()
275+
276+
iferr:=<-done;err!=nil {
277+
if!errors.Is(err,context.Canceled) {
278+
cliui.Errorf(inv.Stderr,"Failed to start the MCP server: %s",err)
279+
returnerr
280+
}
281+
}
282+
283+
returnnil
284+
}

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp