Thanks to visit codestin.com
Credit goes to github.com

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

+57-15
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

+46-89
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)