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

Commit88bae05

Browse files
authored
feat(cli): implement exp mcp configure claude-code command (#17195)
Updates `~/.claude.json` and `~/.claude/CLAUDE.md` with requiredsettings for agentic usage.
1 parentf3e5bb9 commit88bae05

File tree

2 files changed

+682
-4
lines changed

2 files changed

+682
-4
lines changed

‎cli/exp_mcp.go

Lines changed: 356 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,11 @@ import (
66
"errors"
77
"os"
88
"path/filepath"
9+
"strings"
910

1011
"github.com/mark3labs/mcp-go/server"
12+
"github.com/spf13/afero"
13+
"golang.org/x/xerrors"
1114

1215
"cdr.dev/slog"
1316
"cdr.dev/slog/sloggers/sloghuman"
@@ -106,12 +109,118 @@ func (*RootCmd) mcpConfigureClaudeDesktop() *serpent.Command {
106109
}
107110

108111
func (*RootCmd)mcpConfigureClaudeCode()*serpent.Command {
112+
var (
113+
apiKeystring
114+
claudeConfigPathstring
115+
claudeMDPathstring
116+
systemPromptstring
117+
appStatusSlugstring
118+
testBinaryNamestring
119+
)
109120
cmd:=&serpent.Command{
110-
Use:"claude-code",
111-
Short:"Configure the Claude Code server.",
112-
Handler:func(_*serpent.Invocation)error {
121+
Use:"claude-code <project-directory>",
122+
Short:"Configure the Claude Code server. You will need to run this command for each project you want to use. Specify the project directory as the first argument.",
123+
Handler:func(inv*serpent.Invocation)error {
124+
iflen(inv.Args)==0 {
125+
returnxerrors.Errorf("project directory is required")
126+
}
127+
projectDirectory:=inv.Args[0]
128+
fs:=afero.NewOsFs()
129+
binPath,err:=os.Executable()
130+
iferr!=nil {
131+
returnxerrors.Errorf("failed to get executable path: %w",err)
132+
}
133+
iftestBinaryName!="" {
134+
binPath=testBinaryName
135+
}
136+
configureClaudeEnv:=map[string]string{}
137+
agentToken,err:=getAgentToken(fs)
138+
iferr!=nil {
139+
cliui.Warnf(inv.Stderr,"failed to get agent token: %s",err)
140+
}else {
141+
configureClaudeEnv["CODER_AGENT_TOKEN"]=agentToken
142+
}
143+
ifappStatusSlug!="" {
144+
configureClaudeEnv["CODER_MCP_APP_STATUS_SLUG"]=appStatusSlug
145+
}
146+
ifdeprecatedSystemPromptEnv,ok:=os.LookupEnv("SYSTEM_PROMPT");ok {
147+
cliui.Warnf(inv.Stderr,"SYSTEM_PROMPT is deprecated, use CODER_MCP_CLAUDE_SYSTEM_PROMPT instead")
148+
systemPrompt=deprecatedSystemPromptEnv
149+
}
150+
151+
iferr:=configureClaude(fs,ClaudeConfig{
152+
// TODO: will this always be stable?
153+
AllowedTools: []string{`mcp__coder__coder_report_task`},
154+
APIKey:apiKey,
155+
ConfigPath:claudeConfigPath,
156+
ProjectDirectory:projectDirectory,
157+
MCPServers:map[string]ClaudeConfigMCP{
158+
"coder": {
159+
Command:binPath,
160+
Args: []string{"exp","mcp","server"},
161+
Env:configureClaudeEnv,
162+
},
163+
},
164+
});err!=nil {
165+
returnxerrors.Errorf("failed to modify claude.json: %w",err)
166+
}
167+
cliui.Infof(inv.Stderr,"Wrote config to %s",claudeConfigPath)
168+
169+
// We also write the system prompt to the CLAUDE.md file.
170+
iferr:=injectClaudeMD(fs,systemPrompt,claudeMDPath);err!=nil {
171+
returnxerrors.Errorf("failed to modify CLAUDE.md: %w",err)
172+
}
173+
cliui.Infof(inv.Stderr,"Wrote CLAUDE.md to %s",claudeMDPath)
113174
returnnil
114175
},
176+
Options: []serpent.Option{
177+
{
178+
Name:"claude-config-path",
179+
Description:"The path to the Claude config file.",
180+
Env:"CODER_MCP_CLAUDE_CONFIG_PATH",
181+
Flag:"claude-config-path",
182+
Value:serpent.StringOf(&claudeConfigPath),
183+
Default:filepath.Join(os.Getenv("HOME"),".claude.json"),
184+
},
185+
{
186+
Name:"claude-md-path",
187+
Description:"The path to CLAUDE.md.",
188+
Env:"CODER_MCP_CLAUDE_MD_PATH",
189+
Flag:"claude-md-path",
190+
Value:serpent.StringOf(&claudeMDPath),
191+
Default:filepath.Join(os.Getenv("HOME"),".claude","CLAUDE.md"),
192+
},
193+
{
194+
Name:"api-key",
195+
Description:"The API key to use for the Claude Code server.",
196+
Env:"CODER_MCP_CLAUDE_API_KEY",
197+
Flag:"claude-api-key",
198+
Value:serpent.StringOf(&apiKey),
199+
},
200+
{
201+
Name:"system-prompt",
202+
Description:"The system prompt to use for the Claude Code server.",
203+
Env:"CODER_MCP_CLAUDE_SYSTEM_PROMPT",
204+
Flag:"claude-system-prompt",
205+
Value:serpent.StringOf(&systemPrompt),
206+
Default:"Send a task status update to notify the user that you are ready for input, and then wait for user input.",
207+
},
208+
{
209+
Name:"app-status-slug",
210+
Description:"The app status slug to use when running the Coder MCP server.",
211+
Env:"CODER_MCP_CLAUDE_APP_STATUS_SLUG",
212+
Flag:"claude-app-status-slug",
213+
Value:serpent.StringOf(&appStatusSlug),
214+
},
215+
{
216+
Name:"test-binary-name",
217+
Description:"Only used for testing.",
218+
Env:"CODER_MCP_CLAUDE_TEST_BINARY_NAME",
219+
Flag:"claude-test-binary-name",
220+
Value:serpent.StringOf(&testBinaryName),
221+
Hidden:true,
222+
},
223+
},
115224
}
116225
returncmd
117226
}
@@ -317,3 +426,247 @@ func mcpServerHandler(inv *serpent.Invocation, client *codersdk.Client, instruct
317426

318427
returnnil
319428
}
429+
430+
typeClaudeConfigstruct {
431+
ConfigPathstring
432+
ProjectDirectorystring
433+
APIKeystring
434+
AllowedTools []string
435+
MCPServersmap[string]ClaudeConfigMCP
436+
}
437+
438+
typeClaudeConfigMCPstruct {
439+
Commandstring`json:"command"`
440+
Args []string`json:"args"`
441+
Envmap[string]string`json:"env"`
442+
}
443+
444+
funcconfigureClaude(fs afero.Fs,cfgClaudeConfig)error {
445+
ifcfg.ConfigPath=="" {
446+
cfg.ConfigPath=filepath.Join(os.Getenv("HOME"),".claude.json")
447+
}
448+
varconfigmap[string]any
449+
_,err:=fs.Stat(cfg.ConfigPath)
450+
iferr!=nil {
451+
if!os.IsNotExist(err) {
452+
returnxerrors.Errorf("failed to stat claude config: %w",err)
453+
}
454+
// Touch the file to create it if it doesn't exist.
455+
iferr=afero.WriteFile(fs,cfg.ConfigPath, []byte(`{}`),0o600);err!=nil {
456+
returnxerrors.Errorf("failed to touch claude config: %w",err)
457+
}
458+
}
459+
oldConfigBytes,err:=afero.ReadFile(fs,cfg.ConfigPath)
460+
iferr!=nil {
461+
returnxerrors.Errorf("failed to read claude config: %w",err)
462+
}
463+
err=json.Unmarshal(oldConfigBytes,&config)
464+
iferr!=nil {
465+
returnxerrors.Errorf("failed to unmarshal claude config: %w",err)
466+
}
467+
468+
ifcfg.APIKey!="" {
469+
// Stops Claude from requiring the user to generate
470+
// a Claude-specific API key.
471+
config["primaryApiKey"]=cfg.APIKey
472+
}
473+
// Stops Claude from asking for onboarding.
474+
config["hasCompletedOnboarding"]=true
475+
// Stops Claude from asking for permissions.
476+
config["bypassPermissionsModeAccepted"]=true
477+
config["autoUpdaterStatus"]="disabled"
478+
// Stops Claude from asking for cost threshold.
479+
config["hasAcknowledgedCostThreshold"]=true
480+
481+
projects,ok:=config["projects"].(map[string]any)
482+
if!ok {
483+
projects=make(map[string]any)
484+
}
485+
486+
project,ok:=projects[cfg.ProjectDirectory].(map[string]any)
487+
if!ok {
488+
project=make(map[string]any)
489+
}
490+
491+
allowedTools,ok:=project["allowedTools"].([]string)
492+
if!ok {
493+
allowedTools= []string{}
494+
}
495+
496+
// Add cfg.AllowedTools to the list if they're not already present.
497+
for_,tool:=rangecfg.AllowedTools {
498+
for_,existingTool:=rangeallowedTools {
499+
iftool==existingTool {
500+
continue
501+
}
502+
}
503+
allowedTools=append(allowedTools,tool)
504+
}
505+
project["allowedTools"]=allowedTools
506+
project["hasTrustDialogAccepted"]=true
507+
project["hasCompletedProjectOnboarding"]=true
508+
509+
mcpServers,ok:=project["mcpServers"].(map[string]any)
510+
if!ok {
511+
mcpServers=make(map[string]any)
512+
}
513+
forname,mcp:=rangecfg.MCPServers {
514+
mcpServers[name]=mcp
515+
}
516+
project["mcpServers"]=mcpServers
517+
// Prevents Claude from asking the user to complete the project onboarding.
518+
project["hasCompletedProjectOnboarding"]=true
519+
520+
history,ok:=project["history"].([]string)
521+
injectedHistoryLine:="make sure to read claude.md and report tasks properly"
522+
523+
if!ok||len(history)==0 {
524+
// History doesn't exist or is empty, create it with our injected line
525+
history= []string{injectedHistoryLine}
526+
}elseifhistory[0]!=injectedHistoryLine {
527+
// Check if our line is already the first item
528+
// Prepend our line to the existing history
529+
history=append([]string{injectedHistoryLine},history...)
530+
}
531+
project["history"]=history
532+
533+
projects[cfg.ProjectDirectory]=project
534+
config["projects"]=projects
535+
536+
newConfigBytes,err:=json.MarshalIndent(config,""," ")
537+
iferr!=nil {
538+
returnxerrors.Errorf("failed to marshal claude config: %w",err)
539+
}
540+
err=afero.WriteFile(fs,cfg.ConfigPath,newConfigBytes,0o644)
541+
iferr!=nil {
542+
returnxerrors.Errorf("failed to write claude config: %w",err)
543+
}
544+
returnnil
545+
}
546+
547+
var (
548+
coderPrompt=`YOU MUST REPORT YOUR STATUS IMMEDIATELY AFTER EACH USER MESSAGE.
549+
INTERRUPT READING FILES OR ANY OTHER TOOL CALL IF YOU HAVE NOT REPORTED A STATUS YET.
550+
You MUST use the mcp__coder__coder_report_task function with all required parameters:
551+
- summary: Short description of what you're doing
552+
- link: A relevant link for the status
553+
- done: Boolean indicating if the task is complete (true/false)
554+
- emoji: Relevant emoji for the status
555+
- need_user_attention: Boolean indicating if the task needs user attention (true/false)
556+
WHEN TO REPORT (MANDATORY):
557+
1. IMMEDIATELY after receiving ANY user message, before any other actions
558+
2. After completing any task
559+
3. When making significant progress
560+
4. When encountering roadblocks
561+
5. When asking questions
562+
6. Before and after using search tools or making code changes
563+
FAILING TO REPORT STATUS PROPERLY WILL RESULT IN INCORRECT BEHAVIOR.`
564+
565+
// Define the guard strings
566+
coderPromptStartGuard="<coder-prompt>"
567+
coderPromptEndGuard="</coder-prompt>"
568+
systemPromptStartGuard="<system-prompt>"
569+
systemPromptEndGuard="</system-prompt>"
570+
)
571+
572+
funcinjectClaudeMD(fs afero.Fs,systemPromptstring,claudeMDPathstring)error {
573+
_,err:=fs.Stat(claudeMDPath)
574+
iferr!=nil {
575+
if!os.IsNotExist(err) {
576+
returnxerrors.Errorf("failed to stat claude config: %w",err)
577+
}
578+
// Write a new file with the system prompt.
579+
iferr=fs.MkdirAll(filepath.Dir(claudeMDPath),0o700);err!=nil {
580+
returnxerrors.Errorf("failed to create claude config directory: %w",err)
581+
}
582+
583+
returnafero.WriteFile(fs,claudeMDPath, []byte(promptsBlock(coderPrompt,systemPrompt,"")),0o600)
584+
}
585+
586+
bs,err:=afero.ReadFile(fs,claudeMDPath)
587+
iferr!=nil {
588+
returnxerrors.Errorf("failed to read claude config: %w",err)
589+
}
590+
591+
// Extract the content without the guarded sections
592+
cleanContent:=string(bs)
593+
594+
// Remove existing coder prompt section if it exists
595+
coderStartIdx:=indexOf(cleanContent,coderPromptStartGuard)
596+
coderEndIdx:=indexOf(cleanContent,coderPromptEndGuard)
597+
ifcoderStartIdx!=-1&&coderEndIdx!=-1&&coderStartIdx<coderEndIdx {
598+
beforeCoderPrompt:=cleanContent[:coderStartIdx]
599+
afterCoderPrompt:=cleanContent[coderEndIdx+len(coderPromptEndGuard):]
600+
cleanContent=beforeCoderPrompt+afterCoderPrompt
601+
}
602+
603+
// Remove existing system prompt section if it exists
604+
systemStartIdx:=indexOf(cleanContent,systemPromptStartGuard)
605+
systemEndIdx:=indexOf(cleanContent,systemPromptEndGuard)
606+
ifsystemStartIdx!=-1&&systemEndIdx!=-1&&systemStartIdx<systemEndIdx {
607+
beforeSystemPrompt:=cleanContent[:systemStartIdx]
608+
afterSystemPrompt:=cleanContent[systemEndIdx+len(systemPromptEndGuard):]
609+
cleanContent=beforeSystemPrompt+afterSystemPrompt
610+
}
611+
612+
// Trim any leading whitespace from the clean content
613+
cleanContent=strings.TrimSpace(cleanContent)
614+
615+
// Create the new content with coder and system prompt prepended
616+
newContent:=promptsBlock(coderPrompt,systemPrompt,cleanContent)
617+
618+
// Write the updated content back to the file
619+
err=afero.WriteFile(fs,claudeMDPath, []byte(newContent),0o600)
620+
iferr!=nil {
621+
returnxerrors.Errorf("failed to write claude config: %w",err)
622+
}
623+
624+
returnnil
625+
}
626+
627+
funcpromptsBlock(coderPrompt,systemPrompt,existingContentstring)string {
628+
varnewContent strings.Builder
629+
_,_=newContent.WriteString(coderPromptStartGuard)
630+
_,_=newContent.WriteRune('\n')
631+
_,_=newContent.WriteString(coderPrompt)
632+
_,_=newContent.WriteRune('\n')
633+
_,_=newContent.WriteString(coderPromptEndGuard)
634+
_,_=newContent.WriteRune('\n')
635+
_,_=newContent.WriteString(systemPromptStartGuard)
636+
_,_=newContent.WriteRune('\n')
637+
_,_=newContent.WriteString(systemPrompt)
638+
_,_=newContent.WriteRune('\n')
639+
_,_=newContent.WriteString(systemPromptEndGuard)
640+
_,_=newContent.WriteRune('\n')
641+
ifexistingContent!="" {
642+
_,_=newContent.WriteString(existingContent)
643+
}
644+
returnnewContent.String()
645+
}
646+
647+
// indexOf returns the index of the first instance of substr in s,
648+
// or -1 if substr is not present in s.
649+
funcindexOf(s,substrstring)int {
650+
fori:=0;i<=len(s)-len(substr);i++ {
651+
ifs[i:i+len(substr)]==substr {
652+
returni
653+
}
654+
}
655+
return-1
656+
}
657+
658+
funcgetAgentToken(fs afero.Fs) (string,error) {
659+
token,ok:=os.LookupEnv("CODER_AGENT_TOKEN")
660+
ifok {
661+
returntoken,nil
662+
}
663+
tokenFile,ok:=os.LookupEnv("CODER_AGENT_TOKEN_FILE")
664+
if!ok {
665+
return"",xerrors.Errorf("CODER_AGENT_TOKEN or CODER_AGENT_TOKEN_FILE must be set for token auth")
666+
}
667+
bs,err:=afero.ReadFile(fs,tokenFile)
668+
iferr!=nil {
669+
return"",xerrors.Errorf("failed to read agent token file: %w",err)
670+
}
671+
returnstring(bs),nil
672+
}

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp