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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy