package mcp import ( "context" "github.com/google/uuid" mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/yan1h/agent-coding-workflow/internal/infra/errs" "github.com/yan1h/agent-coding-workflow/internal/infra/git" "github.com/yan1h/agent-coding-workflow/internal/project" "github.com/yan1h/agent-coding-workflow/internal/workspace" ) // registerWorkspaceTools 注册 workspace / worktree / git 操作工具。 // // 范围边界(spec 决策):git 凭证(UpsertCredential / GetCredentialMeta)不经 // MCP 暴露——秘密读写仅走 Web 后台。 // // 项目 scope 校验策略:workspace 级工具先 Get workspace 再按 ProjectID 做 // CheckProjectFromCtx;worktree 级工具要求同时给 workspace_id,并通过 // Worktrees.List 校验 worktree 归属,防止用 scope 外 worktree 绕过限定。 func registerWorkspaceTools(srv *mcpsdk.Server, deps ServerDeps) { registerGetWorkspace(srv, deps) registerCreateWorkspace(srv, deps) registerUpdateWorkspace(srv, deps) registerDeleteWorkspace(srv, deps) registerSyncWorkspace(srv, deps) registerListBranches(srv, deps) registerListWorktrees(srv, deps) registerCreateWorktree(srv, deps) registerDeleteWorktree(srv, deps) registerAcquireWorktree(srv, deps) registerReleaseWorktree(srv, deps) registerGitStatus(srv, deps) registerGitCommit(srv, deps) registerGitPush(srv, deps) registerGitDiff(srv, deps) } // ===== 共用 DTO / helper ===== type workspaceDetail struct { ID string `json:"id"` ProjectID string `json:"project_id"` Slug string `json:"slug"` Name string `json:"name"` Description string `json:"description,omitempty"` GitRemoteURL string `json:"git_remote_url"` DefaultBranch string `json:"default_branch"` SyncStatus string `json:"sync_status"` LastSyncedAt string `json:"last_synced_at,omitempty"` LastSyncError string `json:"last_sync_error,omitempty"` CreatedAt string `json:"created_at"` } func workspaceDetailFrom(w *workspace.Workspace) workspaceDetail { d := workspaceDetail{ ID: w.ID.String(), ProjectID: w.ProjectID.String(), Slug: w.Slug, Name: w.Name, Description: w.Description, GitRemoteURL: w.GitRemoteURL, DefaultBranch: w.DefaultBranch, SyncStatus: string(w.SyncStatus), LastSyncError: w.LastSyncError, CreatedAt: w.CreatedAt.Format("2006-01-02T15:04:05Z07:00"), } if w.LastSyncedAt != nil { d.LastSyncedAt = w.LastSyncedAt.Format("2006-01-02T15:04:05Z07:00") } return d } type worktreeDetail struct { ID string `json:"id"` WorkspaceID string `json:"workspace_id"` Branch string `json:"branch"` Path string `json:"path"` Status string `json:"status"` ActiveHolder string `json:"active_holder,omitempty"` CreatedAt string `json:"created_at"` } func worktreeDetailFrom(wt *workspace.Worktree) worktreeDetail { return worktreeDetail{ ID: wt.ID.String(), WorkspaceID: wt.WorkspaceID.String(), Branch: wt.Branch, Path: wt.Path, Status: string(wt.Status), ActiveHolder: wt.ActiveHolder, CreatedAt: wt.CreatedAt.Format("2006-01-02T15:04:05Z07:00"), } } // scopedWorkspace 解析 workspace_id、取 workspace 并做项目 scope 校验。 // 返回的 workspace.Caller 供后续 service 调用复用。 func scopedWorkspace(ctx context.Context, deps ServerDeps, c project.Caller, workspaceID string) (*workspace.Workspace, workspace.Caller, error) { wsID, err := uuid.Parse(workspaceID) if err != nil { return nil, workspace.Caller{}, errs.Wrap(err, errs.CodeInvalidInput, "workspace_id parse") } wc := workspaceCallerFrom(c) w, err := deps.Workspaces.Get(ctx, wc, wsID) if err != nil { return nil, wc, err } if err := CheckProjectFromCtx(ctx, w.ProjectID); err != nil { return nil, wc, err } return w, wc, nil } // worktreeInWorkspace 校验 worktree 归属于给定 workspace 并返回该 worktree。 func worktreeInWorkspace(ctx context.Context, deps ServerDeps, wc workspace.Caller, wsID uuid.UUID, worktreeID string) (*workspace.Worktree, error) { wtID, err := uuid.Parse(worktreeID) if err != nil { return nil, errs.Wrap(err, errs.CodeInvalidInput, "worktree_id parse") } wts, err := deps.Worktrees.List(ctx, wc, wsID) if err != nil { return nil, err } for _, wt := range wts { if wt.ID == wtID { return wt, nil } } return nil, errs.New(errs.CodeNotFound, "worktree not found in workspace") } // ===== get_workspace ===== type getWorkspaceIn struct { WorkspaceID string `json:"workspace_id" jsonschema:"workspace UUID"` } func registerGetWorkspace(srv *mcpsdk.Server, deps ServerDeps) { mcpsdk.AddTool(srv, &mcpsdk.Tool{Name: "get_workspace", Description: "Get a single workspace by ID."}, func(ctx context.Context, _ *mcpsdk.CallToolRequest, in getWorkspaceIn) (*mcpsdk.CallToolResult, workspaceDetail, error) { if err := CheckToolFromCtx(ctx, "get_workspace"); err != nil { return nil, workspaceDetail{}, err } c, err := deps.Caller.Resolve(ctx) if err != nil { return nil, workspaceDetail{}, err } w, _, err := scopedWorkspace(ctx, deps, c, in.WorkspaceID) if err != nil { return nil, workspaceDetail{}, err } return nil, workspaceDetailFrom(w), nil }) } // ===== create_workspace ===== type createWorkspaceIn struct { ProjectSlug string `json:"project_slug" jsonschema:"project slug"` Slug string `json:"slug" jsonschema:"workspace slug (non-empty)"` Name string `json:"name" jsonschema:"display name (non-empty)"` Description string `json:"description,omitempty"` GitRemoteURL string `json:"git_remote_url" jsonschema:"git remote URL to clone"` DefaultBranch string `json:"default_branch,omitempty" jsonschema:"default branch (service default when empty)"` } type workspaceCreateOut struct { OK bool `json:"ok"` Workspace workspaceDetail `json:"workspace"` } func registerCreateWorkspace(srv *mcpsdk.Server, deps ServerDeps) { mcpsdk.AddTool(srv, &mcpsdk.Tool{Name: "create_workspace", Description: "Create a workspace in a project. Git credentials cannot be set via MCP; private repos need credentials configured in the web console."}, func(ctx context.Context, _ *mcpsdk.CallToolRequest, in createWorkspaceIn) (*mcpsdk.CallToolResult, workspaceCreateOut, error) { if err := CheckToolFromCtx(ctx, "create_workspace"); err != nil { return nil, workspaceCreateOut{}, err } c, err := deps.Caller.Resolve(ctx) if err != nil { return nil, workspaceCreateOut{}, err } p, err := deps.Projects.Get(ctx, c, in.ProjectSlug) if err != nil { return nil, workspaceCreateOut{}, err } if err := CheckProjectFromCtx(ctx, p.ID); err != nil { return nil, workspaceCreateOut{}, err } w, err := deps.Workspaces.Create(ctx, workspaceCallerFrom(c), in.ProjectSlug, workspace.CreateWorkspaceInput{ Slug: in.Slug, Name: in.Name, Description: in.Description, GitRemoteURL: in.GitRemoteURL, DefaultBranch: in.DefaultBranch, Credential: workspace.UpsertCredentialInput{Kind: workspace.CredKindNone}, }) if err != nil { return nil, workspaceCreateOut{}, err } return nil, workspaceCreateOut{OK: true, Workspace: workspaceDetailFrom(w)}, nil }) } // ===== update_workspace ===== type updateWorkspaceIn struct { WorkspaceID string `json:"workspace_id" jsonschema:"workspace UUID"` Name string `json:"name,omitempty"` Description string `json:"description,omitempty"` DefaultBranch string `json:"default_branch,omitempty"` } func registerUpdateWorkspace(srv *mcpsdk.Server, deps ServerDeps) { mcpsdk.AddTool(srv, &mcpsdk.Tool{Name: "update_workspace", Description: "Update a workspace (name/description/default_branch)."}, func(ctx context.Context, _ *mcpsdk.CallToolRequest, in updateWorkspaceIn) (*mcpsdk.CallToolResult, workspaceCreateOut, error) { if err := CheckToolFromCtx(ctx, "update_workspace"); err != nil { return nil, workspaceCreateOut{}, err } c, err := deps.Caller.Resolve(ctx) if err != nil { return nil, workspaceCreateOut{}, err } w, wc, err := scopedWorkspace(ctx, deps, c, in.WorkspaceID) if err != nil { return nil, workspaceCreateOut{}, err } input := workspace.UpdateWorkspaceInput{} if in.Name != "" { input.Name = &in.Name } if in.Description != "" { input.Description = &in.Description } if in.DefaultBranch != "" { input.DefaultBranch = &in.DefaultBranch } w, err = deps.Workspaces.Update(ctx, wc, w.ID, input) if err != nil { return nil, workspaceCreateOut{}, err } return nil, workspaceCreateOut{OK: true, Workspace: workspaceDetailFrom(w)}, nil }) } // ===== delete_workspace ===== type deleteWorkspaceIn struct { WorkspaceID string `json:"workspace_id" jsonschema:"workspace UUID"` } func registerDeleteWorkspace(srv *mcpsdk.Server, deps ServerDeps) { mcpsdk.AddTool(srv, &mcpsdk.Tool{Name: "delete_workspace", Description: "Delete a workspace and its local clone. Irreversible."}, func(ctx context.Context, _ *mcpsdk.CallToolRequest, in deleteWorkspaceIn) (*mcpsdk.CallToolResult, okOut, error) { if err := CheckToolFromCtx(ctx, "delete_workspace"); err != nil { return nil, okOut{}, err } c, err := deps.Caller.Resolve(ctx) if err != nil { return nil, okOut{}, err } w, wc, err := scopedWorkspace(ctx, deps, c, in.WorkspaceID) if err != nil { return nil, okOut{}, err } if err := deps.Workspaces.Delete(ctx, wc, w.ID); err != nil { return nil, okOut{}, err } return nil, okOut{OK: true}, nil }) } // ===== sync_workspace ===== func registerSyncWorkspace(srv *mcpsdk.Server, deps ServerDeps) { mcpsdk.AddTool(srv, &mcpsdk.Tool{Name: "sync_workspace", Description: "Trigger a git clone/fetch sync of the workspace."}, func(ctx context.Context, _ *mcpsdk.CallToolRequest, in getWorkspaceIn) (*mcpsdk.CallToolResult, workspaceCreateOut, error) { if err := CheckToolFromCtx(ctx, "sync_workspace"); err != nil { return nil, workspaceCreateOut{}, err } c, err := deps.Caller.Resolve(ctx) if err != nil { return nil, workspaceCreateOut{}, err } w, wc, err := scopedWorkspace(ctx, deps, c, in.WorkspaceID) if err != nil { return nil, workspaceCreateOut{}, err } w, err = deps.Workspaces.Sync(ctx, wc, w.ID) if err != nil { return nil, workspaceCreateOut{}, err } return nil, workspaceCreateOut{OK: true, Workspace: workspaceDetailFrom(w)}, nil }) } // ===== list_branches ===== type listBranchesOut struct { Branches []string `json:"branches"` } func registerListBranches(srv *mcpsdk.Server, deps ServerDeps) { mcpsdk.AddTool(srv, &mcpsdk.Tool{Name: "list_branches", Description: "List git branches of a workspace."}, func(ctx context.Context, _ *mcpsdk.CallToolRequest, in getWorkspaceIn) (*mcpsdk.CallToolResult, listBranchesOut, error) { if err := CheckToolFromCtx(ctx, "list_branches"); err != nil { return nil, listBranchesOut{}, err } c, err := deps.Caller.Resolve(ctx) if err != nil { return nil, listBranchesOut{}, err } w, wc, err := scopedWorkspace(ctx, deps, c, in.WorkspaceID) if err != nil { return nil, listBranchesOut{}, err } bs, err := deps.Workspaces.ListBranches(ctx, wc, w.ID) if err != nil { return nil, listBranchesOut{}, err } return nil, listBranchesOut{Branches: bs}, nil }) } // ===== list_worktrees ===== type listWorktreesOut struct { Worktrees []worktreeDetail `json:"worktrees"` } func registerListWorktrees(srv *mcpsdk.Server, deps ServerDeps) { mcpsdk.AddTool(srv, &mcpsdk.Tool{Name: "list_worktrees", Description: "List worktrees of a workspace."}, func(ctx context.Context, _ *mcpsdk.CallToolRequest, in getWorkspaceIn) (*mcpsdk.CallToolResult, listWorktreesOut, error) { if err := CheckToolFromCtx(ctx, "list_worktrees"); err != nil { return nil, listWorktreesOut{}, err } c, err := deps.Caller.Resolve(ctx) if err != nil { return nil, listWorktreesOut{}, err } w, wc, err := scopedWorkspace(ctx, deps, c, in.WorkspaceID) if err != nil { return nil, listWorktreesOut{}, err } wts, err := deps.Worktrees.List(ctx, wc, w.ID) if err != nil { return nil, listWorktreesOut{}, err } out := listWorktreesOut{Worktrees: make([]worktreeDetail, 0, len(wts))} for _, wt := range wts { out.Worktrees = append(out.Worktrees, worktreeDetailFrom(wt)) } return nil, out, nil }) } // ===== create_worktree ===== type createWorktreeIn struct { WorkspaceID string `json:"workspace_id" jsonschema:"workspace UUID"` Branch string `json:"branch" jsonschema:"branch name for the new worktree"` BaseBranch string `json:"base_branch,omitempty" jsonschema:"base branch (defaults to workspace default branch)"` } type worktreeCreateOut struct { OK bool `json:"ok"` Worktree worktreeDetail `json:"worktree"` } func registerCreateWorktree(srv *mcpsdk.Server, deps ServerDeps) { mcpsdk.AddTool(srv, &mcpsdk.Tool{Name: "create_worktree", Description: "Create a worktree on a branch."}, func(ctx context.Context, _ *mcpsdk.CallToolRequest, in createWorktreeIn) (*mcpsdk.CallToolResult, worktreeCreateOut, error) { if err := CheckToolFromCtx(ctx, "create_worktree"); err != nil { return nil, worktreeCreateOut{}, err } c, err := deps.Caller.Resolve(ctx) if err != nil { return nil, worktreeCreateOut{}, err } w, wc, err := scopedWorkspace(ctx, deps, c, in.WorkspaceID) if err != nil { return nil, worktreeCreateOut{}, err } wt, err := deps.Worktrees.Create(ctx, wc, w.ID, workspace.CreateWorktreeInput{ Branch: in.Branch, BaseBranch: in.BaseBranch, }) if err != nil { return nil, worktreeCreateOut{}, err } return nil, worktreeCreateOut{OK: true, Worktree: worktreeDetailFrom(wt)}, nil }) } // ===== delete/acquire/release_worktree ===== type worktreeTargetIn struct { WorkspaceID string `json:"workspace_id" jsonschema:"workspace UUID"` WorktreeID string `json:"worktree_id" jsonschema:"worktree UUID"` } func registerDeleteWorktree(srv *mcpsdk.Server, deps ServerDeps) { mcpsdk.AddTool(srv, &mcpsdk.Tool{Name: "delete_worktree", Description: "Delete a worktree. Irreversible."}, func(ctx context.Context, _ *mcpsdk.CallToolRequest, in worktreeTargetIn) (*mcpsdk.CallToolResult, okOut, error) { if err := CheckToolFromCtx(ctx, "delete_worktree"); err != nil { return nil, okOut{}, err } c, err := deps.Caller.Resolve(ctx) if err != nil { return nil, okOut{}, err } w, wc, err := scopedWorkspace(ctx, deps, c, in.WorkspaceID) if err != nil { return nil, okOut{}, err } wt, err := worktreeInWorkspace(ctx, deps, wc, w.ID, in.WorktreeID) if err != nil { return nil, okOut{}, err } if err := deps.Worktrees.Delete(ctx, wc, wt.ID); err != nil { return nil, okOut{}, err } return nil, okOut{OK: true}, nil }) } func registerAcquireWorktree(srv *mcpsdk.Server, deps ServerDeps) { mcpsdk.AddTool(srv, &mcpsdk.Tool{Name: "acquire_worktree", Description: "Acquire (lock) a worktree for exclusive use."}, func(ctx context.Context, _ *mcpsdk.CallToolRequest, in worktreeTargetIn) (*mcpsdk.CallToolResult, worktreeCreateOut, error) { if err := CheckToolFromCtx(ctx, "acquire_worktree"); err != nil { return nil, worktreeCreateOut{}, err } c, err := deps.Caller.Resolve(ctx) if err != nil { return nil, worktreeCreateOut{}, err } w, wc, err := scopedWorkspace(ctx, deps, c, in.WorkspaceID) if err != nil { return nil, worktreeCreateOut{}, err } wt, err := worktreeInWorkspace(ctx, deps, wc, w.ID, in.WorktreeID) if err != nil { return nil, worktreeCreateOut{}, err } wt, err = deps.Worktrees.Acquire(ctx, wc, wt.ID) if err != nil { return nil, worktreeCreateOut{}, err } return nil, worktreeCreateOut{OK: true, Worktree: worktreeDetailFrom(wt)}, nil }) } func registerReleaseWorktree(srv *mcpsdk.Server, deps ServerDeps) { mcpsdk.AddTool(srv, &mcpsdk.Tool{Name: "release_worktree", Description: "Release a previously acquired worktree."}, func(ctx context.Context, _ *mcpsdk.CallToolRequest, in worktreeTargetIn) (*mcpsdk.CallToolResult, worktreeCreateOut, error) { if err := CheckToolFromCtx(ctx, "release_worktree"); err != nil { return nil, worktreeCreateOut{}, err } c, err := deps.Caller.Resolve(ctx) if err != nil { return nil, worktreeCreateOut{}, err } w, wc, err := scopedWorkspace(ctx, deps, c, in.WorkspaceID) if err != nil { return nil, worktreeCreateOut{}, err } wt, err := worktreeInWorkspace(ctx, deps, wc, w.ID, in.WorktreeID) if err != nil { return nil, worktreeCreateOut{}, err } wt, err = deps.Worktrees.Release(ctx, wc, wt.ID) if err != nil { return nil, worktreeCreateOut{}, err } return nil, worktreeCreateOut{OK: true, Worktree: worktreeDetailFrom(wt)}, nil }) } // ===== git_status / git_commit / git_push ===== // // 三个 git 工具统一用 workspace_id + 可选 worktree_id 定位目标: // worktree_id 缺省 → 操作 main 工作区;给定 → 校验归属后操作该支线。 type gitTargetIn struct { WorkspaceID string `json:"workspace_id" jsonschema:"workspace UUID"` WorktreeID string `json:"worktree_id,omitempty" jsonschema:"optional worktree UUID; omit to target the main worktree"` } type gitFileStatus struct { Path string `json:"path"` Staged string `json:"staged"` Unstaged string `json:"unstaged"` } type gitStatusOut struct { Files []gitFileStatus `json:"files"` } func registerGitStatus(srv *mcpsdk.Server, deps ServerDeps) { mcpsdk.AddTool(srv, &mcpsdk.Tool{Name: "git_status", Description: "Show git status of the main worktree or a branch worktree."}, func(ctx context.Context, _ *mcpsdk.CallToolRequest, in gitTargetIn) (*mcpsdk.CallToolResult, gitStatusOut, error) { if err := CheckToolFromCtx(ctx, "git_status"); err != nil { return nil, gitStatusOut{}, err } c, err := deps.Caller.Resolve(ctx) if err != nil { return nil, gitStatusOut{}, err } w, wc, err := scopedWorkspace(ctx, deps, c, in.WorkspaceID) if err != nil { return nil, gitStatusOut{}, err } files, err := gitStatusByTarget(ctx, deps, wc, w.ID, in.WorktreeID) if err != nil { return nil, gitStatusOut{}, err } out := gitStatusOut{Files: make([]gitFileStatus, 0, len(files))} for _, f := range files { out.Files = append(out.Files, gitFileStatus{ Path: f.Path, Staged: string(f.IndexX), Unstaged: string(f.WorktreeY), }) } return nil, out, nil }) } func gitStatusByTarget(ctx context.Context, deps ServerDeps, wc workspace.Caller, wsID uuid.UUID, worktreeID string) ([]git.FileStatus, error) { if worktreeID == "" { return deps.GitOps.StatusOnMain(ctx, wc, wsID) } wt, err := worktreeInWorkspace(ctx, deps, wc, wsID, worktreeID) if err != nil { return nil, err } return deps.GitOps.StatusOnWorktree(ctx, wc, wt.ID) } type createCommitIn struct { WorkspaceID string `json:"workspace_id" jsonschema:"workspace UUID"` WorktreeID string `json:"worktree_id,omitempty" jsonschema:"optional worktree UUID; omit to target the main worktree"` Message string `json:"message" jsonschema:"commit message (non-empty)"` AddAll *bool `json:"add_all,omitempty" jsonschema:"stage all changes before commit (default true)"` Push bool `json:"push,omitempty" jsonschema:"push after commit (default false)"` } type gitCommitOut struct { OK bool `json:"ok"` CommitHash string `json:"commit_hash"` } func registerGitCommit(srv *mcpsdk.Server, deps ServerDeps) { mcpsdk.AddTool(srv, &mcpsdk.Tool{Name: "git_commit", Description: "Commit changes on the main worktree or a branch worktree."}, func(ctx context.Context, _ *mcpsdk.CallToolRequest, in createCommitIn) (*mcpsdk.CallToolResult, gitCommitOut, error) { if err := CheckToolFromCtx(ctx, "git_commit"); err != nil { return nil, gitCommitOut{}, err } c, err := deps.Caller.Resolve(ctx) if err != nil { return nil, gitCommitOut{}, err } w, wc, err := scopedWorkspace(ctx, deps, c, in.WorkspaceID) if err != nil { return nil, gitCommitOut{}, err } addAll := true if in.AddAll != nil { addAll = *in.AddAll } commit := workspace.CommitInput{Message: in.Message, AddAll: addAll, Push: in.Push} var hash string if in.WorktreeID == "" { hash, err = deps.GitOps.CommitOnMain(ctx, wc, w.ID, commit) } else { var wt *workspace.Worktree wt, err = worktreeInWorkspace(ctx, deps, wc, w.ID, in.WorktreeID) if err != nil { return nil, gitCommitOut{}, err } hash, err = deps.GitOps.CommitOnWorktree(ctx, wc, wt.ID, commit) } if err != nil { return nil, gitCommitOut{}, err } return nil, gitCommitOut{OK: true, CommitHash: hash}, nil }) } // ===== git_diff (read-only) ===== type gitDiffIn struct { WorkspaceID string `json:"workspace_id" jsonschema:"workspace UUID"` WorktreeID string `json:"worktree_id,omitempty" jsonschema:"optional worktree UUID; omit to target the main worktree"` Ref string `json:"ref,omitempty" jsonschema:"base ref; empty diffs working tree against HEAD"` Against string `json:"against,omitempty" jsonschema:"second ref; when set with ref produces ref..against"` Staged bool `json:"staged,omitempty" jsonschema:"diff the staged index (--cached) instead of the working tree"` Paths []string `json:"paths,omitempty" jsonschema:"optional pathspecs to restrict the diff"` ContextLines int `json:"context_lines,omitempty" jsonschema:"unified context lines (git default 3 when omitted)"` } type gitDiffOut struct { Diff string `json:"diff"` Truncated bool `json:"truncated"` } func registerGitDiff(srv *mcpsdk.Server, deps ServerDeps) { mcpsdk.AddTool(srv, &mcpsdk.Tool{Name: "git_diff", Description: "Show the unified git diff of the main worktree or a branch worktree. Returns raw diff text (capped at 1 MiB)."}, func(ctx context.Context, _ *mcpsdk.CallToolRequest, in gitDiffIn) (*mcpsdk.CallToolResult, gitDiffOut, error) { if err := CheckToolFromCtx(ctx, "git_diff"); err != nil { return nil, gitDiffOut{}, err } c, err := deps.Caller.Resolve(ctx) if err != nil { return nil, gitDiffOut{}, err } w, wc, err := scopedWorkspace(ctx, deps, c, in.WorkspaceID) if err != nil { return nil, gitDiffOut{}, err } opts := git.DiffOptions{ Ref: in.Ref, Against: in.Against, Staged: in.Staged, Paths: in.Paths, ContextLines: in.ContextLines, } var res git.DiffResult if in.WorktreeID == "" { res, err = deps.GitOps.DiffOnMain(ctx, wc, w.ID, opts) } else { var wt *workspace.Worktree wt, err = worktreeInWorkspace(ctx, deps, wc, w.ID, in.WorktreeID) if err != nil { return nil, gitDiffOut{}, err } res, err = deps.GitOps.DiffOnWorktree(ctx, wc, wt.ID, opts) } if err != nil { return nil, gitDiffOut{}, err } return nil, gitDiffOut{Diff: res.Diff, Truncated: res.Truncated}, nil }) } func registerGitPush(srv *mcpsdk.Server, deps ServerDeps) { mcpsdk.AddTool(srv, &mcpsdk.Tool{Name: "git_push", Description: "Push the main worktree or a branch worktree to the remote."}, func(ctx context.Context, _ *mcpsdk.CallToolRequest, in gitTargetIn) (*mcpsdk.CallToolResult, okOut, error) { if err := CheckToolFromCtx(ctx, "git_push"); err != nil { return nil, okOut{}, err } c, err := deps.Caller.Resolve(ctx) if err != nil { return nil, okOut{}, err } w, wc, err := scopedWorkspace(ctx, deps, c, in.WorkspaceID) if err != nil { return nil, okOut{}, err } if in.WorktreeID == "" { err = deps.GitOps.PushOnMain(ctx, wc, w.ID) } else { var wt *workspace.Worktree wt, err = worktreeInWorkspace(ctx, deps, wc, w.ID, in.WorktreeID) if err != nil { return nil, okOut{}, err } err = deps.GitOps.PushOnWorktree(ctx, wc, wt.ID) } if err != nil { return nil, okOut{}, err } return nil, okOut{OK: true}, nil }) }