Skip to content

Commit 5a93aaf

Browse files
committed
mcp: add log and diff tools
Signed-off-by: Andrea Luzzardi <al@dagger.io>
1 parent 8cb3037 commit 5a93aaf

File tree

11 files changed

+167
-52
lines changed

11 files changed

+167
-52
lines changed

cmd/cu/checkout.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ import (
88
)
99

1010
var checkoutCmd = &cobra.Command{
11-
Use: "checkout <env>",
12-
Short: "Switch to an environment's branch locally",
11+
Use: "checkout <env>",
12+
Short: "Switch to an environment's branch locally",
1313
Long: `Bring an environment's work into your local git workspace.
1414
This creates a local branch from the environment's state so you can
1515
explore files in your IDE, make changes, or continue development.`,
@@ -48,4 +48,4 @@ cu checkout fancy-mallard -b my-review-branch`,
4848
func init() {
4949
checkoutCmd.Flags().StringP("branch", "b", "", "Local branch name to use")
5050
rootCmd.AddCommand(checkoutCmd)
51-
}
51+
}

cmd/cu/delete.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ import (
88
)
99

1010
var deleteCmd = &cobra.Command{
11-
Use: "delete [<env>...]",
12-
Short: "Delete environments and start fresh",
11+
Use: "delete [<env>...]",
12+
Short: "Delete environments and start fresh",
1313
Long: `Delete one or more environments and their associated resources.
1414
This permanently removes the environment's branch and container state.
1515
Use this when starting over with a different approach.
@@ -80,4 +80,4 @@ cu delete --all`,
8080
func init() {
8181
rootCmd.AddCommand(deleteCmd)
8282
deleteCmd.Flags().Bool("all", false, "Delete all environments")
83-
}
83+
}

cmd/cu/diff.go

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,21 @@ import (
88
)
99

1010
var diffCmd = &cobra.Command{
11-
Use: "diff <env>",
12-
Short: "Show what files an agent changed",
11+
Use: "diff <env>",
12+
Short: "Show what files an agent changed",
1313
Long: `Display the code changes made by an agent in an environment.
14-
Shows a git diff between the environment's state and your current branch.`,
14+
Shows a git diff of all changes made since the environment was created.
15+
Use -b to compare against a specific branch instead of showing full diff.`,
1516
Args: cobra.ExactArgs(1),
1617
ValidArgsFunction: suggestEnvironments,
17-
Example: `# See what changes the agent made
18+
Example: `# See what changes the agent made (full diff)
1819
cu diff fancy-mallard
1920
21+
# Compare against main branch
22+
cu diff fancy-mallard -b main
23+
2024
# Quick assessment before merging
21-
cu diff backend-api`,
25+
cu diff backend-api -b main`,
2226
RunE: func(app *cobra.Command, args []string) error {
2327
ctx := app.Context()
2428

@@ -28,10 +32,13 @@ cu diff backend-api`,
2832
return err
2933
}
3034

31-
return repo.Diff(ctx, args[0], os.Stdout)
35+
branch, _ := app.Flags().GetString("branch")
36+
37+
return repo.Diff(ctx, args[0], branch, os.Stdout)
3238
},
3339
}
3440

3541
func init() {
42+
diffCmd.Flags().StringP("branch", "b", "", "Compare against specified branch (uses merge-base)")
3643
rootCmd.AddCommand(diffCmd)
3744
}

cmd/cu/list.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,4 +57,4 @@ func init() {
5757
listCmd.Flags().BoolP("quiet", "q", false, "Display only environment IDs")
5858
listCmd.Flags().BoolP("no-trunc", "", false, "Don't truncate output")
5959
rootCmd.AddCommand(listCmd)
60-
}
60+
}

cmd/cu/log.go

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,25 @@ import (
88
)
99

