Skip to content

fix(cli): fix prompt issue in mcp configure claude-code #17599

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Apr 29, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
feat(cli): allow overriding default coder prompt in exp mcp configure…
… claude-code
  • Loading branch information
johnstcn committed Apr 29, 2025
commit 8902a93fef434ef25c26ec3397d5e0e9dad3ed73
37 changes: 24 additions & 13 deletions cli/exp_mcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ func (*RootCmd) mcpConfigureClaudeCode() *serpent.Command {
claudeConfigPath string
claudeMDPath string
systemPrompt string
coderPrompt string
appStatusSlug string
testBinaryName string

Expand Down Expand Up @@ -185,8 +186,18 @@ func (*RootCmd) mcpConfigureClaudeCode() *serpent.Command {
reportTaskPrompt = defaultReportTaskPrompt
}

// If a user overrides the coder prompt, we don't want to append
// the report task prompt, as it then becomes the responsibility
// of the user.
actualCoderPrompt := defaultCoderPrompt
if coderPrompt != "" {
actualCoderPrompt = coderPrompt
} else if reportTaskPrompt != "" {
actualCoderPrompt += "\n\n" + reportTaskPrompt
}

// We also write the system prompt to the CLAUDE.md file.
if err := injectClaudeMD(fs, systemPrompt, reportTaskPrompt, claudeMDPath); err != nil {
if err := injectClaudeMD(fs, actualCoderPrompt, systemPrompt, claudeMDPath); err != nil {
return xerrors.Errorf("failed to modify CLAUDE.md: %w", err)
}
cliui.Infof(inv.Stderr, "Wrote CLAUDE.md to %s", claudeMDPath)
Expand Down Expand Up @@ -231,6 +242,14 @@ func (*RootCmd) mcpConfigureClaudeCode() *serpent.Command {
Value: serpent.StringOf(&systemPrompt),
Default: "Send a task status update to notify the user that you are ready for input, and then wait for user input.",
},
{
Name: "coder-prompt",
Description: "The coder prompt to use for the Claude Code server.",
Env: "CODER_MCP_CLAUDE_CODER_PROMPT",
Flag: "claude-coder-prompt",
Value: serpent.StringOf(&coderPrompt),
Default: "", // Empty default means we'll use defaultCoderPrompt from the variable
},
{
Name: "app-status-slug",
Description: "The app status slug to use when running the Coder MCP server.",
Expand Down Expand Up @@ -603,7 +622,7 @@ Task summaries MUST:
systemPromptEndGuard = "</system-prompt>"
)

func injectClaudeMD(fs afero.Fs, systemPrompt, reportTaskPrompt, claudeMDPath string) error {
func injectClaudeMD(fs afero.Fs, coderPrompt, systemPrompt, claudeMDPath string) error {
_, err := fs.Stat(claudeMDPath)
if err != nil {
if !os.IsNotExist(err) {
Expand All @@ -614,7 +633,7 @@ func injectClaudeMD(fs afero.Fs, systemPrompt, reportTaskPrompt, claudeMDPath st
return xerrors.Errorf("failed to create claude config directory: %w", err)
}

return afero.WriteFile(fs, claudeMDPath, []byte(promptsBlock(defaultCoderPrompt, reportTaskPrompt, systemPrompt, "")), 0o600)
return afero.WriteFile(fs, claudeMDPath, []byte(promptsBlock(coderPrompt, systemPrompt, "")), 0o600)
}

bs, err := afero.ReadFile(fs, claudeMDPath)
Expand Down Expand Up @@ -647,7 +666,7 @@ func injectClaudeMD(fs afero.Fs, systemPrompt, reportTaskPrompt, claudeMDPath st
cleanContent = strings.TrimSpace(cleanContent)

// Create the new content with coder and system prompt prepended
newContent := promptsBlock(defaultCoderPrompt, reportTaskPrompt, systemPrompt, cleanContent)
newContent := promptsBlock(coderPrompt, systemPrompt, cleanContent)

// Write the updated content back to the file
err = afero.WriteFile(fs, claudeMDPath, []byte(newContent), 0o600)
Expand All @@ -658,19 +677,11 @@ func injectClaudeMD(fs afero.Fs, systemPrompt, reportTaskPrompt, claudeMDPath st
return nil
}

func promptsBlock(coderPrompt, reportTaskPrompt, systemPrompt, existingContent string) string {
func promptsBlock(coderPrompt, systemPrompt, existingContent string) string {
var newContent strings.Builder
_, _ = newContent.WriteString(coderPromptStartGuard)
_, _ = newContent.WriteRune('\n')
_, _ = newContent.WriteString(coderPrompt)

// Only include the report task prompt if it's provided
if reportTaskPrompt != "" {
_, _ = newContent.WriteRune('\n')
_, _ = newContent.WriteRune('\n')
_, _ = newContent.WriteString(reportTaskPrompt)
}

_, _ = newContent.WriteRune('\n')
_, _ = newContent.WriteString(coderPromptEndGuard)
_, _ = newContent.WriteRune('\n')
Expand Down
46 changes: 46 additions & 0 deletions cli/exp_mcp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,52 @@ test-system-prompt
}
})

t.Run("CustomCoderPrompt", func(t *testing.T) {
t.Setenv("CODER_AGENT_TOKEN", "test-agent-token")
ctx := testutil.Context(t, testutil.WaitShort)
cancelCtx, cancel := context.WithCancel(ctx)
t.Cleanup(cancel)

client := coderdtest.New(t, nil)
_ = coderdtest.CreateFirstUser(t, client)

tmpDir := t.TempDir()
claudeConfigPath := filepath.Join(tmpDir, "claude.json")
claudeMDPath := filepath.Join(tmpDir, "CLAUDE.md")

customCoderPrompt := "This is a custom coder prompt from flag."

// This should include the custom coderPrompt and reportTaskPrompt
expectedClaudeMD := `<coder-prompt>
This is a custom coder prompt from flag.
</coder-prompt>
<system-prompt>
test-system-prompt
</system-prompt>
`

inv, root := clitest.New(t, "exp", "mcp", "configure", "claude-code", "/path/to/project",
"--claude-api-key=test-api-key",
"--claude-config-path="+claudeConfigPath,
"--claude-md-path="+claudeMDPath,
"--claude-system-prompt=test-system-prompt",
"--claude-app-status-slug=some-app-name",
"--claude-test-binary-name=pathtothecoderbinary",
"--claude-coder-prompt="+customCoderPrompt,
)
clitest.SetupConfig(t, client, root)

err := inv.WithContext(cancelCtx).Run()
require.NoError(t, err, "failed to configure claude code")

require.FileExists(t, claudeMDPath, "claude md file should exist")
claudeMD, err := os.ReadFile(claudeMDPath)
require.NoError(t, err, "failed to read claude md path")
if diff := cmp.Diff(expectedClaudeMD, string(claudeMD)); diff != "" {
t.Fatalf("claude md file content mismatch (-want +got):\n%s", diff)
}
})

t.Run("NoReportTaskWhenNoAppSlug", func(t *testing.T) {
t.Setenv("CODER_AGENT_TOKEN", "test-agent-token")
ctx := testutil.Context(t, testutil.WaitShort)
Expand Down
Loading