You've already forked agentic-coding-workflow
809 lines
27 KiB
Go
809 lines
27 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)
|
|
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)
|
|
registerAddRequirementPrototype(srv, deps)
|
|
registerListRequirementPrototypes(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_prototype =====
|
|
|
|
type addRequirementPrototypeIn struct {
|
|
ProjectSlug string `json:"project_slug" jsonschema:"required field"`
|
|
Number int `json:"number" jsonschema:"requirement number (>=1)"`
|
|
Content string `json:"content" jsonschema:"prototype content (non-empty)"`
|
|
Note string `json:"note,omitempty"`
|
|
}
|
|
type prototypeDetail struct {
|
|
Version int `json:"version"`
|
|
Content string `json:"content"`
|
|
Note string `json:"note,omitempty"`
|
|
CreatedAt string `json:"created_at"`
|
|
}
|
|
type prototypeCreateOut struct {
|
|
OK bool `json:"ok"`
|
|
Prototype prototypeDetail `json:"prototype"`
|
|
}
|
|
|
|
func registerAddRequirementPrototype(srv *mcpsdk.Server, deps ServerDeps) {
|
|
mcpsdk.AddTool(srv,
|
|
&mcpsdk.Tool{Name: "add_requirement_prototype", Description: "Add a new prototype version to a requirement."},
|
|
func(ctx context.Context, _ *mcpsdk.CallToolRequest, in addRequirementPrototypeIn) (*mcpsdk.CallToolResult, prototypeCreateOut, error) {
|
|
if err := CheckToolFromCtx(ctx, "add_requirement_prototype"); err != nil {
|
|
return nil, prototypeCreateOut{}, err
|
|
}
|
|
c, err := deps.Caller.Resolve(ctx)
|
|
if err != nil {
|
|
return nil, prototypeCreateOut{}, err
|
|
}
|
|
p, err := deps.Projects.Get(ctx, c, in.ProjectSlug)
|
|
if err != nil {
|
|
return nil, prototypeCreateOut{}, err
|
|
}
|
|
if err := CheckProjectFromCtx(ctx, p.ID); err != nil {
|
|
return nil, prototypeCreateOut{}, err
|
|
}
|
|
proto, err := deps.Prototypes.Create(ctx, c, in.ProjectSlug, in.Number, project.CreatePrototypeInput{
|
|
Content: in.Content, Note: in.Note,
|
|
})
|
|
if err != nil {
|
|
return nil, prototypeCreateOut{}, err
|
|
}
|
|
return nil, prototypeCreateOut{OK: true, Prototype: prototypeDetail{
|
|
Version: proto.Version, Content: proto.Content, Note: proto.Note,
|
|
CreatedAt: proto.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
|
}}, nil
|
|
})
|
|
}
|
|
|
|
// ===== list_requirement_prototypes =====
|
|
|
|
type listRequirementPrototypesIn struct {
|
|
ProjectSlug string `json:"project_slug" jsonschema:"required field"`
|
|
Number int `json:"number" jsonschema:"requirement number (>=1)"`
|
|
}
|
|
type listRequirementPrototypesOut struct {
|
|
Prototypes []prototypeDetail `json:"prototypes"`
|
|
}
|
|
|
|
func registerListRequirementPrototypes(srv *mcpsdk.Server, deps ServerDeps) {
|
|
mcpsdk.AddTool(srv,
|
|
&mcpsdk.Tool{Name: "list_requirement_prototypes", Description: "List prototype versions of a requirement."},
|
|
func(ctx context.Context, _ *mcpsdk.CallToolRequest, in listRequirementPrototypesIn) (*mcpsdk.CallToolResult, listRequirementPrototypesOut, error) {
|
|
if err := CheckToolFromCtx(ctx, "list_requirement_prototypes"); err != nil {
|
|
return nil, listRequirementPrototypesOut{}, err
|
|
}
|
|
c, err := deps.Caller.Resolve(ctx)
|
|
if err != nil {
|
|
return nil, listRequirementPrototypesOut{}, err
|
|
}
|
|
p, err := deps.Projects.Get(ctx, c, in.ProjectSlug)
|
|
if err != nil {
|
|
return nil, listRequirementPrototypesOut{}, err
|
|
}
|
|
if err := CheckProjectFromCtx(ctx, p.ID); err != nil {
|
|
return nil, listRequirementPrototypesOut{}, err
|
|
}
|
|
protos, err := deps.Prototypes.List(ctx, c, in.ProjectSlug, in.Number)
|
|
if err != nil {
|
|
return nil, listRequirementPrototypesOut{}, err
|
|
}
|
|
out := listRequirementPrototypesOut{Prototypes: make([]prototypeDetail, 0, len(protos))}
|
|
for _, proto := range protos {
|
|
out.Prototypes = append(out.Prototypes, prototypeDetail{
|
|
Version: proto.Version, Content: proto.Content, Note: proto.Note,
|
|
CreatedAt: proto.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
|
})
|
|
}
|
|
return nil, out, nil
|
|
})
|
|
}
|
|
|
|
// 保证 fmt 引用(编译期检查)
|
|
var _ = fmt.Sprintf
|