Skip to content

Commit 86fdd92

Browse files
committed
feat(cli): add in exp mcp configure commands from kyle/tasks
1 parent 9086555 commit 86fdd92

File tree

4 files changed

+179
-9
lines changed

4 files changed

+179
-9
lines changed

cli/exp_mcp.go

Lines changed: 174 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ package cli
22

33
import (
44
"context"
5+
"encoding/json"
56
"errors"
7+
"os"
8+
"path/filepath"
69

710
"cdr.dev/slog"
811
"cdr.dev/slog/sloggers/sloghuman"
@@ -13,17 +16,184 @@ import (
1316
)
1417

1518
func (r *RootCmd) mcpCommand() *serpent.Command {
19+
cmd := &serpent.Command{
20+
Use: "mcp",
21+
Short: "Run the Coder MCP server and configure it to work with AI tools.",
22+
Long: "The Coder MCP server allows you to automatically create workspaces with parameters.",
23+
Children: []*serpent.Command{
24+
r.mcpConfigure(),
25+
r.mcpServer(),
26+
},
27+
}
28+
return cmd
29+
}
30+
31+
func (r *RootCmd) mcpConfigure() *serpent.Command {
32+
cmd := &serpent.Command{
33+
Use: "configure",
34+
Short: "Automatically configure the MCP server.",
35+
Children: []*serpent.Command{
36+
r.mcpConfigureClaudeDesktop(),
37+
r.mcpConfigureClaudeCode(),
38+
r.mcpConfigureCursor(),
39+
},
40+
}
41+
return cmd
42+
}
43+
44+
func (r *RootCmd) mcpConfigureClaudeDesktop() *serpent.Command {
45+
cmd := &serpent.Command{
46+
Use: "claude-desktop",
47+
Short: "Configure the Claude Desktop server.",
48+
Handler: func(_ *serpent.Invocation) error {
49+
configPath, err := os.UserConfigDir()
50+
if err != nil {
51+
return err
52+
}
53+
configPath = filepath.Join(configPath, "Claude")
54+
err = os.MkdirAll(configPath, 0755)
55+
if err != nil {
56+
return err
57+
}
58+
configPath = filepath.Join(configPath, "claude_desktop_config.json")
59+
_, err = os.Stat(configPath)
60+
if err != nil {
61+
if !os.IsNotExist(err) {
62+
return err
63+
}
64+
}
65+
contents := map[string]any{}
66+
data, err := os.ReadFile(configPath)
67+
if err != nil {
68+
if !os.IsNotExist(err) {
69+
return err
70+
}
71+
} else {
72+
err = json.Unmarshal(data, &contents)
73+
if err != nil {
74+
return err
75+
}
76+
}
77+
binPath, err := os.Executable()
78+
if err != nil {
79+
return err
80+
}
81+
contents["mcpServers"] = map[string]any{
82+
"coder": map[string]any{"command": binPath, "args": []string{"mcp", "server"}},
83+
}
84+
data, err = json.MarshalIndent(contents, "", " ")
85+
if err != nil {
86+
return err
87+
}
88+
err = os.WriteFile(configPath, data, 0600)
89+
if err != nil {
90+
return err
91+
}
92+
return nil
93+
},
94+
}
95+
return cmd
96+
}
97+
98+
func (_ *RootCmd) mcpConfigureClaudeCode() *serpent.Command {
99+
cmd := &serpent.Command{
100+
Use: "claude-code",
101+
Short: "Configure the Claude Code server.",
102+
Handler: func(_ *serpent.Invocation) error {
103+
return nil
104+
},
105+
}
106+
return cmd
107+
}
108+
109+
func (_ *RootCmd) mcpConfigureCursor() *serpent.Command {
110+
var project bool
111+
cmd := &serpent.Command{
112+
Use: "cursor",
113+
Short: "Configure Cursor to use Coder MCP.",
114+
Options: serpent.OptionSet{
115+
serpent.Option{
116+
Flag: "project",
117+
Env: "CODER_MCP_CURSOR_PROJECT",
118+
Description: "Use to configure a local project to use the Cursor MCP.",
119+
Value: serpent.BoolOf(&project),
120+
},
121+
},
122+
Handler: func(_ *serpent.Invocation) error {
123+
dir, err := os.Getwd()
124+
if err != nil {
125+
return err
126+
}
127+
if !project {
128+
dir, err = os.UserHomeDir()
129+
if err != nil {
130+
return err
131+
}
132+
}
133+
cursorDir := filepath.Join(dir, ".cursor")
134+
err = os.MkdirAll(cursorDir, 0755)
135+
if err != nil {
136+
return err
137+
}
138+
mcpConfig := filepath.Join(cursorDir, "mcp.json")
139+
_, err = os.Stat(mcpConfig)
140+
contents := map[string]any{}
141+
if err != nil {
142+
if !os.IsNotExist(err) {
143+
return err
144+
}
145+
} else {
146+
data, err := os.ReadFile(mcpConfig)
147+
if err != nil {
148+
return err
149+
}
150+
// The config can be empty, so we don't want to return an error if it is.
151+
if len(data) > 0 {
152+
err = json.Unmarshal(data, &contents)
153+
if err != nil {
154+
return err
155+
}
156+
}
157+
}
158+
mcpServers, ok := contents["mcpServers"].(map[string]any)
159+
if !ok {
160+
mcpServers = map[string]any{}
161+
}
162+
binPath, err := os.Executable()
163+
if err != nil {
164+
return err
165+
}
166+
mcpServers["coder"] = map[string]any{
167+
"command": binPath,
168+
"args": []string{"mcp", "server"},
169+
}
170+
contents["mcpServers"] = mcpServers
171+
data, err := json.MarshalIndent(contents, "", " ")
172+
if err != nil {
173+
return err
174+
}
175+
err = os.WriteFile(mcpConfig, data, 0600)
176+
if err != nil {
177+
return err
178+
}
179+
return nil
180+
},
181+
}
182+
return cmd
183+
}
184+
185+
func (r *RootCmd) mcpServer() *serpent.Command {
16186
var (
17187
client = new(codersdk.Client)
18188
instructions string
19189
allowedTools []string
20190
)
21191
return &serpent.Command{
22-
Use: "mcp",
192+
Use: "server",
23193
Handler: func(inv *serpent.Invocation) error {
24-
return mcpHandler(inv, client, instructions, allowedTools)
194+
return mcpServerHandler(inv, client, instructions, allowedTools)
25195
},
26-
Short: "Start an MCP server that can be used to interact with a Coder deployment.",
196+
Short: "Start the Coder MCP server.",
27197
Middleware: serpent.Chain(
28198
r.InitClient(client),
29199
),
@@ -44,7 +214,7 @@ func (r *RootCmd) mcpCommand() *serpent.Command {
44214
}
45215
}
46216

47-
func mcpHandler(inv *serpent.Invocation, client *codersdk.Client, instructions string, allowedTools []string) error {
217+
func mcpServerHandler(inv *serpent.Invocation, client *codersdk.Client, instructions string, allowedTools []string) error {
48218
ctx, cancel := context.WithCancel(inv.Context())
49219
defer cancel()
50220

cli/exp_mcp_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ func TestExpMcp(t *testing.T) {
3636
_ = coderdtest.CreateFirstUser(t, client)
3737

3838
// Given: we run the exp mcp command with allowed tools set
39-
inv, root := clitest.New(t, "exp", "mcp", "--allowed-tools=coder_whoami,coder_list_templates")
39+
inv, root := clitest.New(t, "exp", "mcp", "server", "--allowed-tools=coder_whoami,coder_list_templates")
4040
inv = inv.WithContext(cancelCtx)
4141

4242
pty := ptytest.New(t)
@@ -88,7 +88,7 @@ func TestExpMcp(t *testing.T) {
8888

8989
client := coderdtest.New(t, nil)
9090
_ = coderdtest.CreateFirstUser(t, client)
91-
inv, root := clitest.New(t, "exp", "mcp")
91+
inv, root := clitest.New(t, "exp", "mcp", "server")
9292
inv = inv.WithContext(cancelCtx)
9393

9494
pty := ptytest.New(t)
@@ -128,7 +128,7 @@ func TestExpMcp(t *testing.T) {
128128
t.Cleanup(cancel)
129129

130130
client := coderdtest.New(t, nil)
131-
inv, root := clitest.New(t, "exp", "mcp")
131+
inv, root := clitest.New(t, "exp", "mcp", "server")
132132
inv = inv.WithContext(cancelCtx)
133133

134134
pty := ptytest.New(t)

cli/testdata/TestProvisioners_Golden/list.golden

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
ID CREATED AT LAST SEEN AT NAME VERSION TAGS KEY NAME STATUS CURRENT JOB ID CURRENT JOB STATUS PREVIOUS JOB ID PREVIOUS JOB STATUS ORGANIZATION
1+
ID CREATED AT LAST SEEN AT NAME VERSION TAGS KEY NAME STATUS CURRENT JOB ID CURRENT JOB STATUS PREVIOUS JOB ID PREVIOUS JOB STATUS ORGANIZATION
22
00000000-0000-0000-aaaa-000000000000 ====[timestamp]===== ====[timestamp]===== default-provisioner v0.0.0-devel map[owner: scope:organization] built-in idle <nil> <nil> 00000000-0000-0000-bbbb-000000000001 succeeded Coder
33
00000000-0000-0000-aaaa-000000000001 ====[timestamp]===== ====[timestamp]===== provisioner-1 v0.0.0 map[foo:bar owner: scope:organization] built-in busy 00000000-0000-0000-bbbb-000000000002 running <nil> <nil> Coder
44
00000000-0000-0000-aaaa-000000000002 ====[timestamp]===== ====[timestamp]===== provisioner-2 v0.0.0 map[owner: scope:organization] built-in offline <nil> <nil> 00000000-0000-0000-bbbb-000000000003 succeeded Coder
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
CREATED AT LAST SEEN AT KEY NAME NAME VERSION STATUS TAGS
1+
CREATED AT LAST SEEN AT KEY NAME NAME VERSION STATUS TAGS
22
====[timestamp]===== ====[timestamp]===== built-in test v0.0.0-devel idle map[owner: scope:organization]

0 commit comments

Comments
 (0)