From f6b45fdf6eae5f03cbae84331be3d52e9e77c7c8 Mon Sep 17 00:00:00 2001 From: Daniel Date: Mon, 20 Oct 2025 01:43:39 +0000 Subject: [PATCH] feat(org): add MCP tools for organization-level labels (list/create/edit/delete) (#99) (#102) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR adds full support for managing organization-level labels via MCP. It uses the newly added SDK APIs now available on main branch, see https://gitea.com/gitea/go-sdk/issues/732. Registers following tools under label module and wires them into the MCP server as read/write tools: - list_org_labels: list labels defined at the organization level (pagination supported) - create_org_label: create a label in an organization (name, color, description, exclusive) - edit_org_label: edit an organization label (name, color, description, exclusive) - delete_org_label: delete an organization label by ID Dependency note: go.mod/go.sum updated to use the SDK main branch pseudo-version that includes the org-label APIs. If you prefer to merge only after a tagged SDK release, I can bump the dependency to the new tag as soon as it’s available. Thanks for considering! Reviewed-on: https://gitea.com/gitea/gitea-mcp/pulls/102 Reviewed-by: Bo-Yi Wu (吳柏毅) Co-authored-by: Daniel Co-committed-by: Daniel --- README.md | 4 + go.mod | 2 +- go.sum | 4 +- operation/label/label.go | 182 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 189 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index a869551..b94d6d2 100644 --- a/README.md +++ b/README.md @@ -201,6 +201,10 @@ The Gitea MCP Server supports the following tools: | create_pull_request | Pull Request | Create a new pull request | | search_users | User | Search for users | | search_org_teams | Organization | Search for teams in an organization | +| list_org_labels | Organization | List labels defined at organization level | +| create_org_label | Organization | Create a label in an organization | +| edit_org_label | Organization | Edit a label in an organization | +| delete_org_label | Organization | Delete a label in an organization | | search_repos | Repository | Search for repositories | | get_gitea_mcp_server_version | Server | Get the version of the Gitea MCP Server | | list_wiki_pages | Wiki | List all wiki pages in a repository | diff --git a/go.mod b/go.mod index 3908443..35f2431 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module gitea.com/gitea/gitea-mcp go 1.24.0 require ( - code.gitea.io/sdk/gitea v0.22.0 + code.gitea.io/sdk/gitea v0.22.1-0.20251016220613-060554f46291 github.com/mark3labs/mcp-go v0.40.0 go.uber.org/zap v1.27.0 gopkg.in/natefinch/lumberjack.v2 v2.2.1 diff --git a/go.sum b/go.sum index c84befe..5db71d9 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -code.gitea.io/sdk/gitea v0.22.0 h1:HCKq7bX/HQ85Nw7c/HAhWgRye+vBp5nQOE8Md1+9Ef0= -code.gitea.io/sdk/gitea v0.22.0/go.mod h1:yyF5+GhljqvA30sRDreoyHILruNiy4ASufugzYg0VHM= +code.gitea.io/sdk/gitea v0.22.1-0.20251016220613-060554f46291 h1:oVIBPpbLraXywuuapimBbI3uMRGF0qJP3wjU2vb2bU4= +code.gitea.io/sdk/gitea v0.22.1-0.20251016220613-060554f46291/go.mod h1:yyF5+GhljqvA30sRDreoyHILruNiy4ASufugzYg0VHM= github.com/42wim/httpsig v1.2.3 h1:xb0YyWhkYj57SPtfSttIobJUPJZB9as1nsfo7KWVcEs= github.com/42wim/httpsig v1.2.3/go.mod h1:nZq9OlYKDrUBhptd77IHx4/sZZD+IxTBADvAPI9G/EM= github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= diff --git a/operation/label/label.go b/operation/label/label.go index 0954508..e6bbbb8 100644 --- a/operation/label/label.go +++ b/operation/label/label.go @@ -27,6 +27,10 @@ const ( ReplaceIssueLabelsToolName = "replace_issue_labels" ClearIssueLabelsToolName = "clear_issue_labels" RemoveIssueLabelToolName = "remove_issue_label" + ListOrgLabelsToolName = "list_org_labels" + CreateOrgLabelToolName = "create_org_label" + EditOrgLabelToolName = "edit_org_label" + DeleteOrgLabelToolName = "delete_org_label" ) var ( @@ -110,6 +114,43 @@ var ( mcp.WithNumber("index", mcp.Required(), mcp.Description("issue index")), mcp.WithNumber("label_id", mcp.Required(), mcp.Description("label ID to remove")), ) + + ListOrgLabelsTool = mcp.NewTool( + ListOrgLabelsToolName, + mcp.WithDescription("Lists labels defined at organization level"), + mcp.WithString("org", mcp.Required(), mcp.Description("organization name")), + mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)), + mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)), + ) + + CreateOrgLabelTool = mcp.NewTool( + CreateOrgLabelToolName, + mcp.WithDescription("Creates a new label for an organization"), + mcp.WithString("org", mcp.Required(), mcp.Description("organization name")), + mcp.WithString("name", mcp.Required(), mcp.Description("label name")), + mcp.WithString("color", mcp.Required(), mcp.Description("label color (hex code, e.g., #RRGGBB)")), + mcp.WithString("description", mcp.Description("label description")), + mcp.WithBoolean("exclusive", mcp.Description("whether the label is exclusive"), mcp.DefaultBool(false)), + ) + + EditOrgLabelTool = mcp.NewTool( + EditOrgLabelToolName, + mcp.WithDescription("Edits an existing organization label"), + mcp.WithString("org", mcp.Required(), mcp.Description("organization name")), + mcp.WithNumber("id", mcp.Required(), mcp.Description("label ID")), + mcp.WithString("name", mcp.Description("new label name")), + mcp.WithString("color", mcp.Description("new label color (hex code, e.g., #RRGGBB)")), + mcp.WithString("description", mcp.Description("new label description")), + mcp.WithBoolean("exclusive", mcp.Description("whether the label is exclusive")), + ) + + DeleteOrgLabelTool = mcp.NewTool( + DeleteOrgLabelToolName, + mcp.WithDescription("Deletes an organization label by ID"), + mcp.WithString("org", mcp.Required(), mcp.Description("organization name")), + mcp.WithNumber("id", mcp.Required(), mcp.Description("label ID")), + ) + ) func init() { @@ -149,6 +190,22 @@ func init() { Tool: RemoveIssueLabelTool, Handler: RemoveIssueLabelFn, }) + Tool.RegisterRead(server.ServerTool{ + Tool: ListOrgLabelsTool, + Handler: ListOrgLabelsFn, + }) + Tool.RegisterWrite(server.ServerTool{ + Tool: CreateOrgLabelTool, + Handler: CreateOrgLabelFn, + }) + Tool.RegisterWrite(server.ServerTool{ + Tool: EditOrgLabelTool, + Handler: EditOrgLabelFn, + }) + Tool.RegisterWrite(server.ServerTool{ + Tool: DeleteOrgLabelTool, + Handler: DeleteOrgLabelFn, + }) } func ListRepoLabelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { @@ -452,3 +509,128 @@ func RemoveIssueLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.Call } return to.TextResult("Label removed successfully") } + +func ListOrgLabelsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called ListOrgLabelsFn") + org, ok := req.GetArguments()["org"].(string) + if !ok { + return to.ErrorResult(fmt.Errorf("org is required")) + } + page, ok := req.GetArguments()["page"].(float64) + if !ok { + page = 1 + } + pageSize, ok := req.GetArguments()["pageSize"].(float64) + if !ok { + pageSize = 100 + } + + opt := gitea_sdk.ListOrgLabelsOptions{ + ListOptions: gitea_sdk.ListOptions{ + Page: int(page), + PageSize: int(pageSize), + }, + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + labels, _, err := client.ListOrgLabels(org, opt) + if err != nil { + return to.ErrorResult(fmt.Errorf("list %v/labels err: %v", org, err)) + } + return to.TextResult(labels) +} + +func CreateOrgLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called CreateOrgLabelFn") + org, ok := req.GetArguments()["org"].(string) + if !ok { + return to.ErrorResult(fmt.Errorf("org 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")) + } + description, _ := req.GetArguments()["description"].(string) + exclusive, _ := req.GetArguments()["exclusive"].(bool) + + opt := gitea_sdk.CreateOrgLabelOption{ + Name: name, + Color: color, + Description: description, + Exclusive: exclusive, + } + + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + label, _, err := client.CreateOrgLabel(org, opt) + if err != nil { + return to.ErrorResult(fmt.Errorf("create %v/labels err: %v", org, err)) + } + return to.TextResult(label) +} + +func EditOrgLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called EditOrgLabelFn") + org, ok := req.GetArguments()["org"].(string) + if !ok { + return to.ErrorResult(fmt.Errorf("org is required")) + } + id, ok := req.GetArguments()["id"].(float64) + if !ok { + return to.ErrorResult(fmt.Errorf("label ID is required")) + } + + opt := gitea_sdk.EditOrgLabelOption{} + if name, ok := req.GetArguments()["name"].(string); ok { + opt.Name = ptr.To(name) + } + if color, ok := req.GetArguments()["color"].(string); ok { + opt.Color = ptr.To(color) + } + if description, ok := req.GetArguments()["description"].(string); ok { + opt.Description = ptr.To(description) + } + if exclusive, ok := req.GetArguments()["exclusive"].(bool); ok { + opt.Exclusive = ptr.To(exclusive) + } + + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + label, _, err := client.EditOrgLabel(org, int64(id), opt) + if err != nil { + return to.ErrorResult(fmt.Errorf("edit %v/labels/%v err: %v", org, int64(id), err)) + } + return to.TextResult(label) +} + +func DeleteOrgLabelFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called DeleteOrgLabelFn") + org, ok := req.GetArguments()["org"].(string) + if !ok { + return to.ErrorResult(fmt.Errorf("org is required")) + } + id, ok := req.GetArguments()["id"].(float64) + if !ok { + return to.ErrorResult(fmt.Errorf("label ID is required")) + } + + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + _, err = client.DeleteOrgLabel(org, int64(id)) + if err != nil { + return to.ErrorResult(fmt.Errorf("delete %v/labels/%v err: %v", org, int64(id), err)) + } + return to.TextResult("Label deleted successfully") +} \ No newline at end of file