@@ -2,7 +2,10 @@ package cli
2
2
3
3
import (
4
4
"context"
5
+ "encoding/json"
5
6
"errors"
7
+ "os"
8
+ "path/filepath"
6
9
7
10
"cdr.dev/slog"
8
11
"cdr.dev/slog/sloggers/sloghuman"
@@ -13,17 +16,184 @@ import (
13
16
)
14
17
15
18
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 {
16
186
var (
17
187
client = new (codersdk.Client )
18
188
instructions string
19
189
allowedTools []string
20
190
)
21
191
return & serpent.Command {
22
- Use :"mcp " ,
192
+ Use :"server " ,
23
193
Handler :func (inv * serpent.Invocation )error {
24
- return mcpHandler (inv ,client ,instructions ,allowedTools )
194
+ return mcpServerHandler (inv ,client ,instructions ,allowedTools )
25
195
},
26
- Short :"Startan MCP server that can be used to interact with a Coder deployment ." ,
196
+ Short :"Startthe Coder MCP server." ,
27
197
Middleware :serpent .Chain (
28
198
r .InitClient (client ),
29
199
),
@@ -44,7 +214,7 @@ func (r *RootCmd) mcpCommand() *serpent.Command {
44
214
}
45
215
}
46
216
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 {
48
218
ctx ,cancel := context .WithCancel (inv .Context ())
49
219
defer cancel ()
50
220