You've already forked agentic-coding-workflow
完善 MCP 工具
This commit is contained in:
@@ -0,0 +1,614 @@
|
||||
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
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user