Skip to content

Commit 1e11e82

Browse files
authored
fix(mcp): report task status correctly (#17187)
1 parent 3a243c1 commit 1e11e82

File tree

3 files changed

+144
-113
lines changed

3 files changed

+144
-113
lines changed

cli/exp_mcp.go

Lines changed: 57 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,18 @@ import (
44
"context"
55
"encoding/json"
66
"errors"
7-
"log"
87
"os"
98
"path/filepath"
109

10+
"github.com/mark3labs/mcp-go/server"
11+
"golang.org/x/xerrors"
12+
1113
"cdr.dev/slog"
1214
"cdr.dev/slog/sloggers/sloghuman"
15+
"github.com/coder/coder/v2/buildinfo"
1316
"github.com/coder/coder/v2/cli/cliui"
1417
"github.com/coder/coder/v2/codersdk"
18+
"github.com/coder/coder/v2/codersdk/agentsdk"
1519
codermcp "github.com/coder/coder/v2/mcp"
1620
"github.com/coder/serpent"
1721
)
@@ -191,14 +195,16 @@ func (*RootCmd) mcpConfigureCursor() *serpent.Command {
191195

192196
func (r *RootCmd) mcpServer() *serpent.Command {
193197
var (
194-
client = new(codersdk.Client)
195-
instructions string
196-
allowedTools []string
198+
client = new(codersdk.Client)
199+
instructions string
200+
allowedTools []string
201+
appStatusSlug string
202+
mcpServerAgent bool
197203
)
198204
return &serpent.Command{
199205
Use: "server",
200206
Handler: func(inv *serpent.Invocation) error {
201-
return mcpServerHandler(inv, client, instructions, allowedTools)
207+
return mcpServerHandler(inv, client, instructions, allowedTools, appStatusSlug, mcpServerAgent)
202208
},
203209
Short: "Start the Coder MCP server.",
204210
Middleware: serpent.Chain(
@@ -209,24 +215,39 @@ func (r *RootCmd) mcpServer() *serpent.Command {
209215
Name: "instructions",
210216
Description: "The instructions to pass to the MCP server.",
211217
Flag: "instructions",
218+
Env: "CODER_MCP_INSTRUCTIONS",
212219
Value: serpent.StringOf(&instructions),
213220
},
214221
{
215222
Name: "allowed-tools",
216223
Description: "Comma-separated list of allowed tools. If not specified, all tools are allowed.",
217224
Flag: "allowed-tools",
225+
Env: "CODER_MCP_ALLOWED_TOOLS",
218226
Value: serpent.StringArrayOf(&allowedTools),
219227
},
228+
{
229+
Name: "app-status-slug",
230+
Description: "When reporting a task, the coder_app slug under which to report the task.",
231+
Flag: "app-status-slug",
232+
Env: "CODER_MCP_APP_STATUS_SLUG",
233+
Value: serpent.StringOf(&appStatusSlug),
234+
Default: "",
235+
},
236+
{
237+
Flag: "agent",
238+
Env: "CODER_MCP_SERVER_AGENT",
239+
Description: "Start the MCP server in agent mode, with a different set of tools.",
240+
Value: serpent.BoolOf(&mcpServerAgent),
241+
},
220242
},
221243
}
222244
}
223245

224-
func mcpServerHandler(inv *serpent.Invocation, client *codersdk.Client, instructions string, allowedTools []string) error {
246+
//nolint:revive // control coupling
247+
func mcpServerHandler(inv *serpent.Invocation, client *codersdk.Client, instructions string, allowedTools []string, appStatusSlug string, mcpServerAgent bool) error {
225248
ctx, cancel := context.WithCancel(inv.Context())
226249
defer cancel()
227250

228-
logger := slog.Make(sloghuman.Sink(inv.Stdout))
229-
230251
me, err := client.User(ctx, codersdk.Me)
231252
if err != nil {
232253
cliui.Errorf(inv.Stderr, "Failed to log in to the Coder deployment.")
@@ -253,19 +274,40 @@ func mcpServerHandler(inv *serpent.Invocation, client *codersdk.Client, instruct
253274
inv.Stderr = invStderr
254275
}()
255276

256-
options := []codermcp.Option{
257-
codermcp.WithInstructions(instructions),
258-
codermcp.WithLogger(&logger),
277+
mcpSrv := server.NewMCPServer(
278+
"Coder Agent",
279+
buildinfo.Version(),
280+
server.WithInstructions(instructions),
281+
)
282+
283+
// Create a separate logger for the tools.
284+
toolLogger := slog.Make(sloghuman.Sink(invStderr))
285+
286+
toolDeps := codermcp.ToolDeps{
287+
Client: client,
288+
Logger: &toolLogger,
289+
AppStatusSlug: appStatusSlug,
290+
AgentClient: agentsdk.New(client.URL),
291+
}
292+
293+
if mcpServerAgent {
294+
// Get the workspace agent token from the environment.
295+
agentToken, ok := os.LookupEnv("CODER_AGENT_TOKEN")
296+
if !ok || agentToken == "" {
297+
return xerrors.New("CODER_AGENT_TOKEN is not set")
298+
}
299+
toolDeps.AgentClient.SetSessionToken(agentToken)
259300
}
260301

261-
// Add allowed tools option if specified
302+
// Register tools based on the allowlist (if specified)
303+
reg := codermcp.AllTools()
262304
if len(allowedTools) > 0 {
263-
options = append(options, codermcp.WithAllowedTools(allowedTools))
305+
reg = reg.WithOnlyAllowed(allowedTools...)
264306
}
265307

266-
srv := codermcp.NewStdio(client, options...)
267-
srv.SetErrorLogger(log.New(invStderr, "", log.LstdFlags))
308+
reg.Register(mcpSrv, toolDeps)
268309

310+
srv := server.NewStdioServer(mcpSrv)
269311
done := make(chan error)
270312
go func() {
271313
defer close(done)

mcp/mcp.go

Lines changed: 46 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import (
66
"encoding/json"
77
"errors"
88
"io"
9-
"os"
109
"slices"
1110
"strings"
1211
"time"
@@ -17,76 +16,12 @@ import (
1716
"golang.org/x/xerrors"
1817

1918
"cdr.dev/slog"
20-
"cdr.dev/slog/sloggers/sloghuman"
21-
"github.com/coder/coder/v2/buildinfo"
2219
"github.com/coder/coder/v2/coderd/util/ptr"
2320
"github.com/coder/coder/v2/codersdk"
21+
"github.com/coder/coder/v2/codersdk/agentsdk"
2422
"github.com/coder/coder/v2/codersdk/workspacesdk"
2523
)
2624

27-
type mcpOptions struct {
28-
instructions string
29-
logger *slog.Logger
30-
allowedTools []string
31-
}
32-
33-
// Option is a function that configures the MCP server.
34-
type Option func(*mcpOptions)
35-
36-
// WithInstructions sets the instructions for the MCP server.
37-
func WithInstructions(instructions string) Option {
38-
return func(o *mcpOptions) {
39-
o.instructions = instructions
40-
}
41-
}
42-
43-
// WithLogger sets the logger for the MCP server.
44-
func WithLogger(logger *slog.Logger) Option {
45-
return func(o *mcpOptions) {
46-
o.logger = logger
47-
}
48-
}
49-
50-
// WithAllowedTools sets the allowed tools for the MCP server.
51-
func WithAllowedTools(tools []string) Option {
52-
return func(o *mcpOptions) {
53-
o.allowedTools = tools
54-
}
55-
}
56-
57-
// NewStdio creates a new MCP stdio server with the given client and options.
58-
// It is the responsibility of the caller to start and stop the server.
59-
func NewStdio(client *codersdk.Client, opts ...Option) *server.StdioServer {
60-
options := &mcpOptions{
61-
instructions: ``,
62-
logger: ptr.Ref(slog.Make(sloghuman.Sink(os.Stdout))),
63-
}
64-
for _, opt := range opts {
65-
opt(options)
66-
}
67-
68-
mcpSrv := server.NewMCPServer(
69-
"Coder Agent",
70-
buildinfo.Version(),
71-
server.WithInstructions(options.instructions),
72-
)
73-
74-
logger := slog.Make(sloghuman.Sink(os.Stdout))
75-
76-
// Register tools based on the allowed list (if specified)
77-
reg := AllTools()
78-
if len(options.allowedTools) > 0 {
79-
reg = reg.WithOnlyAllowed(options.allowedTools...)
80-
}
81-
reg.Register(mcpSrv, ToolDeps{
82-
Client: client,
83-
Logger: &logger,
84-
})
85-
86-
srv := server.NewStdioServer(mcpSrv)
87-
return srv
88-
}
89-
9025
// allTools is the list of all available tools. When adding a new tool,
9126
// make sure to update this list.
9227
var allTools = ToolRegistry{
@@ -120,6 +55,8 @@ Choose an emoji that helps the user understand the current phase at a glance.`),
12055
mcp.WithBoolean("done", mcp.Description(`Whether the overall task the user requested is complete.
12156
Set to true only when the entire requested operation is finished successfully.
12257
For multi-step processes, use false until all steps are complete.`), mcp.Required()),
58+
mcp.WithBoolean("need_user_attention", mcp.Description(`Whether the user needs to take action on the task.
59+
Set to true if the task is in a failed state or if the user needs to take action to continue.`), mcp.Required()),
12360
),
12461
MakeHandler: handleCoderReportTask,
12562
},
@@ -265,8 +202,10 @@ Can be either "start" or "stop".`)),
265202

266203
// ToolDeps contains all dependencies needed by tool handlers
267204
type ToolDeps struct {
268-
Client *codersdk.Client
269-
Logger *slog.Logger
205+
Client *codersdk.Client
206+
AgentClient *agentsdk.Client
207+
Logger *slog.Logger
208+
AppStatusSlug string
270209
}
271210

272211
// ToolHandler associates a tool with its handler creation function
@@ -313,18 +252,23 @@ func AllTools() ToolRegistry {
313252
}
314253

315254
type handleCoderReportTaskArgs struct {
316-
Summary string `json:"summary"`
317-
Link string `json:"link"`
318-
Emoji string `json:"emoji"`
319-
Done bool `json:"done"`
255+
Summary string `json:"summary"`
256+
Link string `json:"link"`
257+
Emoji string `json:"emoji"`
258+
Done bool `json:"done"`
259+
NeedUserAttention bool `json:"need_user_attention"`
320260
}
321261

322262
// Example payload:
323-
// {"jsonrpc":"2.0","id":1,"method":"tools/call", "params": {"name": "coder_report_task", "arguments": {"summary": "I'm working on the login page.", "link": "https://github.com/coder/coder/pull/1234", "emoji": "🔍", "done": false}}}
263+
// {"jsonrpc":"2.0","id":1,"method":"tools/call", "params": {"name": "coder_report_task", "arguments": {"summary": "I need help with the login page.", "link": "https://github.com/coder/coder/pull/1234", "emoji": "🔍", "done": false, "need_user_attention": true}}}
324264
func handleCoderReportTask(deps ToolDeps) server.ToolHandlerFunc {
325265
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
326-
if deps.Client == nil {
327-
return nil, xerrors.New("developer error: client is required")
266+
if deps.AgentClient == nil {
267+
return nil, xerrors.New("developer error: agent client is required")
268+
}
269+
270+
if deps.AppStatusSlug == "" {
271+
return nil, xerrors.New("No app status slug provided, set CODER_MCP_APP_STATUS_SLUG when running the MCP server to report tasks.")
328272
}
329273

330274
// Convert the request parameters to a json.RawMessage so we can unmarshal
@@ -334,20 +278,33 @@ func handleCoderReportTask(deps ToolDeps) server.ToolHandlerFunc {
334278
return nil, xerrors.Errorf("failed to unmarshal arguments: %w", err)
335279
}
336280

337-
// TODO: Waiting on support for tasks.
338-
deps.Logger.Info(ctx, "report task tool called", slog.F("summary", args.Summary), slog.F("link", args.Link), slog.F("done", args.Done), slog.F("emoji", args.Emoji))
339-
/*
340-
err := sdk.PostTask(ctx, agentsdk.PostTaskRequest{
341-
Reporter: "claude",
342-
Summary: summary,
343-
URL: link,
344-
Completion: done,
345-
Icon: emoji,
346-
})
347-
if err != nil {
348-
return nil, err
349-
}
350-
*/
281+
deps.Logger.Info(ctx, "report task tool called",
282+
slog.F("summary", args.Summary),
283+
slog.F("link", args.Link),
284+
slog.F("emoji", args.Emoji),
285+
slog.F("done", args.Done),
286+
slog.F("need_user_attention", args.NeedUserAttention),
287+
)
288+
289+
newStatus := agentsdk.PatchAppStatus{
290+
AppSlug: deps.AppStatusSlug,
291+
Message: args.Summary,
292+
URI: args.Link,
293+
Icon: args.Emoji,
294+
NeedsUserAttention: args.NeedUserAttention,
295+
State: codersdk.WorkspaceAppStatusStateWorking,
296+
}
297+
298+
if args.Done {
299+
newStatus.State = codersdk.WorkspaceAppStatusStateComplete
300+
}
301+
if args.NeedUserAttention {
302+
newStatus.State = codersdk.WorkspaceAppStatusStateFailure
303+
}
304+
305+
if err := deps.AgentClient.PatchAppStatus(ctx, newStatus); err != nil {
306+
return nil, xerrors.Errorf("failed to patch app status: %w", err)
307+
}
351308

352309
return &mcp.CallToolResult{
353310
Content: []mcp.Content{

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy