package mcp import ( "context" "fmt" "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/project" "github.com/yan1h/agent-coding-workflow/internal/workspace" ) func registerPMTools(srv *mcpsdk.Server, deps ServerDeps) { registerListProjects(srv, deps) registerCreateProject(srv, deps) registerUpdateProject(srv, deps) registerArchiveProject(srv, deps) registerUnarchiveProject(srv, deps) registerListWorkspaces(srv, deps) registerListRequirements(srv, deps) registerListIssues(srv, deps) registerGetIssue(srv, deps) registerGetRequirement(srv, deps) registerCreateIssue(srv, deps) registerCreateSubtask(srv, deps) registerAddDependency(srv, deps) registerRemoveDependency(srv, deps) registerListDependencies(srv, deps) registerSubmitDecomposition(srv, deps) registerUpdateIssue(srv, deps) registerCloseIssue(srv, deps) registerReopenIssue(srv, deps) registerAssignIssue(srv, deps) registerCreateRequirement(srv, deps) registerUpdateRequirement(srv, deps) registerCloseRequirement(srv, deps) registerReopenRequirement(srv, deps) registerSetRequirementPhase(srv, deps) registerAddRequirementArtifact(srv, deps) registerApproveRequirementArtifact(srv, deps) registerListRequirementArtifacts(srv, deps) } // ===== list_projects ===== type listProjectsIn struct { Archived bool `json:"archived,omitempty" jsonschema:"include archived projects"` } type projectSummary struct { Slug string `json:"slug"` Name string `json:"name"` Visibility string `json:"visibility"` } type listProjectsOut struct { Projects []projectSummary `json:"projects"` } func registerListProjects(srv *mcpsdk.Server, deps ServerDeps) { mcpsdk.AddTool(srv, &mcpsdk.Tool{Name: "list_projects", Description: "List projects visible to the caller."}, func(ctx context.Context, _ *mcpsdk.CallToolRequest, in listProjectsIn) (*mcpsdk.CallToolResult, listProjectsOut, error) { if err := CheckToolFromCtx(ctx, "list_projects"); err != nil { return nil, listProjectsOut{}, err } c, err := deps.Caller.Resolve(ctx) if err != nil { return nil, listProjectsOut{}, err } ps, err := deps.Projects.List(ctx, c, in.Archived) if err != nil { return nil, listProjectsOut{}, err } out := listProjectsOut{Projects: make([]projectSummary, 0, len(ps))} for _, p := range ps { if err := CheckProjectFromCtx(ctx, p.ID); err != nil { continue } out.Projects = append(out.Projects, projectSummary{ Slug: p.Slug, Name: p.Name, Visibility: string(p.Visibility), }) } return nil, out, nil }) } // ===== list_workspaces ===== type listWorkspacesIn struct { ProjectSlug string `json:"project_slug" jsonschema:"project slug"` } type workspaceSummary struct { ID string `json:"id"` Slug string `json:"slug"` Name string `json:"name"` DefaultBranch string `json:"default_branch"` SyncStatus string `json:"sync_status"` } type listWorkspacesOut struct { Workspaces []workspaceSummary `json:"workspaces"` } func registerListWorkspaces(srv *mcpsdk.Server, deps ServerDeps) { mcpsdk.AddTool(srv, &mcpsdk.Tool{Name: "list_workspaces", Description: "List workspaces in a project."}, func(ctx context.Context, _ *mcpsdk.CallToolRequest, in listWorkspacesIn) (*mcpsdk.CallToolResult, listWorkspacesOut, error) { if err := CheckToolFromCtx(ctx, "list_workspaces"); err != nil { return nil, listWorkspacesOut{}, err } c, err := deps.Caller.Resolve(ctx) if err != nil { return nil, listWorkspacesOut{}, err } p, err := deps.Projects.Get(ctx, c, in.ProjectSlug) if err != nil { return nil, listWorkspacesOut{}, err } if err := CheckProjectFromCtx(ctx, p.ID); err != nil { return nil, listWorkspacesOut{}, err } wsCaller := workspace.Caller{UserID: c.UserID, IsAdmin: c.IsAdmin} ws, err := deps.Workspaces.List(ctx, wsCaller, in.ProjectSlug) if err != nil { return nil, listWorkspacesOut{}, err } out := listWorkspacesOut{Workspaces: make([]workspaceSummary, 0, len(ws))} for _, w := range ws { out.Workspaces = append(out.Workspaces, workspaceSummary{ ID: w.ID.String(), Slug: w.Slug, Name: w.Name, DefaultBranch: w.DefaultBranch, SyncStatus: string(w.SyncStatus), }) } return nil, out, nil }) } // ===== list_requirements ===== type listRequirementsIn struct { ProjectSlug string `json:"project_slug" jsonschema:"required field"` Phase string `json:"phase,omitempty"` Status string `json:"status,omitempty"` } type requirementSummary struct { Number int `json:"number"` Title string `json:"title"` Phase string `json:"phase"` Status string `json:"status"` } type listRequirementsOut struct { Requirements []requirementSummary `json:"requirements"` } func registerListRequirements(srv *mcpsdk.Server, deps ServerDeps) { mcpsdk.AddTool(srv, &mcpsdk.Tool{Name: "list_requirements", Description: "List requirements in a project."}, func(ctx context.Context, _ *mcpsdk.CallToolRequest, in listRequirementsIn) (*mcpsdk.CallToolResult, listRequirementsOut, error) { if err := CheckToolFromCtx(ctx, "list_requirements"); err != nil { return nil, listRequirementsOut{}, err } c, err := deps.Caller.Resolve(ctx) if err != nil { return nil, listRequirementsOut{}, err } p, err := deps.Projects.Get(ctx, c, in.ProjectSlug) if err != nil { return nil, listRequirementsOut{}, err } if err := CheckProjectFromCtx(ctx, p.ID); err != nil { return nil, listRequirementsOut{}, err } rs, err := deps.Reqs.List(ctx, c, in.ProjectSlug, project.RequirementFilter{ Phase: project.Phase(in.Phase), Status: project.Status(in.Status), }) if err != nil { return nil, listRequirementsOut{}, err } out := listRequirementsOut{Requirements: make([]requirementSummary, 0, len(rs))} for _, r := range rs { out.Requirements = append(out.Requirements, requirementSummary{ Number: r.Number, Title: r.Title, Phase: string(r.Phase), Status: string(r.Status), }) } return nil, out, nil }) } // ===== list_issues ===== type listIssuesIn struct { ProjectSlug string `json:"project_slug" jsonschema:"required field"` RequirementNumber any `json:"requirement_number,omitempty"` Status string `json:"status,omitempty"` AssigneeID string `json:"assignee_id,omitempty"` } type issueSummary struct { Number int `json:"number"` Title string `json:"title"` Status string `json:"status"` } type listIssuesOut struct { Issues []issueSummary `json:"issues"` } func registerListIssues(srv *mcpsdk.Server, deps ServerDeps) { mcpsdk.AddTool(srv, &mcpsdk.Tool{Name: "list_issues", Description: "List issues in a project."}, func(ctx context.Context, _ *mcpsdk.CallToolRequest, in listIssuesIn) (*mcpsdk.CallToolResult, listIssuesOut, error) { if err := CheckToolFromCtx(ctx, "list_issues"); err != nil { return nil, listIssuesOut{}, err } c, err := deps.Caller.Resolve(ctx) if err != nil { return nil, listIssuesOut{}, err } p, err := deps.Projects.Get(ctx, c, in.ProjectSlug) if err != nil { return nil, listIssuesOut{}, err } if err := CheckProjectFromCtx(ctx, p.ID); err != nil { return nil, listIssuesOut{}, err } f := project.IssueFilter{Status: project.Status(in.Status)} switch v := in.RequirementNumber.(type) { case nil: case string: if v == "none" { zero := 0 f.Requirement = &zero } case float64: n := int(v) f.Requirement = &n } if in.AssigneeID != "" { aid, err := uuid.Parse(in.AssigneeID) if err != nil { return nil, listIssuesOut{}, errs.Wrap(err, errs.CodeInvalidInput, "assignee_id parse") } f.AssigneeID = &aid } is, err := deps.Issues.List(ctx, c, in.ProjectSlug, f) if err != nil { return nil, listIssuesOut{}, err } out := listIssuesOut{Issues: make([]issueSummary, 0, len(is))} for _, i := range is { out.Issues = append(out.Issues, issueSummary{ Number: i.Number, Title: i.Title, Status: string(i.Status), }) } return nil, out, nil }) } // ===== get_issue ===== type getIssueIn struct { ProjectSlug string `json:"project_slug" jsonschema:"required field"` Number int `json:"number" jsonschema:"issue number (>=1)"` } type issueDetail struct { Number int `json:"number"` Title string `json:"title"` Description string `json:"description,omitempty"` Status string `json:"status"` CreatedAt string `json:"created_at"` } func registerGetIssue(srv *mcpsdk.Server, deps ServerDeps) { mcpsdk.AddTool(srv, &mcpsdk.Tool{Name: "get_issue", Description: "Get a single issue by project_slug + number."}, func(ctx context.Context, _ *mcpsdk.CallToolRequest, in getIssueIn) (*mcpsdk.CallToolResult, issueDetail, error) { if err := CheckToolFromCtx(ctx, "get_issue"); err != nil { return nil, issueDetail{}, err } c, err := deps.Caller.Resolve(ctx) if err != nil { return nil, issueDetail{}, err } p, err := deps.Projects.Get(ctx, c, in.ProjectSlug) if err != nil { return nil, issueDetail{}, err } if err := CheckProjectFromCtx(ctx, p.ID); err != nil { return nil, issueDetail{}, err } i, err := deps.Issues.Get(ctx, c, in.ProjectSlug, in.Number) if err != nil { return nil, issueDetail{}, err } return nil, issueDetail{ Number: i.Number, Title: i.Title, Description: i.Description, Status: string(i.Status), CreatedAt: i.CreatedAt.Format("2006-01-02T15:04:05Z07:00"), }, nil }) } // ===== get_requirement ===== type getRequirementIn struct { ProjectSlug string `json:"project_slug" jsonschema:"required field"` Number int `json:"number" jsonschema:"issue number (>=1)"` } type requirementDetail struct { Number int `json:"number"` Title string `json:"title"` Description string `json:"description,omitempty"` Phase string `json:"phase"` Status string `json:"status"` CreatedAt string `json:"created_at"` } func registerGetRequirement(srv *mcpsdk.Server, deps ServerDeps) { mcpsdk.AddTool(srv, &mcpsdk.Tool{Name: "get_requirement", Description: "Get a single requirement by project_slug + number."}, func(ctx context.Context, _ *mcpsdk.CallToolRequest, in getRequirementIn) (*mcpsdk.CallToolResult, requirementDetail, error) { if err := CheckToolFromCtx(ctx, "get_requirement"); err != nil { return nil, requirementDetail{}, err } c, err := deps.Caller.Resolve(ctx) if err != nil { return nil, requirementDetail{}, err } p, err := deps.Projects.Get(ctx, c, in.ProjectSlug) if err != nil { return nil, requirementDetail{}, err } if err := CheckProjectFromCtx(ctx, p.ID); err != nil { return nil, requirementDetail{}, err } r, err := deps.Reqs.Get(ctx, c, in.ProjectSlug, in.Number) if err != nil { return nil, requirementDetail{}, err } return nil, requirementDetail{ Number: r.Number, Title: r.Title, Description: r.Description, Phase: string(r.Phase), Status: string(r.Status), CreatedAt: r.CreatedAt.Format("2006-01-02T15:04:05Z07:00"), }, nil }) } // ===== create_issue ===== type createIssueIn struct { ProjectSlug string `json:"project_slug" jsonschema:"required field"` Title string `json:"title" jsonschema:"title (non-empty)"` Description string `json:"description,omitempty"` } type issueCreateOut struct { OK bool `json:"ok"` Issue issueDetail `json:"issue"` } func registerCreateIssue(srv *mcpsdk.Server, deps ServerDeps) { mcpsdk.AddTool(srv, &mcpsdk.Tool{Name: "create_issue", Description: "Create a new issue in a project."}, func(ctx context.Context, _ *mcpsdk.CallToolRequest, in createIssueIn) (*mcpsdk.CallToolResult, issueCreateOut, error) { if err := CheckToolFromCtx(ctx, "create_issue"); err != nil { return nil, issueCreateOut{}, err } c, err := deps.Caller.Resolve(ctx) if err != nil { return nil, issueCreateOut{}, err } p, err := deps.Projects.Get(ctx, c, in.ProjectSlug) if err != nil { return nil, issueCreateOut{}, err } if err := CheckProjectFromCtx(ctx, p.ID); err != nil { return nil, issueCreateOut{}, err } i, err := deps.Issues.Create(ctx, c, in.ProjectSlug, project.CreateIssueInput{ Title: in.Title, Description: in.Description, }) if err != nil { return nil, issueCreateOut{}, err } return nil, issueCreateOut{OK: true, Issue: issueDetail{ Number: i.Number, Title: i.Title, Description: i.Description, Status: string(i.Status), CreatedAt: i.CreatedAt.Format("2006-01-02T15:04:05Z07:00"), }}, nil }) } // ===== create_subtask ===== type createSubtaskIn struct { ProjectSlug string `json:"project_slug" jsonschema:"required field"` ParentNumber int `json:"parent_number" jsonschema:"parent issue number (>=1)"` Title string `json:"title" jsonschema:"title (non-empty)"` Description string `json:"description,omitempty"` Priority int `json:"priority,omitempty" jsonschema:"scheduling priority 0..3 (higher = sooner)"` } func registerCreateSubtask(srv *mcpsdk.Server, deps ServerDeps) { mcpsdk.AddTool(srv, &mcpsdk.Tool{Name: "create_subtask", Description: "Create a subtask under a parent issue (task decomposition). Sets parent_id + priority."}, func(ctx context.Context, _ *mcpsdk.CallToolRequest, in createSubtaskIn) (*mcpsdk.CallToolResult, issueCreateOut, error) { if err := CheckToolFromCtx(ctx, "create_subtask"); err != nil { return nil, issueCreateOut{}, err } c, err := deps.Caller.Resolve(ctx) if err != nil { return nil, issueCreateOut{}, err } p, err := deps.Projects.Get(ctx, c, in.ProjectSlug) if err != nil { return nil, issueCreateOut{}, err } if err := CheckProjectFromCtx(ctx, p.ID); err != nil { return nil, issueCreateOut{}, err } prio := in.Priority i, err := deps.Issues.CreateSubtask(ctx, c, in.ProjectSlug, in.ParentNumber, project.CreateIssueInput{ Title: in.Title, Description: in.Description, Priority: &prio, }) if err != nil { return nil, issueCreateOut{}, err } return nil, issueCreateOut{OK: true, Issue: issueDetail{ Number: i.Number, Title: i.Title, Description: i.Description, Status: string(i.Status), CreatedAt: i.CreatedAt.Format("2006-01-02T15:04:05Z07:00"), }}, nil }) } // ===== add_dependency ===== type addDependencyIn struct { ProjectSlug string `json:"project_slug" jsonschema:"required field"` BlockedNumber int `json:"blocked_number" jsonschema:"issue blocked-by blocker_number"` BlockerNumber int `json:"blocker_number" jsonschema:"blocking issue number"` } type dependencyOut struct { OK bool `json:"ok"` BlockedNumber int `json:"blocked_number"` BlockerNumber int `json:"blocker_number"` } func registerAddDependency(srv *mcpsdk.Server, deps ServerDeps) { mcpsdk.AddTool(srv, &mcpsdk.Tool{Name: "add_dependency", Description: "Add a dependency edge: blocked_number is blocked-by blocker_number. Rejects cycles and cross-project edges."}, func(ctx context.Context, _ *mcpsdk.CallToolRequest, in addDependencyIn) (*mcpsdk.CallToolResult, dependencyOut, error) { if err := CheckToolFromCtx(ctx, "add_dependency"); err != nil { return nil, dependencyOut{}, err } c, err := deps.Caller.Resolve(ctx) if err != nil { return nil, dependencyOut{}, err } p, err := deps.Projects.Get(ctx, c, in.ProjectSlug) if err != nil { return nil, dependencyOut{}, err } if err := CheckProjectFromCtx(ctx, p.ID); err != nil { return nil, dependencyOut{}, err } if _, err := deps.Issues.AddDependency(ctx, c, in.ProjectSlug, project.AddDependencyInput{ BlockedNumber: in.BlockedNumber, BlockerNumber: in.BlockerNumber, }); err != nil { return nil, dependencyOut{}, err } return nil, dependencyOut{OK: true, BlockedNumber: in.BlockedNumber, BlockerNumber: in.BlockerNumber}, nil }) } // ===== remove_dependency ===== func registerRemoveDependency(srv *mcpsdk.Server, deps ServerDeps) { mcpsdk.AddTool(srv, &mcpsdk.Tool{Name: "remove_dependency", Description: "Remove a dependency edge (plan correction)."}, func(ctx context.Context, _ *mcpsdk.CallToolRequest, in addDependencyIn) (*mcpsdk.CallToolResult, dependencyOut, error) { if err := CheckToolFromCtx(ctx, "remove_dependency"); err != nil { return nil, dependencyOut{}, err } c, err := deps.Caller.Resolve(ctx) if err != nil { return nil, dependencyOut{}, err } p, err := deps.Projects.Get(ctx, c, in.ProjectSlug) if err != nil { return nil, dependencyOut{}, err } if err := CheckProjectFromCtx(ctx, p.ID); err != nil { return nil, dependencyOut{}, err } if err := deps.Issues.RemoveDependency(ctx, c, in.ProjectSlug, in.BlockedNumber, in.BlockerNumber); err != nil { return nil, dependencyOut{}, err } return nil, dependencyOut{OK: true, BlockedNumber: in.BlockedNumber, BlockerNumber: in.BlockerNumber}, nil }) } // ===== list_dependencies ===== type listDependenciesIn struct { ProjectSlug string `json:"project_slug" jsonschema:"required field"` Number int `json:"number" jsonschema:"issue number (>=1)"` } type listDependenciesOut struct { BlockedBy []issueSummary `json:"blocked_by"` Blocks []issueSummary `json:"blocks"` } func registerListDependencies(srv *mcpsdk.Server, deps ServerDeps) { mcpsdk.AddTool(srv, &mcpsdk.Tool{Name: "list_dependencies", Description: "List an issue's dependencies: {blocked_by:[...], blocks:[...]}."}, func(ctx context.Context, _ *mcpsdk.CallToolRequest, in listDependenciesIn) (*mcpsdk.CallToolResult, listDependenciesOut, error) { if err := CheckToolFromCtx(ctx, "list_dependencies"); err != nil { return nil, listDependenciesOut{}, err } c, err := deps.Caller.Resolve(ctx) if err != nil { return nil, listDependenciesOut{}, err } p, err := deps.Projects.Get(ctx, c, in.ProjectSlug) if err != nil { return nil, listDependenciesOut{}, err } if err := CheckProjectFromCtx(ctx, p.ID); err != nil { return nil, listDependenciesOut{}, err } blockedBy, blocks, err := deps.Issues.ListDependencies(ctx, c, in.ProjectSlug, in.Number) if err != nil { return nil, listDependenciesOut{}, err } out := listDependenciesOut{ BlockedBy: make([]issueSummary, 0, len(blockedBy)), Blocks: make([]issueSummary, 0, len(blocks)), } for _, i := range blockedBy { out.BlockedBy = append(out.BlockedBy, issueSummary{Number: i.Number, Title: i.Title, Status: string(i.Status)}) } for _, i := range blocks { out.Blocks = append(out.Blocks, issueSummary{Number: i.Number, Title: i.Title, Status: string(i.Status)}) } return nil, out, nil }) } // ===== submit_decomposition ===== // // 一次原子调用提交整张任务分解图(子任务 + 依赖边)。先做 in-batch DAG 校验,全部 // 通过后再建子任务、建边。ref 为 batch 内本地引用键,落库后映射为 issue number。 type decompSubtaskIn struct { Ref string `json:"ref" jsonschema:"local reference key used by edges"` Title string `json:"title" jsonschema:"title (non-empty)"` Description string `json:"description,omitempty"` Priority int `json:"priority,omitempty" jsonschema:"0..3 (higher = sooner)"` } type decompEdgeIn struct { BlockedRef string `json:"blocked_ref" jsonschema:"ref of the blocked subtask"` BlockerRef string `json:"blocker_ref" jsonschema:"ref of the blocking subtask"` } type submitDecompositionIn struct { ProjectSlug string `json:"project_slug" jsonschema:"required field"` ParentNumber *int `json:"parent_number,omitempty" jsonschema:"optional anchor issue number; subtasks become its children"` Subtasks []decompSubtaskIn `json:"subtasks"` Edges []decompEdgeIn `json:"edges,omitempty"` } type submitDecompositionOut struct { OK bool `json:"ok"` RefToNumber map[string]int `json:"ref_to_number"` } func registerSubmitDecomposition(srv *mcpsdk.Server, deps ServerDeps) { mcpsdk.AddTool(srv, &mcpsdk.Tool{Name: "submit_decomposition", Description: "Submit a whole task decomposition (subtasks + blocks/blocked-by edges) atomically. Validates acyclicity before any write; maps refs to created issue numbers."}, func(ctx context.Context, _ *mcpsdk.CallToolRequest, in submitDecompositionIn) (*mcpsdk.CallToolResult, submitDecompositionOut, error) { if err := CheckToolFromCtx(ctx, "submit_decomposition"); err != nil { return nil, submitDecompositionOut{}, err } c, err := deps.Caller.Resolve(ctx) if err != nil { return nil, submitDecompositionOut{}, err } p, err := deps.Projects.Get(ctx, c, in.ProjectSlug) if err != nil { return nil, submitDecompositionOut{}, err } if err := CheckProjectFromCtx(ctx, p.ID); err != nil { return nil, submitDecompositionOut{}, err } res, err := applyDecomposition(ctx, deps.Issues, c, in) if err != nil { return nil, submitDecompositionOut{}, err } return nil, submitDecompositionOut{OK: true, RefToNumber: res}, nil }) } // applyDecomposition 在 mcp 包内实现分解落库(不引入 orchestrator 以避免 import cycle)。 // 先 ref 去重 + 边端点校验 + DAG 校验,再建子任务、建边。 func applyDecomposition(ctx context.Context, issues project.IssueService, c project.Caller, in submitDecompositionIn) (map[string]int, error) { if len(in.Subtasks) == 0 { return nil, errs.New(errs.CodeInvalidInput, "decomposition 至少需要一个子任务") } refSet := make(map[string]struct{}, len(in.Subtasks)) for _, st := range in.Subtasks { if st.Ref == "" { return nil, errs.New(errs.CodeInvalidInput, "子任务 ref 不能为空") } if st.Title == "" { return nil, errs.New(errs.CodeInvalidInput, "子任务 title 不能为空") } if _, dup := refSet[st.Ref]; dup { return nil, errs.New(errs.CodeInvalidInput, "子任务 ref 重复: "+st.Ref) } if st.Priority < 0 || st.Priority > 3 { return nil, errs.New(errs.CodeInvalidInput, "子任务 priority 必须在 0..3") } refSet[st.Ref] = struct{}{} } for _, e := range in.Edges { if _, ok := refSet[e.BlockedRef]; !ok { return nil, errs.New(errs.CodeInvalidInput, "边引用了未知 blocked ref: "+e.BlockedRef) } if _, ok := refSet[e.BlockerRef]; !ok { return nil, errs.New(errs.CodeInvalidInput, "边引用了未知 blocker ref: "+e.BlockerRef) } if e.BlockedRef == e.BlockerRef { return nil, errs.New(errs.CodeInvalidInput, "边不能自指: "+e.BlockedRef) } } if err := decompositionIsDAG(in.Subtasks, in.Edges); err != nil { return nil, err } refToNum := make(map[string]int, len(in.Subtasks)) for _, st := range in.Subtasks { prio := st.Priority ci := project.CreateIssueInput{Title: st.Title, Description: st.Description, Priority: &prio} var iss *project.Issue var err error if in.ParentNumber != nil { iss, err = issues.CreateSubtask(ctx, c, in.ProjectSlug, *in.ParentNumber, ci) } else { iss, err = issues.Create(ctx, c, in.ProjectSlug, ci) } if err != nil { return nil, err } refToNum[st.Ref] = iss.Number } for _, e := range in.Edges { if _, err := issues.AddDependency(ctx, c, in.ProjectSlug, project.AddDependencyInput{ BlockedNumber: refToNum[e.BlockedRef], BlockerNumber: refToNum[e.BlockerRef], }); err != nil { return nil, err } } return refToNum, nil } // decompositionIsDAG 对 (子任务, 边) 做 Kahn 拓扑排序,存在环返回 CodeInvalidInput。 func decompositionIsDAG(subtasks []decompSubtaskIn, edges []decompEdgeIn) error { indeg := make(map[string]int, len(subtasks)) for _, st := range subtasks { indeg[st.Ref] = 0 } adj := make(map[string][]string, len(subtasks)) for _, e := range edges { adj[e.BlockerRef] = append(adj[e.BlockerRef], e.BlockedRef) indeg[e.BlockedRef]++ } queue := make([]string, 0, len(subtasks)) for ref, d := range indeg { if d == 0 { queue = append(queue, ref) } } visited := 0 for len(queue) > 0 { cur := queue[0] queue = queue[1:] visited++ for _, next := range adj[cur] { indeg[next]-- if indeg[next] == 0 { queue = append(queue, next) } } } if visited != len(subtasks) { return errs.New(errs.CodeInvalidInput, "decomposition 边集合存在环") } return nil } // ===== update_issue ===== type updateIssueIn struct { ProjectSlug string `json:"project_slug" jsonschema:"required field"` Number int `json:"number" jsonschema:"issue number (>=1)"` Title string `json:"title,omitempty"` Description string `json:"description,omitempty"` } type issueUpdateOut struct { OK bool `json:"ok"` Issue issueDetail `json:"issue"` } func registerUpdateIssue(srv *mcpsdk.Server, deps ServerDeps) { mcpsdk.AddTool(srv, &mcpsdk.Tool{Name: "update_issue", Description: "Update an existing issue."}, func(ctx context.Context, _ *mcpsdk.CallToolRequest, in updateIssueIn) (*mcpsdk.CallToolResult, issueUpdateOut, error) { if err := CheckToolFromCtx(ctx, "update_issue"); err != nil { return nil, issueUpdateOut{}, err } c, err := deps.Caller.Resolve(ctx) if err != nil { return nil, issueUpdateOut{}, err } p, err := deps.Projects.Get(ctx, c, in.ProjectSlug) if err != nil { return nil, issueUpdateOut{}, err } if err := CheckProjectFromCtx(ctx, p.ID); err != nil { return nil, issueUpdateOut{}, err } input := project.UpdateIssueInput{} if in.Title != "" { input.Title = &in.Title } if in.Description != "" { input.Description = &in.Description } i, err := deps.Issues.Update(ctx, c, in.ProjectSlug, in.Number, input) if err != nil { return nil, issueUpdateOut{}, err } return nil, issueUpdateOut{OK: true, Issue: issueDetail{ Number: i.Number, Title: i.Title, Description: i.Description, Status: string(i.Status), CreatedAt: i.CreatedAt.Format("2006-01-02T15:04:05Z07:00"), }}, nil }) } // ===== close/reopen_issue ===== type closeIssueIn struct { ProjectSlug string `json:"project_slug" jsonschema:"required field"` Number int `json:"number" jsonschema:"issue number (>=1)"` } type okOut struct { OK bool `json:"ok"` } func registerCloseIssue(srv *mcpsdk.Server, deps ServerDeps) { mcpsdk.AddTool(srv, &mcpsdk.Tool{Name: "close_issue", Description: "Close an issue (idempotent)."}, func(ctx context.Context, _ *mcpsdk.CallToolRequest, in closeIssueIn) (*mcpsdk.CallToolResult, okOut, error) { if err := CheckToolFromCtx(ctx, "close_issue"); err != nil { return nil, okOut{}, err } c, err := deps.Caller.Resolve(ctx) if err != nil { return nil, okOut{}, err } p, err := deps.Projects.Get(ctx, c, in.ProjectSlug) if err != nil { return nil, okOut{}, err } if err := CheckProjectFromCtx(ctx, p.ID); err != nil { return nil, okOut{}, err } if err := deps.Issues.Close(ctx, c, in.ProjectSlug, in.Number); err != nil { return nil, okOut{}, err } return nil, okOut{OK: true}, nil }) } func registerReopenIssue(srv *mcpsdk.Server, deps ServerDeps) { mcpsdk.AddTool(srv, &mcpsdk.Tool{Name: "reopen_issue", Description: "Reopen a closed issue (idempotent)."}, func(ctx context.Context, _ *mcpsdk.CallToolRequest, in closeIssueIn) (*mcpsdk.CallToolResult, okOut, error) { if err := CheckToolFromCtx(ctx, "reopen_issue"); err != nil { return nil, okOut{}, err } c, err := deps.Caller.Resolve(ctx) if err != nil { return nil, okOut{}, err } p, err := deps.Projects.Get(ctx, c, in.ProjectSlug) if err != nil { return nil, okOut{}, err } if err := CheckProjectFromCtx(ctx, p.ID); err != nil { return nil, okOut{}, err } if err := deps.Issues.Reopen(ctx, c, in.ProjectSlug, in.Number); err != nil { return nil, okOut{}, err } return nil, okOut{OK: true}, nil }) } // ===== assign_issue ===== type assignIssueIn struct { ProjectSlug string `json:"project_slug" jsonschema:"required field"` Number int `json:"number" jsonschema:"issue number (>=1)"` AssigneeID *string `json:"assignee_id,omitempty"` } func registerAssignIssue(srv *mcpsdk.Server, deps ServerDeps) { mcpsdk.AddTool(srv, &mcpsdk.Tool{Name: "assign_issue", Description: "Assign or unassign an issue."}, func(ctx context.Context, _ *mcpsdk.CallToolRequest, in assignIssueIn) (*mcpsdk.CallToolResult, okOut, error) { if err := CheckToolFromCtx(ctx, "assign_issue"); err != nil { return nil, okOut{}, err } c, err := deps.Caller.Resolve(ctx) if err != nil { return nil, okOut{}, err } p, err := deps.Projects.Get(ctx, c, in.ProjectSlug) if err != nil { return nil, okOut{}, err } if err := CheckProjectFromCtx(ctx, p.ID); err != nil { return nil, okOut{}, err } var assignee *uuid.UUID if in.AssigneeID != nil && *in.AssigneeID != "" { aid, err := uuid.Parse(*in.AssigneeID) if err != nil { return nil, okOut{}, errs.Wrap(err, errs.CodeInvalidInput, "assignee_id parse") } assignee = &aid } if _, err := deps.Issues.Assign(ctx, c, in.ProjectSlug, in.Number, assignee); err != nil { return nil, okOut{}, err } return nil, okOut{OK: true}, nil }) } // ===== create_requirement ===== type createRequirementIn struct { ProjectSlug string `json:"project_slug" jsonschema:"required field"` Title string `json:"title" jsonschema:"title (non-empty)"` Description string `json:"description,omitempty"` } type requirementCreateOut struct { OK bool `json:"ok"` Requirement requirementDetail `json:"requirement"` } func registerCreateRequirement(srv *mcpsdk.Server, deps ServerDeps) { mcpsdk.AddTool(srv, &mcpsdk.Tool{Name: "create_requirement", Description: "Create a new requirement."}, func(ctx context.Context, _ *mcpsdk.CallToolRequest, in createRequirementIn) (*mcpsdk.CallToolResult, requirementCreateOut, error) { if err := CheckToolFromCtx(ctx, "create_requirement"); err != nil { return nil, requirementCreateOut{}, err } c, err := deps.Caller.Resolve(ctx) if err != nil { return nil, requirementCreateOut{}, err } p, err := deps.Projects.Get(ctx, c, in.ProjectSlug) if err != nil { return nil, requirementCreateOut{}, err } if err := CheckProjectFromCtx(ctx, p.ID); err != nil { return nil, requirementCreateOut{}, err } r, err := deps.Reqs.Create(ctx, c, in.ProjectSlug, project.CreateRequirementInput{ Title: in.Title, Description: in.Description, }) if err != nil { return nil, requirementCreateOut{}, err } return nil, requirementCreateOut{OK: true, Requirement: requirementDetail{ Number: r.Number, Title: r.Title, Description: r.Description, Phase: string(r.Phase), Status: string(r.Status), CreatedAt: r.CreatedAt.Format("2006-01-02T15:04:05Z07:00"), }}, nil }) } // ===== update_requirement ===== type updateRequirementIn struct { ProjectSlug string `json:"project_slug" jsonschema:"required field"` Number int `json:"number" jsonschema:"issue number (>=1)"` Title string `json:"title,omitempty"` Description string `json:"description,omitempty"` } func registerUpdateRequirement(srv *mcpsdk.Server, deps ServerDeps) { mcpsdk.AddTool(srv, &mcpsdk.Tool{Name: "update_requirement", Description: "Update an existing requirement."}, func(ctx context.Context, _ *mcpsdk.CallToolRequest, in updateRequirementIn) (*mcpsdk.CallToolResult, requirementCreateOut, error) { if err := CheckToolFromCtx(ctx, "update_requirement"); err != nil { return nil, requirementCreateOut{}, err } c, err := deps.Caller.Resolve(ctx) if err != nil { return nil, requirementCreateOut{}, err } p, err := deps.Projects.Get(ctx, c, in.ProjectSlug) if err != nil { return nil, requirementCreateOut{}, err } if err := CheckProjectFromCtx(ctx, p.ID); err != nil { return nil, requirementCreateOut{}, err } input := project.UpdateRequirementInput{} if in.Title != "" { input.Title = &in.Title } if in.Description != "" { input.Description = &in.Description } r, err := deps.Reqs.Update(ctx, c, in.ProjectSlug, in.Number, input) if err != nil { return nil, requirementCreateOut{}, err } return nil, requirementCreateOut{OK: true, Requirement: requirementDetail{ Number: r.Number, Title: r.Title, Description: r.Description, Phase: string(r.Phase), Status: string(r.Status), CreatedAt: r.CreatedAt.Format("2006-01-02T15:04:05Z07:00"), }}, nil }) } // ===== close/reopen_requirement ===== type closeRequirementIn struct { ProjectSlug string `json:"project_slug" jsonschema:"required field"` Number int `json:"number" jsonschema:"issue number (>=1)"` } func registerCloseRequirement(srv *mcpsdk.Server, deps ServerDeps) { mcpsdk.AddTool(srv, &mcpsdk.Tool{Name: "close_requirement", Description: "Close a requirement (idempotent)."}, func(ctx context.Context, _ *mcpsdk.CallToolRequest, in closeRequirementIn) (*mcpsdk.CallToolResult, okOut, error) { if err := CheckToolFromCtx(ctx, "close_requirement"); err != nil { return nil, okOut{}, err } c, err := deps.Caller.Resolve(ctx) if err != nil { return nil, okOut{}, err } p, err := deps.Projects.Get(ctx, c, in.ProjectSlug) if err != nil { return nil, okOut{}, err } if err := CheckProjectFromCtx(ctx, p.ID); err != nil { return nil, okOut{}, err } if err := deps.Reqs.Close(ctx, c, in.ProjectSlug, in.Number); err != nil { return nil, okOut{}, err } return nil, okOut{OK: true}, nil }) } func registerReopenRequirement(srv *mcpsdk.Server, deps ServerDeps) { mcpsdk.AddTool(srv, &mcpsdk.Tool{Name: "reopen_requirement", Description: "Reopen a closed requirement (idempotent)."}, func(ctx context.Context, _ *mcpsdk.CallToolRequest, in closeRequirementIn) (*mcpsdk.CallToolResult, okOut, error) { if err := CheckToolFromCtx(ctx, "reopen_requirement"); err != nil { return nil, okOut{}, err } c, err := deps.Caller.Resolve(ctx) if err != nil { return nil, okOut{}, err } p, err := deps.Projects.Get(ctx, c, in.ProjectSlug) if err != nil { return nil, okOut{}, err } if err := CheckProjectFromCtx(ctx, p.ID); err != nil { return nil, okOut{}, err } if err := deps.Reqs.Reopen(ctx, c, in.ProjectSlug, in.Number); err != nil { return nil, okOut{}, err } return nil, okOut{OK: true}, nil }) } // ===== set_requirement_phase ===== type setRequirementPhaseIn struct { ProjectSlug string `json:"project_slug" jsonschema:"required field"` Number int `json:"number" jsonschema:"issue number (>=1)"` Phase string `json:"phase" jsonschema:"required field"` } func registerSetRequirementPhase(srv *mcpsdk.Server, deps ServerDeps) { mcpsdk.AddTool(srv, &mcpsdk.Tool{Name: "set_requirement_phase", Description: "Change a requirement's phase. Entry gates are enforced: e.g. moving to 'implementing' requires an approved (verdict=pass) auditing artifact; moving to 'done' requires the workspace PR to be merged. A blocked transition returns phase_gate_failed."}, func(ctx context.Context, _ *mcpsdk.CallToolRequest, in setRequirementPhaseIn) (*mcpsdk.CallToolResult, requirementCreateOut, error) { if err := CheckToolFromCtx(ctx, "set_requirement_phase"); err != nil { return nil, requirementCreateOut{}, err } c, err := deps.Caller.Resolve(ctx) if err != nil { return nil, requirementCreateOut{}, err } p, err := deps.Projects.Get(ctx, c, in.ProjectSlug) if err != nil { return nil, requirementCreateOut{}, err } if err := CheckProjectFromCtx(ctx, p.ID); err != nil { return nil, requirementCreateOut{}, err } r, err := deps.Reqs.ChangePhase(ctx, c, in.ProjectSlug, in.Number, project.Phase(in.Phase)) if err != nil { return nil, requirementCreateOut{}, err } return nil, requirementCreateOut{OK: true, Requirement: requirementDetail{ Number: r.Number, Title: r.Title, Description: r.Description, Phase: string(r.Phase), Status: string(r.Status), CreatedAt: r.CreatedAt.Format("2006-01-02T15:04:05Z07:00"), }}, nil }) } // ===== add_requirement_artifact ===== type addRequirementArtifactIn struct { ProjectSlug string `json:"project_slug" jsonschema:"required field"` Number int `json:"number" jsonschema:"requirement number (>=1)"` Phase string `json:"phase" jsonschema:"artifact phase: planning|prototyping|auditing|implementing|reviewing"` Content string `json:"content" jsonschema:"artifact content (non-empty)"` Note string `json:"note,omitempty"` } type artifactDetail struct { Phase string `json:"phase"` Version int `json:"version"` Content string `json:"content"` Note string `json:"note,omitempty"` CreatedAt string `json:"created_at"` Verdict string `json:"verdict"` Approved bool `json:"approved"` } func artifactDetailFrom(a *project.Artifact) artifactDetail { return artifactDetail{ Phase: string(a.Phase), Version: a.Version, Content: a.Content, Note: a.Note, CreatedAt: a.CreatedAt.Format("2006-01-02T15:04:05Z07:00"), Verdict: string(a.Verdict), Approved: a.Verdict == project.VerdictPass, } } type artifactCreateOut struct { OK bool `json:"ok"` Artifact artifactDetail `json:"artifact"` } func registerAddRequirementArtifact(srv *mcpsdk.Server, deps ServerDeps) { mcpsdk.AddTool(srv, &mcpsdk.Tool{Name: "add_requirement_artifact", Description: "Add a new phase artifact version (planning/prototyping/auditing/implementing/reviewing) to a requirement."}, func(ctx context.Context, _ *mcpsdk.CallToolRequest, in addRequirementArtifactIn) (*mcpsdk.CallToolResult, artifactCreateOut, error) { if err := CheckToolFromCtx(ctx, "add_requirement_artifact"); err != nil { return nil, artifactCreateOut{}, err } c, err := deps.Caller.Resolve(ctx) if err != nil { return nil, artifactCreateOut{}, err } p, err := deps.Projects.Get(ctx, c, in.ProjectSlug) if err != nil { return nil, artifactCreateOut{}, err } if err := CheckProjectFromCtx(ctx, p.ID); err != nil { return nil, artifactCreateOut{}, err } art, err := deps.Artifacts.Create(ctx, c, in.ProjectSlug, in.Number, project.CreateArtifactInput{ Phase: project.Phase(in.Phase), Content: in.Content, Note: in.Note, }) if err != nil { return nil, artifactCreateOut{}, err } return nil, artifactCreateOut{OK: true, Artifact: artifactDetailFrom(art)}, nil }) } // ===== approve_requirement_artifact ===== type approveRequirementArtifactIn struct { ProjectSlug string `json:"project_slug" jsonschema:"required field"` Number int `json:"number" jsonschema:"requirement number (>=1)"` Phase string `json:"phase" jsonschema:"artifact phase: planning|prototyping|auditing|implementing|reviewing"` Version int `json:"version" jsonschema:"artifact version (>=1)"` Verdict string `json:"verdict" jsonschema:"review verdict: pass|fail"` } func registerApproveRequirementArtifact(srv *mcpsdk.Server, deps ServerDeps) { mcpsdk.AddTool(srv, &mcpsdk.Tool{Name: "approve_requirement_artifact", Description: "Set the review verdict (pass/fail) on a requirement artifact version. A 'pass' verdict marks the phase's approved/current artifact, which downstream phase gates require (e.g. an approved auditing artifact unblocks the move to 'implementing')."}, func(ctx context.Context, _ *mcpsdk.CallToolRequest, in approveRequirementArtifactIn) (*mcpsdk.CallToolResult, artifactCreateOut, error) { if err := CheckToolFromCtx(ctx, "approve_requirement_artifact"); err != nil { return nil, artifactCreateOut{}, err } c, err := deps.Caller.Resolve(ctx) if err != nil { return nil, artifactCreateOut{}, err } p, err := deps.Projects.Get(ctx, c, in.ProjectSlug) if err != nil { return nil, artifactCreateOut{}, err } if err := CheckProjectFromCtx(ctx, p.ID); err != nil { return nil, artifactCreateOut{}, err } art, err := deps.Artifacts.Approve(ctx, c, in.ProjectSlug, in.Number, project.Phase(in.Phase), in.Version, project.Verdict(in.Verdict)) if err != nil { return nil, artifactCreateOut{}, err } return nil, artifactCreateOut{OK: true, Artifact: artifactDetailFrom(art)}, nil }) } // ===== list_requirement_artifacts ===== type listRequirementArtifactsIn struct { ProjectSlug string `json:"project_slug" jsonschema:"required field"` Number int `json:"number" jsonschema:"requirement number (>=1)"` Phase string `json:"phase,omitempty" jsonschema:"filter by phase: planning|prototyping|auditing|implementing|reviewing; empty = all"` } type listRequirementArtifactsOut struct { Artifacts []artifactDetail `json:"artifacts"` } func registerListRequirementArtifacts(srv *mcpsdk.Server, deps ServerDeps) { mcpsdk.AddTool(srv, &mcpsdk.Tool{Name: "list_requirement_artifacts", Description: "List phase artifact versions of a requirement."}, func(ctx context.Context, _ *mcpsdk.CallToolRequest, in listRequirementArtifactsIn) (*mcpsdk.CallToolResult, listRequirementArtifactsOut, error) { if err := CheckToolFromCtx(ctx, "list_requirement_artifacts"); err != nil { return nil, listRequirementArtifactsOut{}, err } c, err := deps.Caller.Resolve(ctx) if err != nil { return nil, listRequirementArtifactsOut{}, err } p, err := deps.Projects.Get(ctx, c, in.ProjectSlug) if err != nil { return nil, listRequirementArtifactsOut{}, err } if err := CheckProjectFromCtx(ctx, p.ID); err != nil { return nil, listRequirementArtifactsOut{}, err } arts, err := deps.Artifacts.List(ctx, c, in.ProjectSlug, in.Number, project.Phase(in.Phase)) if err != nil { return nil, listRequirementArtifactsOut{}, err } out := listRequirementArtifactsOut{Artifacts: make([]artifactDetail, 0, len(arts))} for _, art := range arts { out.Artifacts = append(out.Artifacts, artifactDetailFrom(art)) } return nil, out, nil }) } // ===== create_project ===== type createProjectIn struct { Slug string `json:"slug" jsonschema:"project slug (non-empty)"` Name string `json:"name" jsonschema:"display name (non-empty)"` Description string `json:"description,omitempty"` Visibility string `json:"visibility,omitempty" jsonschema:"private|internal (default private)"` } type projectDetail struct { Slug string `json:"slug"` Name string `json:"name"` Description string `json:"description,omitempty"` Visibility string `json:"visibility"` Archived bool `json:"archived"` CreatedAt string `json:"created_at"` } type projectCreateOut struct { OK bool `json:"ok"` Project projectDetail `json:"project"` } func projectDetailFrom(p *project.Project) projectDetail { return projectDetail{ Slug: p.Slug, Name: p.Name, Description: p.Description, Visibility: string(p.Visibility), Archived: p.ArchivedAt != nil, CreatedAt: p.CreatedAt.Format("2006-01-02T15:04:05Z07:00"), } } // checkProjectScopeUnrestricted 用于 create_project:token 若被限定到具体 // project 列表,则不允许创建落在列表外的新项目(新项目 ID 创建前未知, // 只能整体拒绝)。 func checkProjectScopeUnrestricted(ctx context.Context) error { sess, ok := FromContext(ctx) if !ok { return errs.New(errs.CodeInternal, "mcp: AuthSession missing") } if sess.Scope.ProjectIDs != nil { return errs.New(errs.CodeMcpTokenScopeViolation, "token is restricted to specific projects; creating new projects is not allowed") } return nil } func registerCreateProject(srv *mcpsdk.Server, deps ServerDeps) { mcpsdk.AddTool(srv, &mcpsdk.Tool{Name: "create_project", Description: "Create a new project."}, func(ctx context.Context, _ *mcpsdk.CallToolRequest, in createProjectIn) (*mcpsdk.CallToolResult, projectCreateOut, error) { if err := CheckToolFromCtx(ctx, "create_project"); err != nil { return nil, projectCreateOut{}, err } if err := checkProjectScopeUnrestricted(ctx); err != nil { return nil, projectCreateOut{}, err } c, err := deps.Caller.Resolve(ctx) if err != nil { return nil, projectCreateOut{}, err } p, err := deps.Projects.Create(ctx, c, project.CreateProjectInput{ Slug: in.Slug, Name: in.Name, Description: in.Description, Visibility: project.Visibility(in.Visibility), }) if err != nil { return nil, projectCreateOut{}, err } return nil, projectCreateOut{OK: true, Project: projectDetailFrom(p)}, nil }) } // ===== update_project ===== type updateProjectIn struct { ProjectSlug string `json:"project_slug" jsonschema:"required field"` Name string `json:"name,omitempty"` Description string `json:"description,omitempty"` Visibility string `json:"visibility,omitempty" jsonschema:"private|internal"` } func registerUpdateProject(srv *mcpsdk.Server, deps ServerDeps) { mcpsdk.AddTool(srv, &mcpsdk.Tool{Name: "update_project", Description: "Update a project (name/description/visibility)."}, func(ctx context.Context, _ *mcpsdk.CallToolRequest, in updateProjectIn) (*mcpsdk.CallToolResult, projectCreateOut, error) { if err := CheckToolFromCtx(ctx, "update_project"); err != nil { return nil, projectCreateOut{}, err } c, err := deps.Caller.Resolve(ctx) if err != nil { return nil, projectCreateOut{}, err } p, err := deps.Projects.Get(ctx, c, in.ProjectSlug) if err != nil { return nil, projectCreateOut{}, err } if err := CheckProjectFromCtx(ctx, p.ID); err != nil { return nil, projectCreateOut{}, err } input := project.UpdateProjectInput{} if in.Name != "" { input.Name = &in.Name } if in.Description != "" { input.Description = &in.Description } if in.Visibility != "" { v := project.Visibility(in.Visibility) input.Visibility = &v } p, err = deps.Projects.Update(ctx, c, in.ProjectSlug, input) if err != nil { return nil, projectCreateOut{}, err } return nil, projectCreateOut{OK: true, Project: projectDetailFrom(p)}, nil }) } // ===== archive/unarchive_project ===== type archiveProjectIn struct { ProjectSlug string `json:"project_slug" jsonschema:"required field"` } func registerArchiveProject(srv *mcpsdk.Server, deps ServerDeps) { mcpsdk.AddTool(srv, &mcpsdk.Tool{Name: "archive_project", Description: "Archive a project (idempotent)."}, func(ctx context.Context, _ *mcpsdk.CallToolRequest, in archiveProjectIn) (*mcpsdk.CallToolResult, okOut, error) { if err := CheckToolFromCtx(ctx, "archive_project"); err != nil { return nil, okOut{}, err } c, err := deps.Caller.Resolve(ctx) if err != nil { return nil, okOut{}, err } p, err := deps.Projects.Get(ctx, c, in.ProjectSlug) if err != nil { return nil, okOut{}, err } if err := CheckProjectFromCtx(ctx, p.ID); err != nil { return nil, okOut{}, err } if err := deps.Projects.Archive(ctx, c, in.ProjectSlug); err != nil { return nil, okOut{}, err } return nil, okOut{OK: true}, nil }) } func registerUnarchiveProject(srv *mcpsdk.Server, deps ServerDeps) { mcpsdk.AddTool(srv, &mcpsdk.Tool{Name: "unarchive_project", Description: "Unarchive a project (idempotent)."}, func(ctx context.Context, _ *mcpsdk.CallToolRequest, in archiveProjectIn) (*mcpsdk.CallToolResult, okOut, error) { if err := CheckToolFromCtx(ctx, "unarchive_project"); err != nil { return nil, okOut{}, err } c, err := deps.Caller.Resolve(ctx) if err != nil { return nil, okOut{}, err } p, err := deps.Projects.Get(ctx, c, in.ProjectSlug) if err != nil { return nil, okOut{}, err } if err := CheckProjectFromCtx(ctx, p.ID); err != nil { return nil, okOut{}, err } if err := deps.Projects.Unarchive(ctx, c, in.ProjectSlug); err != nil { return nil, okOut{}, err } return nil, okOut{OK: true}, nil }) } // 保证 fmt 引用(编译期检查) var _ = fmt.Sprintf