1010
var logCmd = &cobra.Command{
11-
Use: "log <env>",
12-
Short: "View what an agent did step-by-step",
11+
Use: "log <env>",
12+
Short: "View what an agent did step-by-step",
1313
Long: `Display the complete development history for an environment.
1414
Shows all commits made by the agent plus command execution notes.
15-
Use -p to include code patches in the output.`,
15+
Use -p to include code patches in the output.
16+
Use -b to compare against a specific branch instead of showing full history.`,
1617
Args: cobra.ExactArgs(1),
1718
ValidArgsFunction: suggestEnvironments,
18-
Example: `# See what agent did
19+
Example: `# See what agent did (full history)
1920
cu log fancy-mallard
2021
2122
# Include code changes
22-
cu log fancy-mallard -p`,
23+
cu log fancy-mallard -p
24+
25+
# Compare against main branch
26+
cu log fancy-mallard -b main
27+
28+
# Compare against main with patches
29+
cu log fancy-mallard -b main -p`,
2330
RunE: func(app *cobra.Command, args []string) error {
2431
ctx := app.Context()
2532

@@ -30,12 +37,14 @@ cu log fancy-mallard -p`,
3037
}
3138

3239
patch, _ := app.Flags().GetBool("patch")
40+
branch, _ := app.Flags().GetString("branch")
3341

34-
return repo.Log(ctx, args[0], patch, os.Stdout)
42+
return repo.Log(ctx, args[0], patch, branch, os.Stdout)
3543
},
3644
}
3745

3846
func init() {
3947
logCmd.Flags().BoolP("patch", "p", false, "Generate patch")
48+
logCmd.Flags().StringP("branch", "b", "", "Compare against specified branch (uses merge-base)")
4049
rootCmd.AddCommand(logCmd)
4150
}

