You've already forked agentic-coding-workflow
615 lines
21 KiB
Go
615 lines
21 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)
|
|
}
|
|
|
|
// ===== 共用 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
|
|
})
|
|
}
|
|
|
|
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
|
|
})
|
|
}
|