Files
2026-06-22 08:55:57 +08:00

669 lines
24 KiB
Go

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
})
}