Files
agentic-coding-workflow/internal/mcp/tools_pm.go
T
2026-06-11 08:21:17 +08:00

987 lines
33 KiB
Go

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)
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)
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
})
}
// ===== 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."},
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"`
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"`
}
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) 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: artifactDetail{
Phase: string(art.Phase), Version: art.Version, Content: art.Content, Note: art.Note,
CreatedAt: art.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
}}, 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; 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, artifactDetail{
Phase: string(art.Phase), Version: art.Version, Content: art.Content, Note: art.Note,
CreatedAt: art.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
})
}
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