feat: add milestone and label management functionality (#39)
- Add comprehensive milestone CRUD operations (8 tools) - create_milestone, get_milestone, get_milestone_by_name - list_repo_milestones, edit_milestone, edit_milestone_by_name - delete_milestone, delete_milestone_by_name - Add comprehensive label CRUD operations (9 tools) - create_label, get_repo_label, list_repo_labels - edit_label, delete_label, get_issue_labels - add_issue_labels, remove_issue_labels, replace_issue_labels - Update README.md with new tool documentation - Integrate with existing gitea-mcp architecture - Support read-only mode and proper error handling
This commit is contained in:
455
operation/label/label.go
Normal file
455
operation/label/label.go
Normal file
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user