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

Commit92282b2

Browse files
committed
feat(cli): implement exp mcp configure claude-code command
1 parent27d2343 commit92282b2

File tree

2 files changed

+329
-2
lines changed

2 files changed

+329
-2
lines changed

‎cli/exp_mcp.go

Lines changed: 203 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import (
88
"path/filepath"
99

1010
"github.com/mark3labs/mcp-go/server"
11+
"github.com/spf13/afero"
12+
"golang.org/x/xerrors"
1113

1214
"cdr.dev/slog"
1315
"cdr.dev/slog/sloggers/sloghuman"
@@ -106,12 +108,95 @@ func (*RootCmd) mcpConfigureClaudeDesktop() *serpent.Command {
106108
}
107109

108110
func (*RootCmd)mcpConfigureClaudeCode()*serpent.Command {
111+
var (
112+
apiKeystring
113+
claudeConfigPathstring
114+
projectDirectorystring
115+
systemPromptstring
116+
taskPromptstring
117+
testBinaryNamestring
118+
)
109119
cmd:=&serpent.Command{
110120
Use:"claude-code",
111121
Short:"Configure the Claude Code server.",
112-
Handler:func(_*serpent.Invocation)error {
122+
Handler:func(inv*serpent.Invocation)error {
123+
fs:=afero.NewOsFs()
124+
binPath,err:=os.Executable()
125+
iferr!=nil {
126+
returnxerrors.Errorf("failed to get executable path: %w",err)
127+
}
128+
iftestBinaryName!="" {
129+
binPath=testBinaryName
130+
}
131+
configureClaudeEnv:=map[string]string{}
132+
if_,ok:=os.LookupEnv("CODER_AGENT_TOKEN");ok {
133+
configureClaudeEnv["CODER_AGENT_TOKEN"]=os.Getenv("CODER_AGENT_TOKEN")
134+
}
135+
136+
iferr:=configureClaude(fs,ClaudeConfig{
137+
AllowedTools: []string{},
138+
APIKey:apiKey,
139+
ConfigPath:claudeConfigPath,
140+
ProjectDirectory:projectDirectory,
141+
MCPServers:map[string]ClaudeConfigMCP{
142+
"coder": {
143+
Command:binPath,
144+
Args: []string{"exp","mcp","server"},
145+
Env:configureClaudeEnv,
146+
},
147+
},
148+
});err!=nil {
149+
returnxerrors.Errorf("failed to configure claude: %w",err)
150+
}
151+
cliui.Infof(inv.Stderr,"Wrote config to %s",claudeConfigPath)
113152
returnnil
114153
},
154+
Options: []serpent.Option{
155+
{
156+
Name:"claude-config-path",
157+
Description:"The path to the Claude config file.",
158+
Env:"CODER_MCP_CLAUDE_CONFIG_PATH",
159+
Flag:"claude-config-path",
160+
Value:serpent.StringOf(&claudeConfigPath),
161+
Default:filepath.Join(os.Getenv("HOME"),".claude.json"),
162+
},
163+
{
164+
Name:"api-key",
165+
Description:"The API key to use for the Claude Code server.",
166+
Env:"CODER_MCP_CLAUDE_API_KEY",
167+
Flag:"claude-api-key",
168+
Value:serpent.StringOf(&apiKey),
169+
},
170+
{
171+
Name:"system-prompt",
172+
Description:"The system prompt to use for the Claude Code server.",
173+
Env:"CODER_MCP_CLAUDE_SYSTEM_PROMPT",
174+
Flag:"claude-system-prompt",
175+
Value:serpent.StringOf(&systemPrompt),
176+
},
177+
{
178+
Name:"task-prompt",
179+
Description:"The task prompt to use for the Claude Code server.",
180+
Env:"CODER_MCP_CLAUDE_TASK_PROMPT",
181+
Flag:"claude-task-prompt",
182+
Value:serpent.StringOf(&taskPrompt),
183+
},
184+
{
185+
Name:"project-directory",
186+
Description:"The project directory to use for the Claude Code server.",
187+
Env:"CODER_MCP_CLAUDE_PROJECT_DIRECTORY",
188+
Flag:"claude-project-directory",
189+
Value:serpent.StringOf(&projectDirectory),
190+
},
191+
{
192+
Name:"test-binary-name",
193+
Description:"Only used for testing.",
194+
Env:"CODER_MCP_CLAUDE_TEST_BINARY_NAME",
195+
Flag:"claude-test-binary-name",
196+
Value:serpent.StringOf(&testBinaryName),
197+
Hidden:true,
198+
},
199+
},
115200
}
116201
returncmd
117202
}
@@ -317,3 +402,120 @@ func mcpServerHandler(inv *serpent.Invocation, client *codersdk.Client, instruct
317402

318403
returnnil
319404
}
405+
406+
typeClaudeConfigstruct {
407+
ConfigPathstring
408+
ProjectDirectorystring
409+
APIKeystring
410+
AllowedTools []string
411+
MCPServersmap[string]ClaudeConfigMCP
412+
}
413+
414+
typeClaudeConfigMCPstruct {
415+
Commandstring`json:"command"`
416+
Args []string`json:"args"`
417+
Envmap[string]string`json:"env"`
418+
}
419+
420+
funcconfigureClaude(fs afero.Fs,cfgClaudeConfig)error {
421+
ifcfg.ConfigPath=="" {
422+
cfg.ConfigPath=filepath.Join(os.Getenv("HOME"),".claude.json")
423+
}
424+
varconfigmap[string]any
425+
_,err:=fs.Stat(cfg.ConfigPath)
426+
iferr!=nil {
427+
if!os.IsNotExist(err) {
428+
returnxerrors.Errorf("failed to stat claude config: %w",err)
429+
}
430+
// Touch the file to create it if it doesn't exist.
431+
iferr=afero.WriteFile(fs,cfg.ConfigPath, []byte(`{}`),0600);err!=nil {
432+
returnxerrors.Errorf("failed to touch claude config: %w",err)
433+
}
434+
}
435+
oldConfigBytes,err:=afero.ReadFile(fs,cfg.ConfigPath)
436+
iferr!=nil {
437+
returnxerrors.Errorf("failed to read claude config: %w",err)
438+
}
439+
err=json.Unmarshal(oldConfigBytes,&config)
440+
iferr!=nil {
441+
returnxerrors.Errorf("failed to unmarshal claude config: %w",err)
442+
}
443+
444+
ifcfg.APIKey!="" {
445+
// Stops Claude from requiring the user to generate
446+
// a Claude-specific API key.
447+
config["primaryApiKey"]=cfg.APIKey
448+
}
449+
// Stops Claude from asking for onboarding.
450+
config["hasCompletedOnboarding"]=true
451+
// Stops Claude from asking for permissions.
452+
config["bypassPermissionsModeAccepted"]=true
453+
config["autoUpdaterStatus"]="disabled"
454+
// Stops Claude from asking for cost threshold.
455+
config["hasAcknowledgedCostThreshold"]=true
456+
457+
projects,ok:=config["projects"].(map[string]any)
458+
if!ok {
459+
projects=make(map[string]any)
460+
}
461+
462+
project,ok:=projects[cfg.ProjectDirectory].(map[string]any)
463+
if!ok {
464+
project=make(map[string]any)
465+
}
466+
467+
allowedTools,ok:=project["allowedTools"].([]string)
468+
if!ok {
469+
allowedTools= []string{}
470+
}
471+
472+
// Add cfg.AllowedTools to the list if they're not already present.
473+
for_,tool:=rangecfg.AllowedTools {
474+
for_,existingTool:=rangeallowedTools {
475+
iftool==existingTool {
476+
continue
477+
}
478+
}
479+
allowedTools=append(allowedTools,tool)
480+
}
481+
project["allowedTools"]=allowedTools
482+
project["hasTrustDialogAccepted"]=true
483+
project["hasCompletedProjectOnboarding"]=true
484+
485+
mcpServers,ok:=project["mcpServers"].(map[string]any)
486+
if!ok {
487+
mcpServers=make(map[string]any)
488+
}
489+
forname,mcp:=rangecfg.MCPServers {
490+
mcpServers[name]=mcp
491+
}
492+
project["mcpServers"]=mcpServers
493+
// Prevents Claude from asking the user to complete the project onboarding.
494+
project["hasCompletedProjectOnboarding"]=true
495+
496+
history,ok:=project["history"].([]string)
497+
injectedHistoryLine:="make sure to read claude.md and report tasks properly"
498+
499+
if!ok||len(history)==0 {
500+
// History doesn't exist or is empty, create it with our injected line
501+
history= []string{injectedHistoryLine}
502+
}elseifhistory[0]!=injectedHistoryLine {
503+
// Check if our line is already the first item
504+
// Prepend our line to the existing history
505+
history=append([]string{injectedHistoryLine},history...)
506+
}
507+
project["history"]=history
508+
509+
projects[cfg.ProjectDirectory]=project
510+
config["projects"]=projects
511+
512+
newConfigBytes,err:=json.MarshalIndent(config,""," ")
513+
iferr!=nil {
514+
returnxerrors.Errorf("failed to marshal claude config: %w",err)
515+
}
516+
err=afero.WriteFile(fs,cfg.ConfigPath,newConfigBytes,0644)
517+
iferr!=nil {
518+
returnxerrors.Errorf("failed to write claude config: %w",err)
519+
}
520+
returnnil
521+
}

‎cli/exp_mcp_test.go

Lines changed: 126 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package cli_test
33
import (
44
"context"
55
"encoding/json"
6+
"os"
7+
"path/filepath"
68
"runtime"
79
"slices"
810
"testing"
@@ -16,7 +18,7 @@ import (
1618
"github.com/coder/coder/v2/testutil"
1719
)
1820

19-
funcTestExpMcp(t*testing.T) {
21+
funcTestExpMcpServer(t*testing.T) {
2022
t.Parallel()
2123

2224
// Reading to / writing from the PTY is flaky on non-linux systems.
@@ -140,3 +142,126 @@ func TestExpMcp(t *testing.T) {
140142
assert.ErrorContains(t,err,"your session has expired")
141143
})
142144
}
145+
146+
funcTestExpMcpConfigure(t*testing.T) {
147+
t.Run("ClaudeCode",func(t*testing.T) {
148+
t.Setenv("CODER_AGENT_TOKEN","test-agent-token")
149+
ctx:=testutil.Context(t,testutil.WaitShort)
150+
cancelCtx,cancel:=context.WithCancel(ctx)
151+
t.Cleanup(cancel)
152+
153+
client:=coderdtest.New(t,nil)
154+
_=coderdtest.CreateFirstUser(t,client)
155+
156+
tmpDir:=t.TempDir()
157+
claudeConfigPath:=filepath.Join(tmpDir,"claude.json")
158+
expectedConfig:=`{
159+
"autoUpdaterStatus": "disabled",
160+
"bypassPermissionsModeAccepted": true,
161+
"hasAcknowledgedCostThreshold": true,
162+
"hasCompletedOnboarding": true,
163+
"primaryApiKey": "test-api-key",
164+
"projects": {
165+
"/path/to/project": {
166+
"allowedTools": [],
167+
"hasCompletedProjectOnboarding": true,
168+
"hasTrustDialogAccepted": true,
169+
"history": [
170+
"make sure to read claude.md and report tasks properly"
171+
],
172+
"mcpServers": {
173+
"coder": {
174+
"command": "pathtothecoderbinary",
175+
"args": ["exp", "mcp", "server"],
176+
"env": {
177+
"CODER_AGENT_TOKEN": "test-agent-token"
178+
}
179+
}
180+
}
181+
}
182+
}
183+
}`
184+
185+
inv,root:=clitest.New(t,"exp","mcp","configure","claude-code",
186+
"--claude-api-key=test-api-key",
187+
"--claude-config-path="+claudeConfigPath,
188+
"--claude-project-directory=/path/to/project",
189+
"--claude-system-prompt=test-system-prompt",
190+
"--claude-task-prompt=test-task-prompt",
191+
"--claude-test-binary-name=pathtothecoderbinary",
192+
)
193+
clitest.SetupConfig(t,client,root)
194+
195+
err:=inv.WithContext(cancelCtx).Run()
196+
require.NoError(t,err,"failed to configure claude code")
197+
require.FileExists(t,claudeConfigPath,"claude config file should exist")
198+
claudeConfig,err:=os.ReadFile(claudeConfigPath)
199+
require.NoError(t,err,"failed to read claude config path")
200+
testutil.RequireJSONEq(t,expectedConfig,string(claudeConfig))
201+
})
202+
203+
t.Run("ExistingConfig",func(t*testing.T) {
204+
t.Setenv("CODER_AGENT_TOKEN","test-agent-token")
205+
206+
ctx:=testutil.Context(t,testutil.WaitShort)
207+
cancelCtx,cancel:=context.WithCancel(ctx)
208+
t.Cleanup(cancel)
209+
210+
client:=coderdtest.New(t,nil)
211+
_=coderdtest.CreateFirstUser(t,client)
212+
213+
tmpDir:=t.TempDir()
214+
claudeConfigPath:=filepath.Join(tmpDir,"claude.json")
215+
err:=os.WriteFile(claudeConfigPath, []byte(`{
216+
"bypassPermissionsModeAccepted": false,
217+
"hasCompletedOnboarding": false,
218+
"primaryApiKey": "magic-api-key"
219+
}`),0o600)
220+
require.NoError(t,err,"failed to write claude config path")
221+
222+
expectedConfig:=`{
223+
"autoUpdaterStatus": "disabled",
224+
"bypassPermissionsModeAccepted": true,
225+
"hasAcknowledgedCostThreshold": true,
226+
"hasCompletedOnboarding": true,
227+
"primaryApiKey": "test-api-key",
228+
"projects": {
229+
"/path/to/project": {
230+
"allowedTools": [],
231+
"hasCompletedProjectOnboarding": true,
232+
"hasTrustDialogAccepted": true,
233+
"history": [
234+
"make sure to read claude.md and report tasks properly"
235+
],
236+
"mcpServers": {
237+
"coder": {
238+
"command": "pathtothecoderbinary",
239+
"args": ["exp", "mcp", "server"],
240+
"env": {
241+
"CODER_AGENT_TOKEN": "test-agent-token"
242+
}
243+
}
244+
}
245+
}
246+
}
247+
}`
248+
249+
inv,root:=clitest.New(t,"exp","mcp","configure","claude-code",
250+
"--claude-api-key=test-api-key",
251+
"--claude-config-path="+claudeConfigPath,
252+
"--claude-project-directory=/path/to/project",
253+
"--claude-system-prompt=test-system-prompt",
254+
"--claude-task-prompt=test-task-prompt",
255+
"--claude-test-binary-name=pathtothecoderbinary",
256+
)
257+
258+
clitest.SetupConfig(t,client,root)
259+
260+
err=inv.WithContext(cancelCtx).Run()
261+
require.NoError(t,err,"failed to configure claude code")
262+
require.FileExists(t,claudeConfigPath,"claude config file should exist")
263+
claudeConfig,err:=os.ReadFile(claudeConfigPath)
264+
require.NoError(t,err,"failed to read claude config path")
265+
testutil.RequireJSONEq(t,expectedConfig,string(claudeConfig))
266+
})
267+
}

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp