package repo import ( "context" "errors" "fmt" "git.lethalbits.com/lethalbits/gitea-mcp/pkg/gitea" "git.lethalbits.com/lethalbits/gitea-mcp/pkg/log" "git.lethalbits.com/lethalbits/gitea-mcp/pkg/params" "git.lethalbits.com/lethalbits/gitea-mcp/pkg/to" gitea_sdk "code.gitea.io/sdk/gitea" "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" ) const ( ListBranchProtectionsToolName = "list_branch_protections" GetBranchProtectionToolName = "get_branch_protection" CreateBranchProtectionToolName = "create_branch_protection" EditBranchProtectionToolName = "edit_branch_protection" DeleteBranchProtectionToolName = "delete_branch_protection" ) var ( ListBranchProtectionsTool = mcp.NewTool( ListBranchProtectionsToolName, mcp.WithDescription("List branch protections for a repository"), 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.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)), ) GetBranchProtectionTool = mcp.NewTool( GetBranchProtectionToolName, mcp.WithDescription("Get a branch protection rule 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("branch protection rule name")), ) CreateBranchProtectionTool = mcp.NewTool( CreateBranchProtectionToolName, mcp.WithDescription("Create a branch protection rule"), mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), mcp.WithString("rule_name", mcp.Required(), mcp.Description("rule name (glob pattern for branch matching)")), mcp.WithBoolean("enable_push", mcp.Description("enable push to protected branch")), mcp.WithBoolean("enable_merge_whitelist", mcp.Description("enable merge whitelist")), mcp.WithBoolean("enable_status_check", mcp.Description("enable status check")), mcp.WithNumber("required_approvals", mcp.Description("number of required approvals")), mcp.WithBoolean("block_on_rejected_reviews", mcp.Description("block merge on rejected reviews")), mcp.WithBoolean("dismiss_stale_approvals", mcp.Description("dismiss stale approvals on new commits")), ) EditBranchProtectionTool = mcp.NewTool( EditBranchProtectionToolName, mcp.WithDescription("Edit a branch protection rule"), 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("branch protection rule name")), mcp.WithBoolean("enable_push", mcp.Description("enable push to protected branch")), mcp.WithBoolean("enable_merge_whitelist", mcp.Description("enable merge whitelist")), mcp.WithBoolean("enable_status_check", mcp.Description("enable status check")), mcp.WithNumber("required_approvals", mcp.Description("number of required approvals")), mcp.WithBoolean("block_on_rejected_reviews", mcp.Description("block merge on rejected reviews")), mcp.WithBoolean("dismiss_stale_approvals", mcp.Description("dismiss stale approvals on new commits")), ) DeleteBranchProtectionTool = mcp.NewTool( DeleteBranchProtectionToolName, mcp.WithDescription("Delete a branch protection rule"), 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("branch protection rule name")), ) ) func init() { Tool.RegisterRead(server.ServerTool{Tool: ListBranchProtectionsTool, Handler: ListBranchProtectionsFn}) Tool.RegisterRead(server.ServerTool{Tool: GetBranchProtectionTool, Handler: GetBranchProtectionFn}) Tool.RegisterWrite(server.ServerTool{Tool: CreateBranchProtectionTool, Handler: CreateBranchProtectionFn}) Tool.RegisterWrite(server.ServerTool{Tool: EditBranchProtectionTool, Handler: EditBranchProtectionFn}) Tool.RegisterWrite(server.ServerTool{Tool: DeleteBranchProtectionTool, Handler: DeleteBranchProtectionFn}) } func ListBranchProtectionsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called ListBranchProtectionsFn") owner, _ := req.GetArguments()["owner"].(string) repo, _ := req.GetArguments()["repo"].(string) if owner == "" || repo == "" { return to.ErrorResult(errors.New("owner and repo are required")) } page := params.GetOptionalInt(req.GetArguments(), "page", 1) pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100) client, err := gitea.ClientFromContext(ctx) if err != nil { return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) } bps, _, err := client.ListBranchProtections(owner, repo, gitea_sdk.ListBranchProtectionsOptions{ ListOptions: gitea_sdk.ListOptions{Page: int(page), PageSize: int(pageSize)}, }) if err != nil { return to.ErrorResult(fmt.Errorf("list branch protections err: %v", err)) } return to.TextResult(bps) } func GetBranchProtectionFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called GetBranchProtectionFn") owner, _ := req.GetArguments()["owner"].(string) repo, _ := req.GetArguments()["repo"].(string) name, _ := req.GetArguments()["name"].(string) if owner == "" || repo == "" || name == "" { return to.ErrorResult(errors.New("owner, repo, and name are required")) } client, err := gitea.ClientFromContext(ctx) if err != nil { return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) } bp, _, err := client.GetBranchProtection(owner, repo, name) if err != nil { return to.ErrorResult(fmt.Errorf("get branch protection err: %v", err)) } return to.TextResult(bp) } func CreateBranchProtectionFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called CreateBranchProtectionFn") owner, _ := req.GetArguments()["owner"].(string) repo, _ := req.GetArguments()["repo"].(string) ruleName, _ := req.GetArguments()["rule_name"].(string) if owner == "" || repo == "" || ruleName == "" { return to.ErrorResult(errors.New("owner, repo, and rule_name are required")) } opt := gitea_sdk.CreateBranchProtectionOption{ RuleName: ruleName, } if v, ok := req.GetArguments()["enable_push"].(bool); ok { opt.EnablePush = v } if v, ok := req.GetArguments()["enable_merge_whitelist"].(bool); ok { opt.EnableMergeWhitelist = v } if v, ok := req.GetArguments()["enable_status_check"].(bool); ok { opt.EnableStatusCheck = v } if v, ok := req.GetArguments()["required_approvals"].(float64); ok { opt.RequiredApprovals = int64(v) } if v, ok := req.GetArguments()["block_on_rejected_reviews"].(bool); ok { opt.BlockOnRejectedReviews = v } if v, ok := req.GetArguments()["dismiss_stale_approvals"].(bool); ok { opt.DismissStaleApprovals = v } client, err := gitea.ClientFromContext(ctx) if err != nil { return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) } bp, _, err := client.CreateBranchProtection(owner, repo, opt) if err != nil { return to.ErrorResult(fmt.Errorf("create branch protection err: %v", err)) } return to.TextResult(bp) } func EditBranchProtectionFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called EditBranchProtectionFn") owner, _ := req.GetArguments()["owner"].(string) repo, _ := req.GetArguments()["repo"].(string) name, _ := req.GetArguments()["name"].(string) if owner == "" || repo == "" || name == "" { return to.ErrorResult(errors.New("owner, repo, and name are required")) } opt := gitea_sdk.EditBranchProtectionOption{} if v, ok := req.GetArguments()["enable_push"].(bool); ok { opt.EnablePush = &v } if v, ok := req.GetArguments()["enable_merge_whitelist"].(bool); ok { opt.EnableMergeWhitelist = &v } if v, ok := req.GetArguments()["enable_status_check"].(bool); ok { opt.EnableStatusCheck = &v } if v, ok := req.GetArguments()["required_approvals"].(float64); ok { approvals := int64(v) opt.RequiredApprovals = &approvals } if v, ok := req.GetArguments()["block_on_rejected_reviews"].(bool); ok { opt.BlockOnRejectedReviews = &v } if v, ok := req.GetArguments()["dismiss_stale_approvals"].(bool); ok { opt.DismissStaleApprovals = &v } client, err := gitea.ClientFromContext(ctx) if err != nil { return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) } bp, _, err := client.EditBranchProtection(owner, repo, name, opt) if err != nil { return to.ErrorResult(fmt.Errorf("edit branch protection err: %v", err)) } return to.TextResult(bp) } func DeleteBranchProtectionFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { log.Debugf("Called DeleteBranchProtectionFn") owner, _ := req.GetArguments()["owner"].(string) repo, _ := req.GetArguments()["repo"].(string) name, _ := req.GetArguments()["name"].(string) if owner == "" || repo == "" || name == "" { return to.ErrorResult(errors.New("owner, repo, and name are required")) } client, err := gitea.ClientFromContext(ctx) if err != nil { return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) } _, err = client.DeleteBranchProtection(owner, repo, name) if err != nil { return to.ErrorResult(fmt.Errorf("delete branch protection err: %v", err)) } return to.TextResult(map[string]string{"status": "deleted"}) }