package milestone import ( "context" "fmt" "time" "gitea.com/gitea/gitea-mcp/pkg/gitea" "gitea.com/gitea/gitea-mcp/pkg/log" "gitea.com/gitea/gitea-mcp/pkg/to" "gitea.com/gitea/gitea-mcp/pkg/tool" gitea_sdk "code.gitea.io/sdk/gitea" "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" ) var Tool = tool.New() const ( CreateMilestoneToolName = "create_milestone" GetMilestoneToolName = "get_milestone" GetMilestoneByNameToolName = "get_milestone_by_name" ListRepoMilestonesToolName = "list_repo_milestones" EditMilestoneToolName = "edit_milestone" EditMilestoneByNameToolName = "edit_milestone_by_name" DeleteMilestoneToolName = "delete_milestone" DeleteMilestoneByNameToolName = "delete_milestone_by_name" ) var ( CreateMilestoneTool = mcp.NewTool( CreateMilestoneToolName, mcp.WithDescription("Create milestone"), mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), mcp.WithString("title", mcp.Required(), mcp.Description("milestone title")), mcp.WithString("description", mcp.Description("milestone description")), mcp.WithString("due_date", mcp.Description("milestone due date in RFC3339 format")), mcp.WithString("state", mcp.Description("milestone state (open or closed)"), mcp.DefaultString("open")), ) GetMilestoneTool = mcp.NewTool( GetMilestoneToolName, mcp.WithDescription("Get milestone by ID"), mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), mcp.WithNumber("id", mcp.Required(), mcp.Description("milestone ID")), ) GetMilestoneByNameTool = mcp.NewTool( GetMilestoneByNameToolName, mcp.WithDescription("Get milestone by name"), mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), mcp.WithString("name", mcp.Required(), mcp.Description("milestone name")), ) ListRepoMilestonesTool = mcp.NewTool( ListRepoMilestonesToolName, mcp.WithDescription("List repository milestones"), mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), mcp.WithString("state", mcp.Description("milestone state (open, closed, all)"), mcp.DefaultString("open")), mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1), mcp.Min(1)), mcp.WithNumber("limit", mcp.Description("page size"), mcp.DefaultNumber(10), mcp.Min(1)), ) EditMilestoneTool = mcp.NewTool( EditMilestoneToolName, mcp.WithDescription("Edit milestone by ID"), mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), mcp.WithNumber("id", mcp.Required(), mcp.Description("milestone ID")), mcp.WithString("title", mcp.Description("milestone title")), mcp.WithString("description", mcp.Description("milestone description")), mcp.WithString("due_date", mcp.Description("milestone due date in RFC3339 format")), mcp.WithString("state", mcp.Description("milestone state (open or closed)")), ) EditMilestoneByNameTool = mcp.NewTool( EditMilestoneByNameToolName, mcp.WithDescription("Edit milestone by name"), mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), mcp.WithString("name", mcp.Required(), mcp.Description("milestone name")), mcp.WithString("title", mcp.Description("milestone title")), mcp.WithString("description", mcp.Description("milestone description")), mcp.WithString("due_date", mcp.Description("milestone due date in RFC3339 format")), mcp.WithString("state", mcp.Description("milestone state (open or closed)")), ) DeleteMilestoneTool = mcp.NewTool( DeleteMilestoneToolName, mcp.WithDescription("Delete milestone by ID"), mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), mcp.WithNumber("id", mcp.Required(), mcp.Description("milestone ID")), ) DeleteMilestoneByNameTool = mcp.NewTool( DeleteMilestoneByNameToolName, mcp.WithDescription("Delete milestone by name"), mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), mcp.WithString("name", mcp.Required(), mcp.Description("milestone name")), ) ) func init() { Tool.RegisterWrite(server.ServerTool{ Tool: CreateMilestoneTool, Handler: CreateMilestoneFn, }) Tool.RegisterRead(server.ServerTool{ Tool: GetMilestoneTool, Handler: GetMilestoneFn, }) Tool.RegisterRead(server.ServerTool{ Tool: GetMilestoneByNameTool, Handler: GetMilestoneByNameFn, }) Tool.RegisterRead(server.ServerTool{ Tool: ListRepoMilestonesTool, Handler: ListRepoMilestonesFn, }) Tool.RegisterWrite(server.ServerTool{ Tool: EditMilestoneTool, Handler: EditMilestoneFn, }) Tool.RegisterWrite(server.ServerTool{ Tool: EditMilestoneByNameTool, Handler: EditMilestoneByNameFn, }) Tool.RegisterWrite(server.ServerTool{ Tool: DeleteMilestoneTool, Handler: DeleteMilestoneFn, }) Tool.RegisterWrite(server.ServerTool{ Tool: DeleteMilestoneByNameTool, Handler: DeleteMilestoneByNameFn, }) } func CreateMilestoneFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called CreateMilestoneFn") owner, ok := req.GetArguments()["owner"].(string) if !ok { return to.ErrorResult(fmt.Errorf("owner is required")) } repo, ok := req.GetArguments()["repo"].(string) if !ok { return to.ErrorResult(fmt.Errorf("repo is required")) } title, ok := req.GetArguments()["title"].(string) if !ok { return to.ErrorResult(fmt.Errorf("title is required")) } opt := gitea_sdk.CreateMilestoneOption{ Title: title, } if description, ok := req.GetArguments()["description"].(string); ok { opt.Description = description } if dueDate, ok := req.GetArguments()["due_date"].(string); ok && dueDate != "" { if parsedTime, err := time.Parse(time.RFC3339, dueDate); err == nil { opt.Deadline = &parsedTime } } if state, ok := req.GetArguments()["state"].(string); ok { opt.State = gitea_sdk.StateType(state) } milestone, _, err := gitea.Client().CreateMilestone(owner, repo, opt) if err != nil { return to.ErrorResult(fmt.Errorf("create %v/%v/milestone err: %v", owner, repo, err)) } return to.TextResult(milestone) } func GetMilestoneFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called GetMilestoneFn") owner, ok := req.GetArguments()["owner"].(string) if !ok { return to.ErrorResult(fmt.Errorf("owner is required")) } repo, ok := req.GetArguments()["repo"].(string) if !ok { return to.ErrorResult(fmt.Errorf("repo is required")) } id, ok := req.GetArguments()["id"].(float64) if !ok { return to.ErrorResult(fmt.Errorf("id is required")) } milestone, _, err := gitea.Client().GetMilestone(owner, repo, int64(id)) if err != nil { return to.ErrorResult(fmt.Errorf("get %v/%v/milestone/%v err: %v", owner, repo, int64(id), err)) } return to.TextResult(milestone) } func GetMilestoneByNameFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called GetMilestoneByNameFn") owner, ok := req.GetArguments()["owner"].(string) if !ok { return to.ErrorResult(fmt.Errorf("owner is required")) } repo, ok := req.GetArguments()["repo"].(string) if !ok { return to.ErrorResult(fmt.Errorf("repo is required")) } name, ok := req.GetArguments()["name"].(string) if !ok { return to.ErrorResult(fmt.Errorf("name is required")) } milestone, _, err := gitea.Client().GetMilestoneByName(owner, repo, name) if err != nil { return to.ErrorResult(fmt.Errorf("get %v/%v/milestone/%v err: %v", owner, repo, name, err)) } return to.TextResult(milestone) } func ListRepoMilestonesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called ListRepoMilestonesFn") owner, ok := req.GetArguments()["owner"].(string) if !ok { return to.ErrorResult(fmt.Errorf("owner is required")) } repo, ok := req.GetArguments()["repo"].(string) if !ok { return to.ErrorResult(fmt.Errorf("repo is required")) } state, ok := req.GetArguments()["state"].(string) if !ok { state = "open" } page, ok := req.GetArguments()["page"].(float64) if !ok { page = 1 } limit, ok := req.GetArguments()["limit"].(float64) if !ok { limit = 10 } opt := gitea_sdk.ListMilestoneOption{ State: gitea_sdk.StateType(state), ListOptions: gitea_sdk.ListOptions{ Page: int(page), PageSize: int(limit), }, } milestones, _, err := gitea.Client().ListRepoMilestones(owner, repo, opt) if err != nil { return to.ErrorResult(fmt.Errorf("get %v/%v/milestones err: %v", owner, repo, err)) } return to.TextResult(milestones) } func EditMilestoneFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called EditMilestoneFn") owner, ok := req.GetArguments()["owner"].(string) if !ok { return to.ErrorResult(fmt.Errorf("owner is required")) } repo, ok := req.GetArguments()["repo"].(string) if !ok { return to.ErrorResult(fmt.Errorf("repo is required")) } id, ok := req.GetArguments()["id"].(float64) if !ok { return to.ErrorResult(fmt.Errorf("id is required")) } opt := gitea_sdk.EditMilestoneOption{} if title, ok := req.GetArguments()["title"].(string); ok { opt.Title = title } if description, ok := req.GetArguments()["description"].(string); ok { opt.Description = &description } if dueDate, ok := req.GetArguments()["due_date"].(string); ok { if parsedTime, err := time.Parse(time.RFC3339, dueDate); err == nil { opt.Deadline = &parsedTime } } if state, ok := req.GetArguments()["state"].(string); ok { stateType := gitea_sdk.StateType(state) opt.State = &stateType } milestone, _, err := gitea.Client().EditMilestone(owner, repo, int64(id), opt) if err != nil { return to.ErrorResult(fmt.Errorf("edit %v/%v/milestone/%v err: %v", owner, repo, int64(id), err)) } return to.TextResult(milestone) } func EditMilestoneByNameFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called EditMilestoneByNameFn") owner, ok := req.GetArguments()["owner"].(string) if !ok { return to.ErrorResult(fmt.Errorf("owner is required")) } repo, ok := req.GetArguments()["repo"].(string) if !ok { return to.ErrorResult(fmt.Errorf("repo is required")) } name, ok := req.GetArguments()["name"].(string) if !ok { return to.ErrorResult(fmt.Errorf("name is required")) } opt := gitea_sdk.EditMilestoneOption{} if title, ok := req.GetArguments()["title"].(string); ok { opt.Title = title } if description, ok := req.GetArguments()["description"].(string); ok { opt.Description = &description } if dueDate, ok := req.GetArguments()["due_date"].(string); ok { if parsedTime, err := time.Parse(time.RFC3339, dueDate); err == nil { opt.Deadline = &parsedTime } } if state, ok := req.GetArguments()["state"].(string); ok { stateType := gitea_sdk.StateType(state) opt.State = &stateType } milestone, _, err := gitea.Client().EditMilestoneByName(owner, repo, name, opt) if err != nil { return to.ErrorResult(fmt.Errorf("edit %v/%v/milestone/%v err: %v", owner, repo, name, err)) } return to.TextResult(milestone) } func DeleteMilestoneFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called DeleteMilestoneFn") owner, ok := req.GetArguments()["owner"].(string) if !ok { return to.ErrorResult(fmt.Errorf("owner is required")) } repo, ok := req.GetArguments()["repo"].(string) if !ok { return to.ErrorResult(fmt.Errorf("repo is required")) } id, ok := req.GetArguments()["id"].(float64) if !ok { return to.ErrorResult(fmt.Errorf("id is required")) } _, err := gitea.Client().DeleteMilestone(owner, repo, int64(id)) if err != nil { return to.ErrorResult(fmt.Errorf("delete %v/%v/milestone/%v err: %v", owner, repo, int64(id), err)) } return to.TextResult(map[string]interface{}{ "success": true, "message": fmt.Sprintf("Milestone %d deleted successfully", int64(id)), }) } func DeleteMilestoneByNameFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called DeleteMilestoneByNameFn") owner, ok := req.GetArguments()["owner"].(string) if !ok { return to.ErrorResult(fmt.Errorf("owner is required")) } repo, ok := req.GetArguments()["repo"].(string) if !ok { return to.ErrorResult(fmt.Errorf("repo is required")) } name, ok := req.GetArguments()["name"].(string) if !ok { return to.ErrorResult(fmt.Errorf("name is required")) } _, err := gitea.Client().DeleteMilestoneByName(owner, repo, name) if err != nil { return to.ErrorResult(fmt.Errorf("delete %v/%v/milestone/%v err: %v", owner, repo, name, err)) } return to.TextResult(map[string]interface{}{ "success": true, "message": fmt.Sprintf("Milestone '%s' deleted successfully", name), }) }