- 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
455 lines
14 KiB
Go
455 lines
14 KiB
Go
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)
|
|
} |