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) }