Skip to content

Commit 8166144

Browse files
committed
Add MCP
1 parent 95a8350 commit 8166144

File tree

7 files changed

+313
-94
lines changed

7 files changed

+313
-94
lines changed

agent/agentclaude/agentclaude.go

Lines changed: 75 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package agentclaude
33
import (
44
"context"
55
"encoding/json"
6-
"fmt"
76
"io"
87
"os"
98
"os/exec"
@@ -13,34 +12,42 @@ import (
1312
"time"
1413

1514
"github.com/spf13/afero"
15+
"golang.org/x/xerrors"
1616
)
1717

18-
func New(ctx context.Context, apiKey, systemPrompt, taskPrompt string, onPause func()) error {
18+
func New(ctx context.Context, apiKey, systemPrompt, taskPrompt string, onWaiting func(waiting bool)) error {
1919
claudePath, err := exec.LookPath("claude")
2020
if err != nil {
21-
return fmt.Errorf("claude not found: %w", err)
21+
return xerrors.Errorf("claude not found: %w", err)
2222
}
2323
fs := afero.NewOsFs()
24-
err = injectClaudeMD(fs, `You are an AI agent in a Coder Workspace.
24+
err = injectClaudeMD(fs, `YOU MUST REPORT YOUR STATUS IMMEDIATELY AFTER EACH USER MESSAGE.
2525
26-
The user is running this task entirely autonomously.
26+
INTERRUPT READING FILES OR ANY OTHER TOOL CALL IF YOU HAVE NOT REPORTED A STATUS YET.
2727
28-
Use the coder-agent MCP server to report your progress. You must report when you:
29-
- Start a new task.
30-
- Complete a task (e.g. push a commit, open a PR, etc.)
31-
- Make progress on a task.
28+
You MUST use the mcp__coder-agent__report_status function with all required parameters:
29+
- summary: Short description of what you're doing
30+
- link: A relevant link for the status
31+
- done: Boolean indicating if the task is complete (true/false)
32+
- emoji: Relevant emoji for the status
3233
33-
You should report your progress frequently. Try to report as much as possible.
34+
WHEN TO REPORT (MANDATORY):
35+
1. IMMEDIATELY after receiving ANY user message, before any other actions
36+
2. After completing any task
37+
3. When making significant progress
38+
4. When encountering roadblocks
39+
5. When asking questions
40+
6. Before and after using search tools or making code changes
3441
35-
If you do not, the user will not be able to see your progress.
42+
FAILING TO REPORT STATUS PROPERLY WILL RESULT IN INCORRECT BEHAVIOR.
3643
`, systemPrompt, "")
3744
if err != nil {
38-
return fmt.Errorf("failed to inject claude md: %w", err)
45+
return xerrors.Errorf("failed to inject claude md: %w", err)
3946
}
4047

4148
wd, err := os.Getwd()
4249
if err != nil {
43-
return fmt.Errorf("failed to get working directory: %w", err)
50+
return xerrors.Errorf("failed to get working directory: %w", err)
4451
}
4552

4653
err = configureClaude(fs, ClaudeConfig{
@@ -59,20 +66,22 @@ If you do not, the user will not be able to see your progress.
5966
},
6067
})
6168
if err != nil {
62-
return fmt.Errorf("failed to configure claude: %w", err)
69+
return xerrors.Errorf("failed to configure claude: %w", err)
6370
}
6471

6572
cmd := exec.CommandContext(ctx, claudePath, "--dangerously-skip-permissions", taskPrompt)
6673
// Create a simple wrapper that starts monitoring only after first write
6774
stdoutWriter := &delayedPauseWriter{
6875
writer: os.Stdout,
6976
pauseWindow: 2 * time.Second,
70-
onPause: onPause,
77+
onWaiting: onWaiting,
78+
cooldown: 15 * time.Second,
7179
}
7280
stderrWriter := &delayedPauseWriter{
7381
writer: os.Stderr,
7482
pauseWindow: 2 * time.Second,
75-
onPause: onPause,
83+
onWaiting: onWaiting,
84+
cooldown: 15 * time.Second,
7685
}
7786

7887
cmd.Stdout = stdoutWriter
@@ -86,11 +95,13 @@ If you do not, the user will not be able to see your progress.
8695
type delayedPauseWriter struct {
8796
writer io.Writer
8897
pauseWindow time.Duration
89-
onPause func()
98+
cooldown time.Duration
99+
onWaiting func(waiting bool)
90100
lastWrite time.Time
91101
mu sync.Mutex
92102
started bool
93-
pauseNotified bool
103+
waitingState bool
104+
cooldownUntil time.Time
94105
}
95106

96107
// Write implements io.Writer and starts monitoring on first write
@@ -100,8 +111,12 @@ func (w *delayedPauseWriter) Write(p []byte) (n int, err error) {
100111
w.started = true
101112
w.lastWrite = time.Now()
102113

103-
// Reset pause notification state when new output appears
104-
w.pauseNotified = false
114+
// If we were in waiting state, we're now resumed
115+
if w.waitingState {
116+
w.waitingState = false
117+
w.cooldownUntil = time.Now().Add(w.cooldown)
118+
w.onWaiting(false) // Signal resume
119+
}
105120

106121
w.mu.Unlock()
107122

@@ -113,26 +128,32 @@ func (w *delayedPauseWriter) Write(p []byte) (n int, err error) {
113128
return w.writer.Write(p)
114129
}
115130

116-
// monitorPauses checks for pauses in writing and calls onPause when detected
131+
// monitorPauses checks for pauses in writing and calls onWaiting when detected
117132
func (w *delayedPauseWriter) monitorPauses() {
118133
ticker := time.NewTicker(500 * time.Millisecond)
119134
defer ticker.Stop()
120135

121136
for range ticker.C {
122137
w.mu.Lock()
123-
elapsed := time.Since(w.lastWrite)
124-
alreadyNotified := w.pauseNotified
125138

126-
// If we detect a pause and haven't notified yet, mark as notified
127-
if elapsed >= w.pauseWindow && !alreadyNotified {
128-
w.pauseNotified = true
139+
// Check if we're in a cooldown period
140+
inCooldown := time.Now().Before(w.cooldownUntil)
141+
elapsed := time.Since(w.lastWrite)
142+
shouldWait := elapsed >= w.pauseWindow && !inCooldown
143+
currentState := w.waitingState
144+
shouldNotify := false
145+
146+
// Only update state if it changed
147+
if shouldWait != currentState {
148+
w.waitingState = shouldWait
149+
shouldNotify = true
129150
}
130151

131152
w.mu.Unlock()
132153

133-
// Only notify once per pause period
134-
if elapsed >= w.pauseWindow && !alreadyNotified {
135-
w.onPause()
154+
// Notify outside of the lock to avoid deadlocks
155+
if shouldNotify {
156+
w.onWaiting(shouldWait)
136157
}
137158
}
138159
}
@@ -144,14 +165,14 @@ func injectClaudeMD(fs afero.Fs, coderPrompt, systemPrompt string, configPath st
144165
_, err := fs.Stat(configPath)
145166
if err != nil {
146167
if !os.IsNotExist(err) {
147-
return fmt.Errorf("failed to stat claude config: %w", err)
168+
return xerrors.Errorf("failed to stat claude config: %w", err)
148169
}
149170
}
150171
content := ""
151172
if err == nil {
152173
contentBytes, err := afero.ReadFile(fs, configPath)
153174
if err != nil {
154-
return fmt.Errorf("failed to read claude config: %w", err)
175+
return xerrors.Errorf("failed to read claude config: %w", err)
155176
}
156177
content = string(contentBytes)
157178
}
@@ -202,13 +223,13 @@ func injectClaudeMD(fs afero.Fs, coderPrompt, systemPrompt string, configPath st
202223

203224
err = fs.MkdirAll(filepath.Dir(configPath), 0755)
204225
if err != nil {
205-
return fmt.Errorf("failed to create claude config directory: %w", err)
226+
return xerrors.Errorf("failed to create claude config directory: %w", err)
206227
}
207228

208229
// Write the updated content back to the file
209230
err = afero.WriteFile(fs, configPath, []byte(newContent), 0644)
210231
if err != nil {
211-
return fmt.Errorf("failed to write claude config: %w", err)
232+
return xerrors.Errorf("failed to write claude config: %w", err)
212233
}
213234

214235
return nil
@@ -249,17 +270,17 @@ func configureClaude(fs afero.Fs, cfg ClaudeConfig) error {
249270
if os.IsNotExist(err) {
250271
config = make(map[string]any)
251272
} else {
252-
return fmt.Errorf("failed to stat claude config: %w", err)
273+
return xerrors.Errorf("failed to stat claude config: %w", err)
253274
}
254275
}
255276
if err == nil {
256277
jsonBytes, err := afero.ReadFile(fs, cfg.ConfigPath)
257278
if err != nil {
258-
return fmt.Errorf("failed to read claude config: %w", err)
279+
return xerrors.Errorf("failed to read claude config: %w", err)
259280
}
260281
err = json.Unmarshal(jsonBytes, &config)
261282
if err != nil {
262-
return fmt.Errorf("failed to unmarshal claude config: %w", err)
283+
return xerrors.Errorf("failed to unmarshal claude config: %w", err)
263284
}
264285
}
265286

@@ -312,16 +333,32 @@ func configureClaude(fs afero.Fs, cfg ClaudeConfig) error {
312333
project["mcpServers"] = mcpServers
313334
// Prevents Claude from asking the user to complete the project onboarding.
314335
project["hasCompletedProjectOnboarding"] = true
336+
337+
history, ok := project["history"].([]string)
338+
injectedHistoryLine := "make sure to read claude.md and report tasks properly"
339+
340+
if !ok || len(history) == 0 {
341+
// History doesn't exist or is empty, create it with our injected line
342+
history = []string{injectedHistoryLine}
343+
} else {
344+
// Check if our line is already the first item
345+
if history[0] != injectedHistoryLine {
346+
// Prepend our line to the existing history
347+
history = append([]string{injectedHistoryLine}, history...)
348+
}
349+
}
350+
project["history"] = history
351+
315352
projects[cfg.ProjectDirectory] = project
316353
config["projects"] = projects
317354

318355
jsonBytes, err := json.MarshalIndent(config, "", " ")
319356
if err != nil {
320-
return fmt.Errorf("failed to marshal claude config: %w", err)
357+
return xerrors.Errorf("failed to marshal claude config: %w", err)
321358
}
322359
err = afero.WriteFile(fs, cfg.ConfigPath, jsonBytes, 0644)
323360
if err != nil {
324-
return fmt.Errorf("failed to write claude config: %w", err)
361+
return xerrors.Errorf("failed to write claude config: %w", err)
325362
}
326363
return nil
327364
}

agent/agentmcp/agentmcp.go

Lines changed: 28 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,35 +2,41 @@ package agentmcp
22

33
import (
44
"context"
5-
"errors"
65

7-
"github.com/coder/coder/v2/buildinfo"
8-
"github.com/coder/coder/v2/codersdk/agentsdk"
96
"github.com/mark3labs/mcp-go/mcp"
107
"github.com/mark3labs/mcp-go/server"
8+
"golang.org/x/xerrors"
9+
10+
"github.com/coder/coder/v2/buildinfo"
11+
"github.com/coder/coder/v2/codersdk/agentsdk"
1112
)
1213

1314
func New(ctx context.Context, sdk *agentsdk.Client) error {
1415
srv := server.NewMCPServer(
15-
"Coder Agent",
16+
"coder-agent",
1617
buildinfo.Version(),
17-
server.WithInstructions(`Report your progress when starting, working on, or completing a task.
18-
19-
You MUST report tasks when starting something new, or when you've completed a task.
20-
21-
You MUST report intermediate progress on a task if you've been working on it for a while.
22-
23-
Examples of sending a task:
24-
- Working on a new part of the codebase.
25-
- Starting on an issue (you should include the issue URL as "link").
26-
- Opening a pull request (you should include the PR URL as "link").
27-
- Completing a task (you should set "done" to true).
28-
- Starting a new task (you should set "done" to false).
18+
server.WithInstructions(`Report your status when starting, working on, or completing a task.
19+
20+
You MUST use the mcp__coder-agent__report_status function with all required parameters:
21+
- summary: Short description of what you're doing
22+
- link: Always use "https://github.com/anthropics/claude-code/issues"
23+
- done: Boolean indicating if task is complete (true/false)
24+
- emoji: Relevant emoji for the task
25+
26+
WHEN TO REPORT (MANDATORY):
27+
1. IMMEDIATELY after receiving ANY user message, before any other actions
28+
2. After completing any task
29+
3. When making significant progress
30+
4. When encountering roadblocks
31+
5. When asking questions
32+
6. Before and after using search tools or making code changes
33+
34+
FAILING TO REPORT TASKS PROPERLY WILL RESULT IN INCORRECT BEHAVIOR.
2935
`),
3036
)
3137

32-
tool := mcp.NewTool("report_task",
33-
mcp.WithDescription(`Report progress on a task.`),
38+
tool := mcp.NewTool("report_status",
39+
mcp.WithDescription(`Report your status or progress on a task.`),
3440
mcp.WithString("summary", mcp.Description(`A summary of your progress on a task.
3541
3642
Good Summaries:
@@ -47,22 +53,22 @@ Good Summaries:
4753

4854
summary, ok := args["summary"].(string)
4955
if !ok {
50-
return nil, errors.New("summary is required")
56+
return nil, xerrors.New("summary is required")
5157
}
5258

5359
link, ok := args["link"].(string)
5460
if !ok {
55-
return nil, errors.New("link is required")
61+
return nil, xerrors.New("link is required")
5662
}
5763

5864
emoji, ok := args["emoji"].(string)
5965
if !ok {
60-
return nil, errors.New("emoji is required")
66+
return nil, xerrors.New("emoji is required")
6167
}
6268

6369
done, ok := args["done"].(bool)
6470
if !ok {
65-
return nil, errors.New("done is required")
71+
return nil, xerrors.New("done is required")
6672
}
6773

6874
err := sdk.PostTask(ctx, agentsdk.PostTaskRequest{

cli/agent.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -441,9 +441,9 @@ func (r *RootCmd) workspaceAgent() *serpent.Command {
441441
Completion: false,
442442
})
443443

444-
return agentclaude.New(inv.Context(), claudeAPIKey, claudeSystemPrompt, claudeTaskPrompt, func() {
444+
return agentclaude.New(inv.Context(), claudeAPIKey, claudeSystemPrompt, claudeTaskPrompt, func(waiting bool) {
445445
_ = client.PatchTasks(inv.Context(), agentsdk.PatchTasksRequest{
446-
WaitingForUserInput: true,
446+
WaitingForUserInput: waiting,
447447
})
448448
})
449449
},

coderd/httpmw/csp.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,6 @@ func CSPHeaders(telemetry bool, websocketHosts func() []string, staticAdditions
8888
CSPDirectiveMediaSrc: {"'self'"},
8989
// Report all violations back to the server to log
9090
CSPDirectiveReportURI: {"/api/v2/csp/reports"},
91-
CSPFrameAncestors: {"'none'"},
9291

9392
// Only scripts can manipulate the dom. This prevents someone from
9493
// naming themselves something like '<svg onload="alert(/cross-site-scripting/)" />'.

0 commit comments

Comments
 (0)