cmd/cu/watch.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,4 @@ cu watch`,
3535

3636
func init() {
3737
rootCmd.AddCommand(watchCmd)
38-
}
38+
}

environment/integration/repository_test.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ func TestRepositoryLog(t *testing.T) {
142142

143143
// Get commit log without patches
144144
var logBuf bytes.Buffer
145-
err := repo.Log(ctx, env.ID, false, &logBuf)
145+
err := repo.Log(ctx, env.ID, false, "", &logBuf)
146146
logOutput := logBuf.String()
147147
require.NoError(t, err, logOutput)
148148

@@ -153,7 +153,7 @@ func TestRepositoryLog(t *testing.T) {
153153

154154
// Get commit log with patches
155155
logBuf.Reset()
156-
err = repo.Log(ctx, env.ID, true, &logBuf)
156+
err = repo.Log(ctx, env.ID, true, "", &logBuf)
157157
logWithPatchOutput := logBuf.String()
158158
require.NoError(t, err, logWithPatchOutput)
159159

@@ -162,7 +162,7 @@ func TestRepositoryLog(t *testing.T) {
162162
assert.Contains(t, logWithPatchOutput, "+updated content")
163163

164164
// Test log for non-existent environment
165-
err = repo.Log(ctx, "non-existent-env", false, &logBuf)
165+
err = repo.Log(ctx, "non-existent-env", false, "", &logBuf)
166166
assert.Error(t, err)
167167
})
168168
}
@@ -184,15 +184,15 @@ func TestRepositoryDiff(t *testing.T) {
184184

185185
// Get diff output
186186
var diffBuf bytes.Buffer
187-
err := repo.Diff(ctx, env.ID, &diffBuf)
187+
err := repo.Diff(ctx, env.ID, "", &diffBuf)
188188
diffOutput := diffBuf.String()
189189
require.NoError(t, err, diffOutput)
190190

191191
// Verify diff contains expected changes
192192
assert.Contains(t, diffOutput, "+updated content")
193193

194194
// Test diff with non-existent environment
195-
err = repo.Diff(ctx, "non-existent-env", &diffBuf)
195+
err = repo.Diff(ctx, "non-existent-env", "", &diffBuf)
196196
assert.Error(t, err)
197197
})
198198
}

mcpserver/tools.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package mcpserver
22

33
import (
4+
"bytes"
45
"context"
56
_ "embed"
67
"encoding/json"
@@ -115,6 +116,8 @@ func init() {
115116
EnvironmentAddServiceTool,
116117

117118
EnvironmentCheckpointTool,
119+
EnvironmentLogTool,
120+
EnvironmentDiffTool,
118121
)
119122
}
120123

@@ -816,3 +819,80 @@ Supported schemas are:
816819
return mcp.NewToolResultText(fmt.Sprintf("Service added and started successfully: %s", string(output))), nil
817820
},
818821
}
822+
823+
var EnvironmentLogTool = &Tool{
824+
Definition: mcp.NewTool("environment_log",
825+
mcp.WithDescription("View the development history of an environment, showing all commits made by the agent plus command execution notes."),
826+
mcp.WithString("explanation",
827+
mcp.Description("One sentence explanation for why this environment log is being viewed."),
828+
),
829+
mcp.WithString("environment_source",
830+
mcp.Description("Absolute path to the source git repository for the environment."),
831+
mcp.Required(),
832+
),
833+
mcp.WithString("environment_id",
834+
mcp.Description("The ID of the environment to view the log for."),
835+
mcp.Required(),
836+
),
837+
mcp.WithBoolean("patch",
838+
mcp.Description("Include code patches in the output (default: false)."),
839+
),
840+
),
841+
Handler: func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
842+
repo, err := openRepository(ctx, request)
843+
if err != nil {
844+
return mcp.NewToolResultErrorFromErr("unable to open the repository", err), nil
845+
}
846+
847+
envID, err := request.RequireString("environment_id")
848+
if err != nil {
849+
return nil, err
850+
}
851+
852+
patch := request.GetBool("patch", false)
853+
854+
var buf bytes.Buffer
855+
// MCP tools always show full history (no branch comparison)
856+
if err := repo.Log(ctx, envID, patch, "", &buf); err != nil {
857+
return mcp.NewToolResultErrorFromErr("failed to get environment log", err), nil
858+
}
859+
860+
return mcp.NewToolResultText(buf.String()), nil
861+
},
862+
}
863+
864+
var EnvironmentDiffTool = &Tool{
865+
Definition: mcp.NewTool("environment_diff",
866+
mcp.WithDescription("View the cumulative changes made in an environment from its creation point, showing all code modifications as a unified diff."),
867+
mcp.WithString("explanation",
868+
mcp.Description("One sentence explanation for why this environment diff is being viewed."),
869+
),
870+
mcp.WithString("environment_source",
871+
mcp.Description("Absolute path to the source git repository for the environment."),
872+
mcp.Required(),
873+
),
874+
mcp.WithString("environment_id",
875+
mcp.Description("The ID of the environment to view the diff for."),
876+
mcp.Required(),
877+
),
878+
),
879+
Handler: func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
880+
repo, err := openRepository(ctx, request)
881+
if err != nil {
882+
return mcp.NewToolResultErrorFromErr("unable to open the repository", err), nil
883+
}
884+
885+
envID, err := request.RequireString("environment_id")
886+
if err != nil {
887+
return nil, err
888+
}
889+
890+
var buf bytes.Buffer
891+
// MCP tools always show full diff (no branch comparison)
892+
if err := repo.Diff(ctx, envID, "", &buf); err != nil {
893+
return mcp.NewToolResultErrorFromErr("failed to get environment diff", err), nil
894+
}
895+
896+
return mcp.NewToolResultText(buf.String()), nil
897+
},
898+
}

repository/git.go

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -239,26 +239,41 @@ func (r *Repository) currentUserBranch(ctx context.Context) (string, error) {
239239
return RunGitCommand(ctx, r.userRepoPath, "branch", "--show-current")
240240
}
241241

242-
func (r *Repository) mergeBase(ctx context.Context, env *environment.EnvironmentInfo) (string, error) {
243-
currentBranch, err := r.currentUserBranch(ctx)
244-
if err != nil {
245-
return "", err
246-
}
247-
currentBranch = strings.TrimSpace(currentBranch)
242+
func (r *Repository) mergeBase(ctx context.Context, env *environment.EnvironmentInfo, branch string) (string, error) {
248243
envGitRef := fmt.Sprintf("%s/%s", containerUseRemote, env.ID)
249-
mergeBase, err := RunGitCommand(ctx, r.userRepoPath, "merge-base", currentBranch, envGitRef)
244+
mergeBase, err := RunGitCommand(ctx, r.userRepoPath, "merge-base", branch, envGitRef)
250245
if err != nil {
251246
return "", err
252247
}
253248
return strings.TrimSpace(mergeBase), nil
254249
}
255250

256-
func (r *Repository) revisionRange(ctx context.Context, env *environment.EnvironmentInfo) (string, error) {
257-
mergeBase, err := r.mergeBase(ctx, env)
251+
// revisionRange determines the git revision range for log/diff operations.
252+
// If branch is provided, uses merge-base with that branch.
253+
// Otherwise, uses merge-base with the current user branch.
254+
func (r *Repository) revisionRange(ctx context.Context, env *environment.EnvironmentInfo, branch string) (string, error) {
255+
envGitRef := fmt.Sprintf("%s/%s", containerUseRemote, env.ID)
256+
257+
if branch != "" {
258+
// Use merge-base with specified branch
259+
mergeBase, err := r.mergeBase(ctx, env, branch)
260+
if err != nil {
261+
return "", err
262+
}
263+
return fmt.Sprintf("%s..%s", mergeBase, envGitRef), nil
264+
}
265+
266+
// Use merge-base with current user branch (original behavior)
267+
currentBranch, err := r.currentUserBranch(ctx)
268+
if err != nil {
269+
return "", err
270+
}
271+
currentBranch = strings.TrimSpace(currentBranch)
272+
273+
mergeBase, err := r.mergeBase(ctx, env, currentBranch)
258274
if err != nil {
259275
return "", err
260276
}
261-
envGitRef := fmt.Sprintf("%s/%s", containerUseRemote, env.ID)
262277
return fmt.Sprintf("%s..%s", mergeBase, envGitRef), nil
263278
}
264279

@@ -582,4 +597,4 @@ func matchesScpLike(url string) bool {
582597
func findScpLikeComponents(url string) (user, host, port, path string) {
583598
m := scpLikeURLRegExp.FindStringSubmatch(url)
584599
return m[1], m[2], m[3], m[4]
585-
}
600+
}

repository/repository.go

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -357,7 +357,10 @@ func (r *Repository) Checkout(ctx context.Context, id, branch string) (string, e
357357
return branch, err
358358
}
359359

360-
func (r *Repository) Log(ctx context.Context, id string, patch bool, w io.Writer) error {
360+
// Log displays the development history for an environment.
361+
// If branch is provided, shows log since merge-base with that branch.
362+
// Otherwise, shows log from environment creation.
363+
func (r *Repository) Log(ctx context.Context, id string, patch bool, branch string, w io.Writer) error {
361364
envInfo, err := r.Info(ctx, id)
362365
if err != nil {
363366
return err
@@ -375,7 +378,7 @@ func (r *Repository) Log(ctx context.Context, id string, patch bool, w io.Writer
375378
logArgs = append(logArgs, "--format=%C(yellow)%h%Creset %s %Cgreen(%cr)%Creset %+N")
376379
}
377380

378-
revisionRange, err := r.revisionRange(ctx, envInfo)
381+
revisionRange, err := r.revisionRange(ctx, envInfo, branch)
379382
if err != nil {
380383
return err
381384
}
@@ -391,7 +394,10 @@ func (r *Repository) Log(ctx context.Context, id string, patch bool, w io.Writer
391394
return cmd.Run()
392395
}
393396

394-
func (r *Repository) Diff(ctx context.Context, id string, w io.Writer) error {
397+
// Diff shows changes made in an environment.
398+
// If branch is provided, shows diff since merge-base with that branch.
399+
// Otherwise, shows diff from environment creation.
400+
func (r *Repository) Diff(ctx context.Context, id string, branch string, w io.Writer) error {
395401
envInfo, err := r.Info(ctx, id)
396402
if err != nil {
397403
return err
@@ -402,7 +408,7 @@ func (r *Repository) Diff(ctx context.Context, id string, w io.Writer) error {
402408
"diff",
403409
}
404410

405-
revisionRange, err := r.revisionRange(ctx, envInfo)
411+
revisionRange, err := r.revisionRange(ctx, envInfo, branch)
406412
if err != nil {
407413
return err
408414
}
@@ -416,4 +422,4 @@ func (r *Repository) Diff(ctx context.Context, id string, w io.Writer) error {
416422
cmd.Stderr = w
417423

418424
return cmd.Run()
419-
}
425+
}

0 commit comments

Comments
 (0)