Skip to content

Commit 95a8350

Browse files
committed
Add MCP
1 parent 87e0862 commit 95a8350

File tree

4 files changed

+1270
-0
lines changed

4 files changed

+1270
-0
lines changed

cli/mcp.go

Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
package cli
2+
3+
import (
4+
"encoding/json"
5+
"net/url"
6+
"os"
7+
"path/filepath"
8+
"strings"
9+
10+
"github.com/coder/coder/v2/cli/mcp"
11+
"github.com/coder/coder/v2/codersdk"
12+
"github.com/coder/serpent"
13+
"github.com/mark3labs/mcp-go/server"
14+
"golang.org/x/xerrors"
15+
)
16+
17+
func (r *RootCmd) mcp() *serpent.Command {
18+
cmd := &serpent.Command{
19+
Use: "mcp",
20+
Short: "Run the Coder MCP server and configure it to work with AI tools.",
21+
Long: "The Coder MCP server allows you to automatically create workspaces with parameters.",
22+
Children: []*serpent.Command{
23+
r.mcpConfigure(),
24+
r.mcpServer(),
25+
},
26+
}
27+
return cmd
28+
}
29+
30+
func (r *RootCmd) mcpServer() *serpent.Command {
31+
var mcpServerAgent bool
32+
client := new(codersdk.Client)
33+
cmd := &serpent.Command{
34+
Use: "server",
35+
Short: "Start the Coder MCP server.",
36+
Options: serpent.OptionSet{
37+
serpent.Option{
38+
Flag: "agent",
39+
Env: "CODER_MCP_SERVER_AGENT",
40+
Description: "Start the MCP server in agent mode, with a different set of tools.",
41+
Value: serpent.BoolOf(&mcpServerAgent),
42+
},
43+
},
44+
Handler: func(inv *serpent.Invocation) error {
45+
srv := mcp.New(inv.Context(), func() (*codersdk.Client, error) {
46+
conf := r.createConfig()
47+
var err error
48+
// Read the client URL stored on disk.
49+
if r.clientURL == nil || r.clientURL.String() == "" {
50+
rawURL, err := conf.URL().Read()
51+
// If the configuration files are absent, the user is logged out
52+
if os.IsNotExist(err) {
53+
return nil, xerrors.New(notLoggedInMessage)
54+
}
55+
if err != nil {
56+
return nil, err
57+
}
58+
59+
r.clientURL, err = url.Parse(strings.TrimSpace(rawURL))
60+
if err != nil {
61+
return nil, err
62+
}
63+
}
64+
// Read the token stored on disk.
65+
if r.token == "" {
66+
r.token, err = conf.Session().Read()
67+
// Even if there isn't a token, we don't care.
68+
// Some API routes can be unauthenticated.
69+
if err != nil && !os.IsNotExist(err) {
70+
return nil, err
71+
}
72+
}
73+
74+
err = r.configureClient(inv.Context(), client, r.clientURL, inv)
75+
if err != nil {
76+
return nil, err
77+
}
78+
client.SetSessionToken(r.token)
79+
if r.debugHTTP {
80+
client.PlainLogger = os.Stderr
81+
client.SetLogBodies(true)
82+
}
83+
client.DisableDirectConnections = r.disableDirect
84+
return client, nil
85+
}, &mcp.Options{})
86+
return server.ServeStdio(srv)
87+
},
88+
}
89+
return cmd
90+
}
91+
92+
func (r *RootCmd) mcpConfigure() *serpent.Command {
93+
cmd := &serpent.Command{
94+
Use: "configure",
95+
Short: "Automatically configure the MCP server.",
96+
Children: []*serpent.Command{
97+
r.mcpConfigureClaudeDesktop(),
98+
r.mcpConfigureClaudeCode(),
99+
r.mcpConfigureCursor(),
100+
},
101+
}
102+
return cmd
103+
}
104+
105+
func (r *RootCmd) mcpConfigureClaudeDesktop() *serpent.Command {
106+
cmd := &serpent.Command{
107+
Use: "claude-desktop",
108+
Short: "Configure the Claude Desktop server.",
109+
Handler: func(inv *serpent.Invocation) error {
110+
configPath, err := os.UserConfigDir()
111+
if err != nil {
112+
return err
113+
}
114+
configPath = filepath.Join(configPath, "Claude")
115+
err = os.MkdirAll(configPath, 0755)
116+
if err != nil {
117+
return err
118+
}
119+
configPath = filepath.Join(configPath, "claude_desktop_config.json")
120+
_, err = os.Stat(configPath)
121+
if err != nil {
122+
if !os.IsNotExist(err) {
123+
return err
124+
}
125+
}
126+
contents := map[string]any{}
127+
data, err := os.ReadFile(configPath)
128+
if err != nil {
129+
if !os.IsNotExist(err) {
130+
return err
131+
}
132+
} else {
133+
err = json.Unmarshal(data, &contents)
134+
if err != nil {
135+
return err
136+
}
137+
}
138+
binPath, err := os.Executable()
139+
if err != nil {
140+
return err
141+
}
142+
contents["mcpServers"] = map[string]any{
143+
"coder": map[string]any{"command": binPath, "args": []string{"mcp", "server"}},
144+
}
145+
data, err = json.MarshalIndent(contents, "", " ")
146+
if err != nil {
147+
return err
148+
}
149+
err = os.WriteFile(configPath, data, 0644)
150+
if err != nil {
151+
return err
152+
}
153+
return nil
154+
},
155+
}
156+
return cmd
157+
}
158+
159+
func (r *RootCmd) mcpConfigureClaudeCode() *serpent.Command {
160+
cmd := &serpent.Command{
161+
Use: "claude-code",
162+
Short: "Configure the Claude Code server.",
163+
Handler: func(inv *serpent.Invocation) error {
164+
return nil
165+
},
166+
}
167+
return cmd
168+
}
169+
170+
func (r *RootCmd) mcpConfigureCursor() *serpent.Command {
171+
var project bool
172+
cmd := &serpent.Command{
173+
Use: "cursor",
174+
Short: "Configure Cursor to use Coder MCP.",
175+
Options: serpent.OptionSet{
176+
serpent.Option{
177+
Flag: "project",
178+
Env: "CODER_MCP_CURSOR_PROJECT",
179+
Description: "Use to configure a local project to use the Cursor MCP.",
180+
Value: serpent.BoolOf(&project),
181+
},
182+
},
183+
Handler: func(inv *serpent.Invocation) error {
184+
dir, err := os.Getwd()
185+
if err != nil {
186+
return err
187+
}
188+
if !project {
189+
dir, err = os.UserHomeDir()
190+
if err != nil {
191+
return err
192+
}
193+
}
194+
cursorDir := filepath.Join(dir, ".cursor")
195+
err = os.MkdirAll(cursorDir, 0755)
196+
if err != nil {
197+
return err
198+
}
199+
mcpConfig := filepath.Join(cursorDir, "mcp.json")
200+
_, err = os.Stat(mcpConfig)
201+
contents := map[string]any{}
202+
if err != nil {
203+
if !os.IsNotExist(err) {
204+
return err
205+
}
206+
} else {
207+
data, err := os.ReadFile(mcpConfig)
208+
if err != nil {
209+
return err
210+
}
211+
// The config can be empty, so we don't want to return an error if it is.
212+
if len(data) > 0 {
213+
err = json.Unmarshal(data, &contents)
214+
if err != nil {
215+
return err
216+
}
217+
}
218+
}
219+
mcpServers, ok := contents["mcpServers"].(map[string]any)
220+
if !ok {
221+
mcpServers = map[string]any{}
222+
}
223+
binPath, err := os.Executable()
224+
if err != nil {
225+
return err
226+
}
227+
mcpServers["coder"] = map[string]any{
228+
"command": binPath,
229+
"args": []string{"mcp", "server"},
230+
}
231+
contents["mcpServers"] = mcpServers
232+
data, err := json.MarshalIndent(contents, "", " ")
233+
if err != nil {
234+
return err
235+
}
236+
err = os.WriteFile(mcpConfig, data, 0644)
237+
if err != nil {
238+
return err
239+
}
240+
return nil
241+
},
242+
}
243+
return cmd
244+
}

0 commit comments

Comments
 (0)