|
8 | 8 | "path/filepath"
|
9 | 9 |
|
10 | 10 | "github.com/mark3labs/mcp-go/server"
|
| 11 | +"github.com/spf13/afero" |
| 12 | +"golang.org/x/xerrors" |
11 | 13 |
|
12 | 14 | "cdr.dev/slog"
|
13 | 15 | "cdr.dev/slog/sloggers/sloghuman"
|
@@ -106,12 +108,95 @@ func (*RootCmd) mcpConfigureClaudeDesktop() *serpent.Command {
|
106 | 108 | }
|
107 | 109 |
|
108 | 110 | func (*RootCmd)mcpConfigureClaudeCode()*serpent.Command {
|
| 111 | +var ( |
| 112 | +apiKeystring |
| 113 | +claudeConfigPathstring |
| 114 | +projectDirectorystring |
| 115 | +systemPromptstring |
| 116 | +taskPromptstring |
| 117 | +testBinaryNamestring |
| 118 | +) |
109 | 119 | cmd:=&serpent.Command{
|
110 | 120 | Use:"claude-code",
|
111 | 121 | 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) |
113 | 152 | returnnil
|
114 | 153 | },
|
| 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 | +}, |
115 | 200 | }
|
116 | 201 | returncmd
|
117 | 202 | }
|
@@ -317,3 +402,120 @@ func mcpServerHandler(inv *serpent.Invocation, client *codersdk.Client, instruct
|
317 | 402 |
|
318 | 403 | returnnil
|
319 | 404 | }
|
| 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 | +} |