From e09f3518e0a52e6efcfc3103fe825433768ca786 Mon Sep 17 00:00:00 2001 From: Virgil Date: Thu, 2 Apr 2026 12:51:27 +0000 Subject: [PATCH] feat(mcp): enrich process notifications with runtime context Co-Authored-By: Virgil --- pkg/mcp/process_notifications.go | 3 +++ pkg/mcp/register.go | 37 +++++++++++++++++++++++++------- pkg/mcp/register_test.go | 12 +++++++++++ pkg/mcp/tools_process.go | 33 +++++++++++++++++++++++++--- 4 files changed, 74 insertions(+), 11 deletions(-) diff --git a/pkg/mcp/process_notifications.go b/pkg/mcp/process_notifications.go index a56550e..88313d7 100644 --- a/pkg/mcp/process_notifications.go +++ b/pkg/mcp/process_notifications.go @@ -100,6 +100,9 @@ func (s *Service) emitTestResult(ctx context.Context, processID string, exitCode "status": status, "passed": status == "passed", } + if meta.Dir != "" { + payload["dir"] = meta.Dir + } if !meta.StartedAt.IsZero() { payload["startedAt"] = meta.StartedAt } diff --git a/pkg/mcp/register.go b/pkg/mcp/register.go index 8073742..0a97d81 100644 --- a/pkg/mcp/register.go +++ b/pkg/mcp/register.go @@ -109,18 +109,20 @@ func (s *Service) HandleIPCEvents(c *core.Core, msg core.Message) core.Result { case ChannelPush: s.ChannelSend(ctx, ev.Channel, ev.Data) case process.ActionProcessStarted: + startedAt := time.Now() s.recordProcessRuntime(ev.ID, processRuntime{ Command: ev.Command, Args: ev.Args, Dir: ev.Dir, - StartedAt: time.Now(), + StartedAt: startedAt, }) s.ChannelSend(ctx, ChannelProcessStart, map[string]any{ - "id": ev.ID, - "command": ev.Command, - "args": ev.Args, - "dir": ev.Dir, - "pid": ev.PID, + "id": ev.ID, + "command": ev.Command, + "args": ev.Args, + "dir": ev.Dir, + "pid": ev.PID, + "startedAt": startedAt, }) case process.ActionProcessOutput: s.ChannelSend(ctx, ChannelProcessOutput, map[string]any{ @@ -129,11 +131,20 @@ func (s *Service) HandleIPCEvents(c *core.Core, msg core.Message) core.Result { "stream": ev.Stream, }) case process.ActionProcessExited: + meta, ok := s.processRuntimeFor(ev.ID) payload := map[string]any{ "id": ev.ID, "exitCode": ev.ExitCode, "duration": ev.Duration, } + if ok { + payload["command"] = meta.Command + payload["args"] = meta.Args + payload["dir"] = meta.Dir + if !meta.StartedAt.IsZero() { + payload["startedAt"] = meta.StartedAt + } + } if ev.Error != nil { payload["error"] = ev.Error.Error() } @@ -144,10 +155,20 @@ func (s *Service) HandleIPCEvents(c *core.Core, msg core.Message) core.Result { } s.emitTestResult(ctx, ev.ID, ev.ExitCode, ev.Duration, "", errText) case process.ActionProcessKilled: - s.ChannelSend(ctx, ChannelProcessExit, map[string]any{ + meta, ok := s.processRuntimeFor(ev.ID) + payload := map[string]any{ "id": ev.ID, "signal": ev.Signal, - }) + } + if ok { + payload["command"] = meta.Command + payload["args"] = meta.Args + payload["dir"] = meta.Dir + if !meta.StartedAt.IsZero() { + payload["startedAt"] = meta.StartedAt + } + } + s.ChannelSend(ctx, ChannelProcessExit, payload) s.emitTestResult(ctx, ev.ID, 0, 0, ev.Signal, "") } return core.Result{OK: true} diff --git a/pkg/mcp/register_test.go b/pkg/mcp/register_test.go index b25d689..b130193 100644 --- a/pkg/mcp/register_test.go +++ b/pkg/mcp/register_test.go @@ -135,6 +135,15 @@ func TestHandleIPCEvents_Good_ForwardsProcessActions(t *testing.T) { if payload["id"] != "proc-1" || payload["command"] != "go" { t.Fatalf("unexpected payload: %#v", payload) } + if payload["dir"] != "/workspace" { + t.Fatalf("expected dir /workspace, got %#v", payload["dir"]) + } + if payload["pid"] != float64(1234) { + t.Fatalf("expected pid 1234, got %#v", payload["pid"]) + } + if payload["args"] == nil { + t.Fatalf("expected args in payload, got %#v", payload) + } return case <-deadline.C: t.Fatal("timed out waiting for process start notification") @@ -311,6 +320,9 @@ func TestHandleIPCEvents_Good_ForwardsTestResult(t *testing.T) { if payload["id"] != "proc-test" || payload["command"] != "go" { t.Fatalf("unexpected payload: %#v", payload) } + if payload["dir"] != nil { + t.Fatalf("expected dir to be absent when not recorded, got %#v", payload["dir"]) + } if payload["status"] != "passed" || payload["passed"] != true { t.Fatalf("expected passed test result, got %#v", payload) } diff --git a/pkg/mcp/tools_process.go b/pkg/mcp/tools_process.go index 8b0694a..8c8535f 100644 --- a/pkg/mcp/tools_process.go +++ b/pkg/mcp/tools_process.go @@ -208,7 +208,12 @@ func (s *Service) processStart(ctx context.Context, req *mcp.CallToolRequest, in StartedAt: output.StartedAt, }) s.ChannelSend(ctx, ChannelProcessStart, map[string]any{ - "id": output.ID, "pid": output.PID, "command": output.Command, + "id": output.ID, + "pid": output.PID, + "command": output.Command, + "args": output.Args, + "dir": info.Dir, + "startedAt": output.StartedAt, }) return nil, output, nil } @@ -234,7 +239,15 @@ func (s *Service) processStop(ctx context.Context, req *mcp.CallToolRequest, inp return nil, ProcessStopOutput{}, log.E("processStop", "failed to stop process", err) } - s.ChannelSend(ctx, ChannelProcessExit, map[string]any{"id": input.ID, "signal": "stop"}) + info := proc.Info() + s.ChannelSend(ctx, ChannelProcessExit, map[string]any{ + "id": input.ID, + "signal": "stop", + "command": info.Command, + "args": info.Args, + "dir": info.Dir, + "startedAt": info.StartedAt, + }) s.emitTestResult(ctx, input.ID, 0, 0, "stop", "") return nil, ProcessStopOutput{ ID: input.ID, @@ -251,12 +264,26 @@ func (s *Service) processKill(ctx context.Context, req *mcp.CallToolRequest, inp return nil, ProcessKillOutput{}, errIDEmpty } + proc, err := s.processService.Get(input.ID) + if err != nil { + log.Error("mcp: process kill failed", "id", input.ID, "err", err) + return nil, ProcessKillOutput{}, log.E("processKill", "process not found", err) + } + if err := s.processService.Kill(input.ID); err != nil { log.Error("mcp: process kill failed", "id", input.ID, "err", err) return nil, ProcessKillOutput{}, log.E("processKill", "failed to kill process", err) } - s.ChannelSend(ctx, ChannelProcessExit, map[string]any{"id": input.ID, "signal": "kill"}) + info := proc.Info() + s.ChannelSend(ctx, ChannelProcessExit, map[string]any{ + "id": input.ID, + "signal": "kill", + "command": info.Command, + "args": info.Args, + "dir": info.Dir, + "startedAt": info.StartedAt, + }) s.emitTestResult(ctx, input.ID, 0, 0, "kill", "") return nil, ProcessKillOutput{ ID: input.ID,