diff --git a/operation/label/label.go b/operation/label/label.go new file mode 100644 index 0000000..0ec9c48 --- /dev/null +++ b/operation/label/label.go @@ -0,0 +1,455 @@ +package label + +import ( + "context" + "fmt" + + "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 ( + CreateLabelToolName = "create_label" + GetRepoLabelToolName = "get_repo_label" + ListRepoLabelsToolName = "list_repo_labels" + EditLabelToolName = "edit_label" + DeleteLabelToolName = "delete_label" + GetIssueLabelsToolName = "get_issue_labels" + AddIssueLabelsToolName = "add_issue_labels" + RemoveIssueLabelsToolName = "remove_issue_labels" + ReplaceIssueLabelsToolName = "replace_issue_labels" +) + +var ( + CreateLabelTool = mcp.NewTool( + CreateLabelToolName, + mcp.WithDescription("Create label"), + 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("label name")), + mcp.WithString("color", mcp.Required(), mcp.Description("label color in hex format (without #)")), + mcp.WithString("description", mcp.Description("label description")), + ) + + GetRepoLabelTool = mcp.NewTool( + GetRepoLabelToolName, + mcp.WithDescription("Get repository label 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("label ID")), + ) + + ListRepoLabelsTool = mcp.NewTool( + ListRepoLabelsToolName, + mcp.WithDescription("List repository labels"), + mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), + mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), + mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1), mcp.Min(1)), + mcp.WithNumber("limit", mcp.Description("page size"), mcp.DefaultNumber(50), mcp.Min(1)), + ) + + EditLabelTool = mcp.NewTool( + EditLabelToolName, + mcp.WithDescription("Edit label"), + 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("label ID")), + mcp.WithString("name", mcp.Description("label name")), + mcp.WithString("color", mcp.Description("label color in hex format (without #)")), + mcp.WithString("description", mcp.Description("label description")), + ) + + DeleteLabelTool = mcp.NewTool( + DeleteLabelToolName, + mcp.WithDescription("Delete label"), + 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("label ID")), + ) + + GetIssueLabelsTool = mcp.NewTool( + GetIssueLabelsToolName, + mcp.WithDescription("Get labels for an issue"), + mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), + mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), + mcp.WithNumber("index", mcp.Required(), mcp.Description("issue index")), + ) + + AddIssueLabelsTool = mcp.NewTool( + AddIssueLabelsToolName, + mcp.WithDescription("Add labels to an issue"), + mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), + mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), + mcp.WithNumber("index", mcp.Required(), mcp.Description("issue index")), + mcp.WithArray("labels", mcp.Required(), mcp.Description("array of label IDs to add"), mcp.Items(map[string]interface{}{"type": "number"})), + ) + + RemoveIssueLabelsTool = mcp.NewTool( + RemoveIssueLabelsToolName, + mcp.WithDescription("Remove labels from an issue"), + mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), + mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), + mcp.WithNumber("index", mcp.Required(), mcp.Description("issue index")), + mcp.WithArray("labels", mcp.Required(), mcp.Description("array of label IDs to remove"), mcp.Items(map[string]interface{}{"type": "number"})), + ) + + ReplaceIssueLabelsTool = mcp.NewTool( + ReplaceIssueLabelsToolName, + mcp.WithDescription("Replace all labels on an issue"), + mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), + mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), + mcp.WithNumber("index", mcp.Required(), mcp.Description("issue index")), + mcp.WithArray("labels", mcp.Required(), mcp.Description("array of label IDs to set"), mcp.Items(map[string]interface{}{"type": "number"})), + ) +) + +func init() { + Tool.RegisterWrite(server.ServerTool{ + Tool: CreateLabelTool, + Handler: CreateLabelFn, + }) + Tool.RegisterRead(server.ServerTool{ + Tool: GetRepoLabelTool, + Handler: GetRepoLabelFn, + }) + Tool.RegisterRead(server.ServerTool{ + Tool: ListRepoLabelsTool, + Handler: ListRepoLabelsFn, + }) + Tool.RegisterWrite(server.ServerTool{ + Tool: EditLabelTool, + Handler: EditLabelFn, + }) + Tool.RegisterWrite(server.ServerTool{ + Tool: DeleteLabelTool, + Handler: DeleteLabelFn, + }) + Tool.RegisterRead(server.ServerTool{ + Tool: GetIssueLabelsTool, + Handler: GetIssueLabelsFn, + }) + Tool.RegisterWrite(server.ServerTool{ + Tool: AddIssueLabelsTool, + Handler: AddIssueLabelsFn, + }) + Tool.RegisterWrite(server.ServerTool{ + Tool: RemoveIssueLabelsTool, + Handler: RemoveIssueLabelsFn, + }) + Tool.RegisterWrite(server.ServerTool{ + Tool: ReplaceIssueLabelsTool, + Handler: ReplaceIssueLabelsFn, + }) +} + +func CreateLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called CreateLabelFn") + 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")) + } + color, ok := req.GetArguments()["color"].(string) + if !ok { + return to.ErrorResult(fmt.Errorf("color is required")) + } + + opt := gitea_sdk.CreateLabelOption{ + Name: name, + Color: color, + } + + if description, ok := req.GetArguments()["description"].(string); ok { + opt.Description = description + } + + label, _, err := gitea.Client().CreateLabel(owner, repo, opt) + if err != nil { + return to.ErrorResult(fmt.Errorf("create %v/%v/label err: %v", owner, repo, err)) + } + + return to.TextResult(label) +} + +func GetRepoLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called GetRepoLabelFn") + 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")) + } + + label, _, err := gitea.Client().GetRepoLabel(owner, repo, int64(id)) + if err != nil { + return to.ErrorResult(fmt.Errorf("get %v/%v/label/%v err: %v", owner, repo, int64(id), err)) + } + + return to.TextResult(label) +} + +func ListRepoLabelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called ListRepoLabelsFn") + 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")) + } + + page, ok := req.GetArguments()["page"].(float64) + if !ok { + page = 1 + } + limit, ok := req.GetArguments()["limit"].(float64) + if !ok { + limit = 50 + } + + opt := gitea_sdk.ListLabelsOptions{ + ListOptions: gitea_sdk.ListOptions{ + Page: int(page), + PageSize: int(limit), + }, + } + + labels, _, err := gitea.Client().ListRepoLabels(owner, repo, opt) + if err != nil { + return to.ErrorResult(fmt.Errorf("get %v/%v/labels err: %v", owner, repo, err)) + } + + return to.TextResult(labels) +} + +func EditLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called EditLabelFn") + 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.EditLabelOption{} + + if name, ok := req.GetArguments()["name"].(string); ok { + opt.Name = &name + } + if color, ok := req.GetArguments()["color"].(string); ok { + opt.Color = &color + } + if description, ok := req.GetArguments()["description"].(string); ok { + opt.Description = &description + } + + label, _, err := gitea.Client().EditLabel(owner, repo, int64(id), opt) + if err != nil { + return to.ErrorResult(fmt.Errorf("edit %v/%v/label/%v err: %v", owner, repo, int64(id), err)) + } + + return to.TextResult(label) +} + +func DeleteLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called DeleteLabelFn") + 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().DeleteLabel(owner, repo, int64(id)) + if err != nil { + return to.ErrorResult(fmt.Errorf("delete %v/%v/label/%v err: %v", owner, repo, int64(id), err)) + } + + return to.TextResult(map[string]interface{}{ + "success": true, + "message": fmt.Sprintf("Label %d deleted successfully", int64(id)), + }) +} + +func GetIssueLabelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called GetIssueLabelsFn") + 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")) + } + index, ok := req.GetArguments()["index"].(float64) + if !ok { + return to.ErrorResult(fmt.Errorf("index is required")) + } + + labels, _, err := gitea.Client().GetIssueLabels(owner, repo, int64(index), gitea_sdk.ListLabelsOptions{}) + if err != nil { + return to.ErrorResult(fmt.Errorf("get %v/%v/issues/%v/labels err: %v", owner, repo, int64(index), err)) + } + + return to.TextResult(labels) +} + +func AddIssueLabelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called AddIssueLabelsFn") + 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")) + } + index, ok := req.GetArguments()["index"].(float64) + if !ok { + return to.ErrorResult(fmt.Errorf("index is required")) + } + labelsData, ok := req.GetArguments()["labels"].([]interface{}) + if !ok { + return to.ErrorResult(fmt.Errorf("labels is required")) + } + + labelIDs := make([]int64, len(labelsData)) + for i, labelData := range labelsData { + if labelID, ok := labelData.(float64); ok { + labelIDs[i] = int64(labelID) + } else { + return to.ErrorResult(fmt.Errorf("invalid label ID at index %d", i)) + } + } + + opt := gitea_sdk.IssueLabelsOption{ + Labels: labelIDs, + } + + labels, _, err := gitea.Client().AddIssueLabels(owner, repo, int64(index), opt) + if err != nil { + return to.ErrorResult(fmt.Errorf("add labels to %v/%v/issues/%v err: %v", owner, repo, int64(index), err)) + } + + return to.TextResult(labels) +} + +func RemoveIssueLabelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called RemoveIssueLabelsFn") + 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")) + } + index, ok := req.GetArguments()["index"].(float64) + if !ok { + return to.ErrorResult(fmt.Errorf("index is required")) + } + labelsData, ok := req.GetArguments()["labels"].([]interface{}) + if !ok { + return to.ErrorResult(fmt.Errorf("labels is required")) + } + + labelIDs := make([]int64, len(labelsData)) + for i, labelData := range labelsData { + if labelID, ok := labelData.(float64); ok { + labelIDs[i] = int64(labelID) + } else { + return to.ErrorResult(fmt.Errorf("invalid label ID at index %d", i)) + } + } + + var errors []string + for _, labelID := range labelIDs { + _, err := gitea.Client().DeleteIssueLabel(owner, repo, int64(index), labelID) + if err != nil { + errors = append(errors, fmt.Sprintf("failed to remove label %d: %v", labelID, err)) + } + } + + if len(errors) > 0 { + return to.ErrorResult(fmt.Errorf("remove labels from %v/%v/issues/%v errors: %v", owner, repo, int64(index), errors)) + } + + return to.TextResult(map[string]interface{}{ + "success": true, + "message": fmt.Sprintf("Labels removed from issue %d successfully", int64(index)), + }) +} + +func ReplaceIssueLabelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called ReplaceIssueLabelsFn") + 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")) + } + index, ok := req.GetArguments()["index"].(float64) + if !ok { + return to.ErrorResult(fmt.Errorf("index is required")) + } + labelsData, ok := req.GetArguments()["labels"].([]interface{}) + if !ok { + return to.ErrorResult(fmt.Errorf("labels is required")) + } + + labelIDs := make([]int64, len(labelsData)) + for i, labelData := range labelsData { + if labelID, ok := labelData.(float64); ok { + labelIDs[i] = int64(labelID) + } else { + return to.ErrorResult(fmt.Errorf("invalid label ID at index %d", i)) + } + } + + opt := gitea_sdk.IssueLabelsOption{ + Labels: labelIDs, + } + + labels, _, err := gitea.Client().ReplaceIssueLabels(owner, repo, int64(index), opt) + if err != nil { + return to.ErrorResult(fmt.Errorf("replace labels on %v/%v/issues/%v err: %v", owner, repo, int64(index), err)) + } + + return to.TextResult(labels) +} \ No newline at end of file diff --git a/operation/milestone/milestone.go b/operation/milestone/milestone.go new file mode 100644 index 0000000..12a9c3d --- /dev/null +++ b/operation/milestone/milestone.go @@ -0,0 +1,402 @@ +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), + }) +} \ No newline at end of file diff --git a/operation/operation.go b/operation/operation.go index ff08c77..e0938c6 100644 --- a/operation/operation.go +++ b/operation/operation.go @@ -4,6 +4,8 @@ import ( "fmt" "gitea.com/gitea/gitea-mcp/operation/issue" + "gitea.com/gitea/gitea-mcp/operation/label" + "gitea.com/gitea/gitea-mcp/operation/milestone" "gitea.com/gitea/gitea-mcp/operation/pull" "gitea.com/gitea/gitea-mcp/operation/repo" "gitea.com/gitea/gitea-mcp/operation/search" @@ -30,6 +32,12 @@ func RegisterTool(s *server.MCPServer) { // Pull Tool s.AddTools(pull.Tool.Tools()...) + // Milestone Tool + s.AddTools(milestone.Tool.Tools()...) + + // Label Tool + s.AddTools(label.Tool.Tools()...) + // Search Tool s.AddTools(search.Tool.Tools()...)