feat: expand MCP tool coverage from 93 to 299 tools
Some checks failed
release-nightly / release-image (push) Failing after 2m17s

Fork the official gitea-mcp and massively extend API coverage:

- Organization: orgs, members, teams, hooks, blocks, activity (34 tools)
- User: profile, followers, keys, emails, repos, blocks (30 tools)
- Repository: collaborators, webhooks, branch/tag protection, deploy keys,
  topics, git refs/trees/notes, commit status, stars/watchers, forks,
  transfers, mirrors, templates (53 tools)
- Issue: reactions, pins, subscriptions, timeline, templates (16 tools)
- Notifications: list, check, read, repo-scoped (7 tools)
- Settings: API, attachment, repo, UI settings (4 tools)
- Packages: list, get, delete, files, latest, link/unlink (7 tools)
- Miscellaneous: server version, gitignore/label/license templates,
  markdown/markup rendering, node info, signing keys (12 tools)
- Admin: user/org/repo CRUD, system webhooks, cron tasks, unadopted
  repos, emails, badges (23 tools)

Module path updated to git.lethalbits.com/lethalbits/gitea-mcp.
This commit is contained in:
Andrew Miller
2026-03-05 11:03:20 -05:00
parent 9ce5604e4c
commit df25d328d1
66 changed files with 6976 additions and 122 deletions

View File

@@ -5,9 +5,9 @@ import (
"errors"
"fmt"
"gitea.com/gitea/gitea-mcp/pkg/gitea"
"gitea.com/gitea/gitea-mcp/pkg/log"
"gitea.com/gitea/gitea-mcp/pkg/to"
"git.lethalbits.com/lethalbits/gitea-mcp/pkg/gitea"
"git.lethalbits.com/lethalbits/gitea-mcp/pkg/log"
"git.lethalbits.com/lethalbits/gitea-mcp/pkg/to"
gitea_sdk "code.gitea.io/sdk/gitea"
"github.com/mark3labs/mcp-go/mcp"

View File

@@ -0,0 +1,226 @@
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"})
}

View File

@@ -0,0 +1,233 @@
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 (
ListCollaboratorsToolName = "list_collaborators"
IsCollaboratorToolName = "is_collaborator"
CollaboratorPermissionToolName = "collaborator_permission"
AddCollaboratorToolName = "add_collaborator"
DeleteCollaboratorToolName = "delete_collaborator"
GetReviewersToolName = "get_reviewers"
GetAssigneesToolName = "get_assignees"
)
var (
ListCollaboratorsTool = mcp.NewTool(
ListCollaboratorsToolName,
mcp.WithDescription("List a repository's collaborators"),
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)),
)
IsCollaboratorTool = mcp.NewTool(
IsCollaboratorToolName,
mcp.WithDescription("Check if a user is a collaborator of a repository"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithString("collaborator", mcp.Required(), mcp.Description("username to check")),
)
CollaboratorPermissionTool = mcp.NewTool(
CollaboratorPermissionToolName,
mcp.WithDescription("Get a collaborator's permission level on a repository"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithString("collaborator", mcp.Required(), mcp.Description("collaborator username")),
)
AddCollaboratorTool = mcp.NewTool(
AddCollaboratorToolName,
mcp.WithDescription("Add a collaborator to a repository"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithString("collaborator", mcp.Required(), mcp.Description("username to add")),
mcp.WithString("permission", mcp.Description("permission level: read, write, admin (default: write)")),
)
DeleteCollaboratorTool = mcp.NewTool(
DeleteCollaboratorToolName,
mcp.WithDescription("Remove a collaborator from a repository"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithString("collaborator", mcp.Required(), mcp.Description("username to remove")),
)
GetReviewersTool = mcp.NewTool(
GetReviewersToolName,
mcp.WithDescription("Get the list of users who can review PRs in a repository"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
)
GetAssigneesTool = mcp.NewTool(
GetAssigneesToolName,
mcp.WithDescription("Get the list of users who can be assigned to issues in a repository"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
)
)
func init() {
Tool.RegisterRead(server.ServerTool{Tool: ListCollaboratorsTool, Handler: ListCollaboratorsFn})
Tool.RegisterRead(server.ServerTool{Tool: IsCollaboratorTool, Handler: IsCollaboratorFn})
Tool.RegisterRead(server.ServerTool{Tool: CollaboratorPermissionTool, Handler: CollaboratorPermissionFn})
Tool.RegisterRead(server.ServerTool{Tool: GetReviewersTool, Handler: GetReviewersFn})
Tool.RegisterRead(server.ServerTool{Tool: GetAssigneesTool, Handler: GetAssigneesFn})
Tool.RegisterWrite(server.ServerTool{Tool: AddCollaboratorTool, Handler: AddCollaboratorFn})
Tool.RegisterWrite(server.ServerTool{Tool: DeleteCollaboratorTool, Handler: DeleteCollaboratorFn})
}
func ListCollaboratorsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ListCollaboratorsFn")
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))
}
users, _, err := client.ListCollaborators(owner, repo, gitea_sdk.ListCollaboratorsOptions{
ListOptions: gitea_sdk.ListOptions{Page: int(page), PageSize: int(pageSize)},
})
if err != nil {
return to.ErrorResult(fmt.Errorf("list collaborators err: %v", err))
}
return to.TextResult(users)
}
func IsCollaboratorFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called IsCollaboratorFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
collaborator, _ := req.GetArguments()["collaborator"].(string)
if owner == "" || repo == "" || collaborator == "" {
return to.ErrorResult(errors.New("owner, repo, and collaborator are required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
isCollab, _, err := client.IsCollaborator(owner, repo, collaborator)
if err != nil {
return to.ErrorResult(fmt.Errorf("check collaborator err: %v", err))
}
return to.TextResult(map[string]any{"collaborator": collaborator, "is_collaborator": isCollab})
}
func CollaboratorPermissionFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called CollaboratorPermissionFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
collaborator, _ := req.GetArguments()["collaborator"].(string)
if owner == "" || repo == "" || collaborator == "" {
return to.ErrorResult(errors.New("owner, repo, and collaborator are required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
perm, _, err := client.CollaboratorPermission(owner, repo, collaborator)
if err != nil {
return to.ErrorResult(fmt.Errorf("get collaborator permission err: %v", err))
}
return to.TextResult(perm)
}
func AddCollaboratorFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called AddCollaboratorFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
collaborator, _ := req.GetArguments()["collaborator"].(string)
if owner == "" || repo == "" || collaborator == "" {
return to.ErrorResult(errors.New("owner, repo, and collaborator are required"))
}
opt := gitea_sdk.AddCollaboratorOption{}
if v, ok := req.GetArguments()["permission"].(string); ok {
perm := gitea_sdk.AccessMode(v)
opt.Permission = &perm
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
_, err = client.AddCollaborator(owner, repo, collaborator, opt)
if err != nil {
return to.ErrorResult(fmt.Errorf("add collaborator err: %v", err))
}
return to.TextResult(map[string]string{"status": "added", "collaborator": collaborator})
}
func DeleteCollaboratorFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called DeleteCollaboratorFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
collaborator, _ := req.GetArguments()["collaborator"].(string)
if owner == "" || repo == "" || collaborator == "" {
return to.ErrorResult(errors.New("owner, repo, and collaborator are required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
_, err = client.DeleteCollaborator(owner, repo, collaborator)
if err != nil {
return to.ErrorResult(fmt.Errorf("delete collaborator err: %v", err))
}
return to.TextResult(map[string]string{"status": "removed", "collaborator": collaborator})
}
func GetReviewersFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called GetReviewersFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
if owner == "" || repo == "" {
return to.ErrorResult(errors.New("owner and repo are required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
users, _, err := client.GetReviewers(owner, repo)
if err != nil {
return to.ErrorResult(fmt.Errorf("get reviewers err: %v", err))
}
return to.TextResult(users)
}
func GetAssigneesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called GetAssigneesFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
if owner == "" || repo == "" {
return to.ErrorResult(errors.New("owner and repo are required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
users, _, err := client.GetAssignees(owner, repo)
if err != nil {
return to.ErrorResult(fmt.Errorf("get assignees err: %v", err))
}
return to.TextResult(users)
}

View File

@@ -5,10 +5,10 @@ import (
"errors"
"fmt"
"gitea.com/gitea/gitea-mcp/pkg/gitea"
"gitea.com/gitea/gitea-mcp/pkg/log"
"gitea.com/gitea/gitea-mcp/pkg/params"
"gitea.com/gitea/gitea-mcp/pkg/to"
"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"

View File

@@ -0,0 +1,161 @@
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 (
ListDeployKeysToolName = "list_deploy_keys"
GetDeployKeyToolName = "get_deploy_key"
CreateDeployKeyToolName = "create_deploy_key"
DeleteDeployKeyToolName = "delete_deploy_key"
)
var (
ListDeployKeysTool = mcp.NewTool(
ListDeployKeysToolName,
mcp.WithDescription("List a repository's deploy keys"),
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)),
)
GetDeployKeyTool = mcp.NewTool(
GetDeployKeyToolName,
mcp.WithDescription("Get a deploy key by ID"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithNumber("key_id", mcp.Required(), mcp.Description("deploy key ID")),
)
CreateDeployKeyTool = mcp.NewTool(
CreateDeployKeyToolName,
mcp.WithDescription("Add a deploy key to a repository"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithString("title", mcp.Required(), mcp.Description("key title")),
mcp.WithString("key", mcp.Required(), mcp.Description("public key content")),
mcp.WithBoolean("read_only", mcp.Description("whether the key has read-only access (default: true)")),
)
DeleteDeployKeyTool = mcp.NewTool(
DeleteDeployKeyToolName,
mcp.WithDescription("Remove a deploy key from a repository"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithNumber("key_id", mcp.Required(), mcp.Description("deploy key ID")),
)
)
func init() {
Tool.RegisterRead(server.ServerTool{Tool: ListDeployKeysTool, Handler: ListDeployKeysFn})
Tool.RegisterRead(server.ServerTool{Tool: GetDeployKeyTool, Handler: GetDeployKeyFn})
Tool.RegisterWrite(server.ServerTool{Tool: CreateDeployKeyTool, Handler: CreateDeployKeyFn})
Tool.RegisterWrite(server.ServerTool{Tool: DeleteDeployKeyTool, Handler: DeleteDeployKeyFn})
}
func ListDeployKeysFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ListDeployKeysFn")
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))
}
keys, _, err := client.ListDeployKeys(owner, repo, gitea_sdk.ListDeployKeysOptions{
ListOptions: gitea_sdk.ListOptions{Page: int(page), PageSize: int(pageSize)},
})
if err != nil {
return to.ErrorResult(fmt.Errorf("list deploy keys err: %v", err))
}
return to.TextResult(keys)
}
func GetDeployKeyFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called GetDeployKeyFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
if owner == "" || repo == "" {
return to.ErrorResult(errors.New("owner and repo are required"))
}
keyID, err := params.GetIndex(req.GetArguments(), "key_id")
if err != nil {
return to.ErrorResult(err)
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
key, _, err := client.GetDeployKey(owner, repo, keyID)
if err != nil {
return to.ErrorResult(fmt.Errorf("get deploy key err: %v", err))
}
return to.TextResult(key)
}
func CreateDeployKeyFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called CreateDeployKeyFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
title, _ := req.GetArguments()["title"].(string)
key, _ := req.GetArguments()["key"].(string)
if owner == "" || repo == "" || title == "" || key == "" {
return to.ErrorResult(errors.New("owner, repo, title, and key are required"))
}
opt := gitea_sdk.CreateKeyOption{
Title: title,
Key: key,
ReadOnly: true,
}
if v, ok := req.GetArguments()["read_only"].(bool); ok {
opt.ReadOnly = v
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
dk, _, err := client.CreateDeployKey(owner, repo, opt)
if err != nil {
return to.ErrorResult(fmt.Errorf("create deploy key err: %v", err))
}
return to.TextResult(dk)
}
func DeleteDeployKeyFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called DeleteDeployKeyFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
if owner == "" || repo == "" {
return to.ErrorResult(errors.New("owner and repo are required"))
}
keyID, err := params.GetIndex(req.GetArguments(), "key_id")
if err != nil {
return to.ErrorResult(err)
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
_, err = client.DeleteDeployKey(owner, repo, keyID)
if err != nil {
return to.ErrorResult(fmt.Errorf("delete deploy key err: %v", err))
}
return to.TextResult(map[string]string{"status": "deleted"})
}

View File

@@ -9,9 +9,9 @@ import (
"errors"
"fmt"
"gitea.com/gitea/gitea-mcp/pkg/gitea"
"gitea.com/gitea/gitea-mcp/pkg/log"
"gitea.com/gitea/gitea-mcp/pkg/to"
"git.lethalbits.com/lethalbits/gitea-mcp/pkg/gitea"
"git.lethalbits.com/lethalbits/gitea-mcp/pkg/log"
"git.lethalbits.com/lethalbits/gitea-mcp/pkg/to"
gitea_sdk "code.gitea.io/sdk/gitea"
"github.com/mark3labs/mcp-go/mcp"

175
operation/repo/git.go Normal file
View File

@@ -0,0 +1,175 @@
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/to"
gitea_sdk "code.gitea.io/sdk/gitea"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
const (
GetRepoRefToolName = "get_repo_ref"
ListAllGitRefsToolName = "list_all_git_refs"
GetTreeToolName = "get_tree"
GetRepoNoteToolName = "get_repo_note"
CompareCommitsToolName = "compare_commits"
)
var (
GetRepoRefTool = mcp.NewTool(
GetRepoRefToolName,
mcp.WithDescription("Get a git reference from a repository"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithString("ref", mcp.Required(), mcp.Description("git reference (e.g., refs/heads/main, refs/tags/v1.0)")),
)
ListAllGitRefsTool = mcp.NewTool(
ListAllGitRefsToolName,
mcp.WithDescription("List all git references in a repository"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
)
GetTreeTool = mcp.NewTool(
GetTreeToolName,
mcp.WithDescription("Get the tree of a repository at a given SHA"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithString("sha", mcp.Required(), mcp.Description("SHA of the tree")),
mcp.WithBoolean("recursive", mcp.Description("show all items recursively")),
)
GetRepoNoteTool = mcp.NewTool(
GetRepoNoteToolName,
mcp.WithDescription("Get a git note for a commit"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithString("sha", mcp.Required(), mcp.Description("commit SHA")),
)
CompareCommitsTool = mcp.NewTool(
CompareCommitsToolName,
mcp.WithDescription("Compare two commits in a repository"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithString("base", mcp.Required(), mcp.Description("base commit SHA or branch")),
mcp.WithString("head", mcp.Required(), mcp.Description("head commit SHA or branch")),
)
)
func init() {
Tool.RegisterRead(server.ServerTool{Tool: GetRepoRefTool, Handler: GetRepoRefFn})
Tool.RegisterRead(server.ServerTool{Tool: ListAllGitRefsTool, Handler: ListAllGitRefsFn})
Tool.RegisterRead(server.ServerTool{Tool: GetTreeTool, Handler: GetTreeFn})
Tool.RegisterRead(server.ServerTool{Tool: GetRepoNoteTool, Handler: GetRepoNoteFn})
Tool.RegisterRead(server.ServerTool{Tool: CompareCommitsTool, Handler: CompareCommitsFn})
}
func GetRepoRefFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called GetRepoRefFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
ref, _ := req.GetArguments()["ref"].(string)
if owner == "" || repo == "" || ref == "" {
return to.ErrorResult(errors.New("owner, repo, and ref are required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
reference, _, err := client.GetRepoRef(owner, repo, ref)
if err != nil {
return to.ErrorResult(fmt.Errorf("get repo ref err: %v", err))
}
return to.TextResult(reference)
}
func ListAllGitRefsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ListAllGitRefsFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
if owner == "" || repo == "" {
return to.ErrorResult(errors.New("owner and repo are required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
refs, _, err := client.ListAllGitRefs(owner, repo)
if err != nil {
return to.ErrorResult(fmt.Errorf("list git refs err: %v", err))
}
return to.TextResult(refs)
}
func GetTreeFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called GetTreeFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
sha, _ := req.GetArguments()["sha"].(string)
if owner == "" || repo == "" || sha == "" {
return to.ErrorResult(errors.New("owner, repo, and sha are required"))
}
opt := gitea_sdk.ListTreeOptions{
Ref: sha,
}
if v, ok := req.GetArguments()["recursive"].(bool); ok && v {
opt.Recursive = true
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
tree, _, err := client.GetTrees(owner, repo, opt)
if err != nil {
return to.ErrorResult(fmt.Errorf("get tree err: %v", err))
}
return to.TextResult(tree)
}
func GetRepoNoteFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called GetRepoNoteFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
sha, _ := req.GetArguments()["sha"].(string)
if owner == "" || repo == "" || sha == "" {
return to.ErrorResult(errors.New("owner, repo, and sha are required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
note, _, err := client.GetRepoNote(owner, repo, sha, gitea_sdk.GetRepoNoteOptions{})
if err != nil {
return to.ErrorResult(fmt.Errorf("get repo note err: %v", err))
}
return to.TextResult(note)
}
func CompareCommitsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called CompareCommitsFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
base, _ := req.GetArguments()["base"].(string)
head, _ := req.GetArguments()["head"].(string)
if owner == "" || repo == "" || base == "" || head == "" {
return to.ErrorResult(errors.New("owner, repo, base, and head are required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
compare, _, err := client.CompareCommits(owner, repo, base, head)
if err != nil {
return to.ErrorResult(fmt.Errorf("compare commits err: %v", err))
}
return to.TextResult(compare)
}

227
operation/repo/hook.go Normal file
View File

@@ -0,0 +1,227 @@
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 (
ListRepoHooksToolName = "list_repo_hooks"
GetRepoHookToolName = "get_repo_hook"
CreateRepoHookToolName = "create_repo_hook"
EditRepoHookToolName = "edit_repo_hook"
DeleteRepoHookToolName = "delete_repo_hook"
)
var (
ListRepoHooksTool = mcp.NewTool(
ListRepoHooksToolName,
mcp.WithDescription("List a repository's webhooks"),
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)),
)
GetRepoHookTool = mcp.NewTool(
GetRepoHookToolName,
mcp.WithDescription("Get a repository webhook 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("webhook ID")),
)
CreateRepoHookTool = mcp.NewTool(
CreateRepoHookToolName,
mcp.WithDescription("Create a webhook for a repository"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithString("type", mcp.Required(), mcp.Description("hook type: gitea, gogs, slack, discord, dingtalk, telegram, msteams, feishu, wechatwork, packagist")),
mcp.WithString("url", mcp.Required(), mcp.Description("target URL for the webhook")),
mcp.WithString("content_type", mcp.Description("content type: json or form (default: json)")),
mcp.WithString("secret", mcp.Description("webhook secret")),
mcp.WithBoolean("active", mcp.Description("whether the webhook is active (default: true)")),
)
EditRepoHookTool = mcp.NewTool(
EditRepoHookToolName,
mcp.WithDescription("Edit a repository webhook"),
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("webhook ID")),
mcp.WithString("url", mcp.Description("target URL")),
mcp.WithString("content_type", mcp.Description("content type: json or form")),
mcp.WithString("secret", mcp.Description("webhook secret")),
mcp.WithBoolean("active", mcp.Description("whether the webhook is active")),
)
DeleteRepoHookTool = mcp.NewTool(
DeleteRepoHookToolName,
mcp.WithDescription("Delete a repository webhook"),
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("webhook ID")),
)
)
func init() {
Tool.RegisterRead(server.ServerTool{Tool: ListRepoHooksTool, Handler: ListRepoHooksFn})
Tool.RegisterRead(server.ServerTool{Tool: GetRepoHookTool, Handler: GetRepoHookFn})
Tool.RegisterWrite(server.ServerTool{Tool: CreateRepoHookTool, Handler: CreateRepoHookFn})
Tool.RegisterWrite(server.ServerTool{Tool: EditRepoHookTool, Handler: EditRepoHookFn})
Tool.RegisterWrite(server.ServerTool{Tool: DeleteRepoHookTool, Handler: DeleteRepoHookFn})
}
func ListRepoHooksFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ListRepoHooksFn")
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))
}
hooks, _, err := client.ListRepoHooks(owner, repo, gitea_sdk.ListHooksOptions{
ListOptions: gitea_sdk.ListOptions{Page: int(page), PageSize: int(pageSize)},
})
if err != nil {
return to.ErrorResult(fmt.Errorf("list repo hooks err: %v", err))
}
return to.TextResult(hooks)
}
func GetRepoHookFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called GetRepoHookFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
if owner == "" || repo == "" {
return to.ErrorResult(errors.New("owner and repo are required"))
}
id, err := params.GetIndex(req.GetArguments(), "id")
if err != nil {
return to.ErrorResult(err)
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
hook, _, err := client.GetRepoHook(owner, repo, id)
if err != nil {
return to.ErrorResult(fmt.Errorf("get repo hook err: %v", err))
}
return to.TextResult(hook)
}
func CreateRepoHookFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called CreateRepoHookFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
hookType, _ := req.GetArguments()["type"].(string)
url, _ := req.GetArguments()["url"].(string)
if owner == "" || repo == "" || hookType == "" || url == "" {
return to.ErrorResult(errors.New("owner, repo, type, and url are required"))
}
contentType := "json"
if v, ok := req.GetArguments()["content_type"].(string); ok {
contentType = v
}
config := map[string]string{
"url": url,
"content_type": contentType,
}
if v, ok := req.GetArguments()["secret"].(string); ok {
config["secret"] = v
}
opt := gitea_sdk.CreateHookOption{
Type: gitea_sdk.HookType(hookType),
Config: config,
Active: true,
}
if v, ok := req.GetArguments()["active"].(bool); ok {
opt.Active = v
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
hook, _, err := client.CreateRepoHook(owner, repo, opt)
if err != nil {
return to.ErrorResult(fmt.Errorf("create repo hook err: %v", err))
}
return to.TextResult(hook)
}
func EditRepoHookFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called EditRepoHookFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
if owner == "" || repo == "" {
return to.ErrorResult(errors.New("owner and repo are required"))
}
id, err := params.GetIndex(req.GetArguments(), "id")
if err != nil {
return to.ErrorResult(err)
}
opt := gitea_sdk.EditHookOption{}
config := map[string]string{}
if v, ok := req.GetArguments()["url"].(string); ok {
config["url"] = v
}
if v, ok := req.GetArguments()["content_type"].(string); ok {
config["content_type"] = v
}
if v, ok := req.GetArguments()["secret"].(string); ok {
config["secret"] = v
}
if len(config) > 0 {
opt.Config = config
}
if v, ok := req.GetArguments()["active"].(bool); ok {
opt.Active = &v
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
_, err = client.EditRepoHook(owner, repo, id, opt)
if err != nil {
return to.ErrorResult(fmt.Errorf("edit repo hook err: %v", err))
}
return to.TextResult(map[string]string{"status": "updated"})
}
func DeleteRepoHookFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called DeleteRepoHookFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
if owner == "" || repo == "" {
return to.ErrorResult(errors.New("owner and repo are required"))
}
id, err := params.GetIndex(req.GetArguments(), "id")
if err != nil {
return to.ErrorResult(err)
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
_, err = client.DeleteRepoHook(owner, repo, id)
if err != nil {
return to.ErrorResult(fmt.Errorf("delete repo hook err: %v", err))
}
return to.TextResult(map[string]string{"status": "deleted"})
}

156
operation/repo/mirror.go Normal file
View File

@@ -0,0 +1,156 @@
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/to"
gitea_sdk "code.gitea.io/sdk/gitea"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
const (
CreatePushMirrorToolName = "create_push_mirror"
ListPushMirrorsToolName = "list_push_mirrors"
GetPushMirrorByRemoteNameToolName = "get_push_mirror"
DeletePushMirrorToolName = "delete_push_mirror"
)
var (
CreatePushMirrorTool = mcp.NewTool(
CreatePushMirrorToolName,
mcp.WithDescription("Create a push mirror for a repository"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithString("remote_address", mcp.Required(), mcp.Description("remote git URL to push to")),
mcp.WithString("remote_username", mcp.Description("remote username for authentication")),
mcp.WithString("remote_password", mcp.Description("remote password/token for authentication")),
mcp.WithString("interval", mcp.Description("sync interval (e.g., 8h0m0s)")),
mcp.WithBoolean("sync_on_commit", mcp.Description("sync on commit")),
)
ListPushMirrorsTool = mcp.NewTool(
ListPushMirrorsToolName,
mcp.WithDescription("List push mirrors for a repository"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
)
GetPushMirrorByRemoteNameTool = mcp.NewTool(
GetPushMirrorByRemoteNameToolName,
mcp.WithDescription("Get a push mirror by remote name"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithString("remote_name", mcp.Required(), mcp.Description("remote name")),
)
DeletePushMirrorTool = mcp.NewTool(
DeletePushMirrorToolName,
mcp.WithDescription("Delete a push mirror by remote name"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithString("remote_name", mcp.Required(), mcp.Description("remote name to delete")),
)
)
func init() {
Tool.RegisterRead(server.ServerTool{Tool: ListPushMirrorsTool, Handler: ListPushMirrorsFn})
Tool.RegisterRead(server.ServerTool{Tool: GetPushMirrorByRemoteNameTool, Handler: GetPushMirrorByRemoteNameFn})
Tool.RegisterWrite(server.ServerTool{Tool: CreatePushMirrorTool, Handler: CreatePushMirrorFn})
Tool.RegisterWrite(server.ServerTool{Tool: DeletePushMirrorTool, Handler: DeletePushMirrorFn})
}
func CreatePushMirrorFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called CreatePushMirrorFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
remoteAddr, _ := req.GetArguments()["remote_address"].(string)
if owner == "" || repo == "" || remoteAddr == "" {
return to.ErrorResult(errors.New("owner, repo, and remote_address are required"))
}
opt := gitea_sdk.CreatePushMirrorOption{
RemoteAddress: remoteAddr,
}
if v, ok := req.GetArguments()["remote_username"].(string); ok {
opt.RemoteUsername = v
}
if v, ok := req.GetArguments()["remote_password"].(string); ok {
opt.RemotePassword = v
}
if v, ok := req.GetArguments()["interval"].(string); ok {
opt.Interval = v
}
if v, ok := req.GetArguments()["sync_on_commit"].(bool); ok {
opt.SyncONCommit = v
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
mirror, _, err := client.PushMirrors(owner, repo, opt)
if err != nil {
return to.ErrorResult(fmt.Errorf("create push mirror err: %v", err))
}
return to.TextResult(mirror)
}
func ListPushMirrorsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ListPushMirrorsFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
if owner == "" || repo == "" {
return to.ErrorResult(errors.New("owner and repo are required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
mirrors, _, err := client.ListPushMirrors(owner, repo, gitea_sdk.ListOptions{})
if err != nil {
return to.ErrorResult(fmt.Errorf("list push mirrors err: %v", err))
}
return to.TextResult(mirrors)
}
func GetPushMirrorByRemoteNameFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called GetPushMirrorByRemoteNameFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
remoteName, _ := req.GetArguments()["remote_name"].(string)
if owner == "" || repo == "" || remoteName == "" {
return to.ErrorResult(errors.New("owner, repo, and remote_name are required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
mirror, _, err := client.GetPushMirrorByRemoteName(owner, repo, remoteName)
if err != nil {
return to.ErrorResult(fmt.Errorf("get push mirror err: %v", err))
}
return to.TextResult(mirror)
}
func DeletePushMirrorFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called DeletePushMirrorFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
remoteName, _ := req.GetArguments()["remote_name"].(string)
if owner == "" || repo == "" || remoteName == "" {
return to.ErrorResult(errors.New("owner, repo, and remote_name are required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
_, err = client.DeletePushMirror(owner, repo, remoteName)
if err != nil {
return to.ErrorResult(fmt.Errorf("delete push mirror err: %v", err))
}
return to.TextResult(map[string]string{"status": "deleted"})
}

View File

@@ -6,10 +6,10 @@ import (
"fmt"
"time"
"gitea.com/gitea/gitea-mcp/pkg/gitea"
"gitea.com/gitea/gitea-mcp/pkg/log"
"gitea.com/gitea/gitea-mcp/pkg/params"
"gitea.com/gitea/gitea-mcp/pkg/to"
"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"

View File

@@ -5,11 +5,11 @@ import (
"errors"
"fmt"
"gitea.com/gitea/gitea-mcp/pkg/gitea"
"gitea.com/gitea/gitea-mcp/pkg/log"
"gitea.com/gitea/gitea-mcp/pkg/params"
"gitea.com/gitea/gitea-mcp/pkg/to"
"gitea.com/gitea/gitea-mcp/pkg/tool"
"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"
"git.lethalbits.com/lethalbits/gitea-mcp/pkg/tool"
gitea_sdk "code.gitea.io/sdk/gitea"
"github.com/mark3labs/mcp-go/mcp"

View File

@@ -0,0 +1,216 @@
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 (
ListStargazersToolName = "list_stargazers"
ListForksToolName = "list_forks"
GetWatchedReposToolName = "get_watched_repos"
GetMyWatchedReposToolName = "get_my_watched_repos"
CheckRepoWatchToolName = "check_repo_watch"
WatchRepoToolName = "watch_repo"
UnWatchRepoToolName = "unwatch_repo"
)
var (
ListStargazersTool = mcp.NewTool(
ListStargazersToolName,
mcp.WithDescription("List a repository's stargazers"),
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)),
)
ListForksTool = mcp.NewTool(
ListForksToolName,
mcp.WithDescription("List a repository's forks"),
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)),
)
GetWatchedReposTool = mcp.NewTool(
GetWatchedReposToolName,
mcp.WithDescription("List repositories watched by a user"),
mcp.WithString("username", mcp.Required(), mcp.Description("username")),
)
GetMyWatchedReposTool = mcp.NewTool(
GetMyWatchedReposToolName,
mcp.WithDescription("List repositories watched by the authenticated user"),
)
CheckRepoWatchTool = mcp.NewTool(
CheckRepoWatchToolName,
mcp.WithDescription("Check if the authenticated user is watching a repository"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
)
WatchRepoTool = mcp.NewTool(
WatchRepoToolName,
mcp.WithDescription("Watch a repository"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
)
UnWatchRepoTool = mcp.NewTool(
UnWatchRepoToolName,
mcp.WithDescription("Unwatch a repository"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
)
)
func init() {
Tool.RegisterRead(server.ServerTool{Tool: ListStargazersTool, Handler: ListStargazersFn})
Tool.RegisterRead(server.ServerTool{Tool: ListForksTool, Handler: ListForksFn})
Tool.RegisterRead(server.ServerTool{Tool: GetWatchedReposTool, Handler: GetWatchedReposFn})
Tool.RegisterRead(server.ServerTool{Tool: GetMyWatchedReposTool, Handler: GetMyWatchedReposFn})
Tool.RegisterRead(server.ServerTool{Tool: CheckRepoWatchTool, Handler: CheckRepoWatchFn})
Tool.RegisterWrite(server.ServerTool{Tool: WatchRepoTool, Handler: WatchRepoFn})
Tool.RegisterWrite(server.ServerTool{Tool: UnWatchRepoTool, Handler: UnWatchRepoFn})
}
func ListStargazersFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ListStargazersFn")
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))
}
users, _, err := client.ListRepoStargazers(owner, repo, gitea_sdk.ListStargazersOptions{
ListOptions: gitea_sdk.ListOptions{Page: int(page), PageSize: int(pageSize)},
})
if err != nil {
return to.ErrorResult(fmt.Errorf("list stargazers err: %v", err))
}
return to.TextResult(users)
}
func ListForksFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ListForksFn")
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))
}
repos, _, err := client.ListForks(owner, repo, gitea_sdk.ListForksOptions{
ListOptions: gitea_sdk.ListOptions{Page: int(page), PageSize: int(pageSize)},
})
if err != nil {
return to.ErrorResult(fmt.Errorf("list forks err: %v", err))
}
return to.TextResult(repos)
}
func GetWatchedReposFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called GetWatchedReposFn")
username, _ := req.GetArguments()["username"].(string)
if username == "" {
return to.ErrorResult(errors.New("username is required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
repos, _, err := client.GetWatchedRepos(username)
if err != nil {
return to.ErrorResult(fmt.Errorf("get watched repos err: %v", err))
}
return to.TextResult(repos)
}
func GetMyWatchedReposFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called GetMyWatchedReposFn")
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
repos, _, err := client.GetMyWatchedRepos()
if err != nil {
return to.ErrorResult(fmt.Errorf("get my watched repos err: %v", err))
}
return to.TextResult(repos)
}
func CheckRepoWatchFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called CheckRepoWatchFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
if owner == "" || repo == "" {
return to.ErrorResult(errors.New("owner and repo are required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
watching, _, err := client.CheckRepoWatch(owner, repo)
if err != nil {
return to.ErrorResult(fmt.Errorf("check repo watch err: %v", err))
}
return to.TextResult(map[string]any{"watching": watching})
}
func WatchRepoFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called WatchRepoFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
if owner == "" || repo == "" {
return to.ErrorResult(errors.New("owner and repo are required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
_, err = client.WatchRepo(owner, repo)
if err != nil {
return to.ErrorResult(fmt.Errorf("watch repo err: %v", err))
}
return to.TextResult(map[string]string{"status": "watching"})
}
func UnWatchRepoFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called UnWatchRepoFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
if owner == "" || repo == "" {
return to.ErrorResult(errors.New("owner and repo are required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
_, err = client.UnWatchRepo(owner, repo)
if err != nil {
return to.ErrorResult(fmt.Errorf("unwatch repo err: %v", err))
}
return to.TextResult(map[string]string{"status": "unwatched"})
}

134
operation/repo/status.go Normal file
View File

@@ -0,0 +1,134 @@
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 (
CreateStatusToolName = "create_commit_status"
ListStatusesToolName = "list_commit_statuses"
GetCombinedStatusToolName = "get_combined_status"
)
var (
CreateStatusTool = mcp.NewTool(
CreateStatusToolName,
mcp.WithDescription("Create a commit status"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithString("sha", mcp.Required(), mcp.Description("commit SHA")),
mcp.WithString("state", mcp.Required(), mcp.Description("status state: pending, success, error, failure, warning")),
mcp.WithString("target_url", mcp.Description("URL for status details")),
mcp.WithString("description", mcp.Description("status description")),
mcp.WithString("context", mcp.Description("status context (e.g., ci/build)")),
)
ListStatusesTool = mcp.NewTool(
ListStatusesToolName,
mcp.WithDescription("List commit statuses for a ref"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithString("ref", mcp.Required(), mcp.Description("commit SHA, branch, or tag")),
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)),
mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)),
)
GetCombinedStatusTool = mcp.NewTool(
GetCombinedStatusToolName,
mcp.WithDescription("Get the combined status for a ref"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithString("ref", mcp.Required(), mcp.Description("commit SHA, branch, or tag")),
)
)
func init() {
Tool.RegisterRead(server.ServerTool{Tool: ListStatusesTool, Handler: ListStatusesFn})
Tool.RegisterRead(server.ServerTool{Tool: GetCombinedStatusTool, Handler: GetCombinedStatusFn})
Tool.RegisterWrite(server.ServerTool{Tool: CreateStatusTool, Handler: CreateStatusFn})
}
func CreateStatusFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called CreateStatusFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
sha, _ := req.GetArguments()["sha"].(string)
state, _ := req.GetArguments()["state"].(string)
if owner == "" || repo == "" || sha == "" || state == "" {
return to.ErrorResult(errors.New("owner, repo, sha, and state are required"))
}
opt := gitea_sdk.CreateStatusOption{
State: gitea_sdk.StatusState(state),
}
if v, ok := req.GetArguments()["target_url"].(string); ok {
opt.TargetURL = v
}
if v, ok := req.GetArguments()["description"].(string); ok {
opt.Description = v
}
if v, ok := req.GetArguments()["context"].(string); ok {
opt.Context = v
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
status, _, err := client.CreateStatus(owner, repo, sha, opt)
if err != nil {
return to.ErrorResult(fmt.Errorf("create status err: %v", err))
}
return to.TextResult(status)
}
func ListStatusesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ListStatusesFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
ref, _ := req.GetArguments()["ref"].(string)
if owner == "" || repo == "" || ref == "" {
return to.ErrorResult(errors.New("owner, repo, and ref 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))
}
statuses, _, err := client.ListStatuses(owner, repo, ref, gitea_sdk.ListStatusesOption{
ListOptions: gitea_sdk.ListOptions{Page: int(page), PageSize: int(pageSize)},
})
if err != nil {
return to.ErrorResult(fmt.Errorf("list statuses err: %v", err))
}
return to.TextResult(statuses)
}
func GetCombinedStatusFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called GetCombinedStatusFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
ref, _ := req.GetArguments()["ref"].(string)
if owner == "" || repo == "" || ref == "" {
return to.ErrorResult(errors.New("owner, repo, and ref are required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
combined, _, err := client.GetCombinedStatus(owner, repo, ref)
if err != nil {
return to.ErrorResult(fmt.Errorf("get combined status err: %v", err))
}
return to.TextResult(combined)
}

View File

@@ -5,10 +5,10 @@ import (
"errors"
"fmt"
"gitea.com/gitea/gitea-mcp/pkg/gitea"
"gitea.com/gitea/gitea-mcp/pkg/log"
"gitea.com/gitea/gitea-mcp/pkg/params"
"gitea.com/gitea/gitea-mcp/pkg/to"
"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"

View File

@@ -0,0 +1,200 @@
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 (
ListTagProtectionsToolName = "list_tag_protections"
GetTagProtectionToolName = "get_tag_protection"
CreateTagProtectionToolName = "create_tag_protection"
EditTagProtectionToolName = "edit_tag_protection"
DeleteTagProtectionToolName = "delete_tag_protection"
)
var (
ListTagProtectionsTool = mcp.NewTool(
ListTagProtectionsToolName,
mcp.WithDescription("List tag protections for a repository"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
)
GetTagProtectionTool = mcp.NewTool(
GetTagProtectionToolName,
mcp.WithDescription("Get a tag protection rule 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("tag protection ID")),
)
CreateTagProtectionTool = mcp.NewTool(
CreateTagProtectionToolName,
mcp.WithDescription("Create a tag protection rule"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithString("name_pattern", mcp.Required(), mcp.Description("glob pattern for tag name matching")),
mcp.WithString("whitelist_usernames", mcp.Description("comma-separated list of allowed usernames")),
mcp.WithString("whitelist_teams", mcp.Description("comma-separated list of allowed team names")),
)
EditTagProtectionTool = mcp.NewTool(
EditTagProtectionToolName,
mcp.WithDescription("Edit a tag protection rule"),
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("tag protection ID")),
mcp.WithString("name_pattern", mcp.Description("glob pattern for tag name matching")),
mcp.WithString("whitelist_usernames", mcp.Description("comma-separated list of allowed usernames")),
mcp.WithString("whitelist_teams", mcp.Description("comma-separated list of allowed team names")),
)
DeleteTagProtectionTool = mcp.NewTool(
DeleteTagProtectionToolName,
mcp.WithDescription("Delete a tag protection rule"),
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("tag protection ID")),
)
)
func init() {
Tool.RegisterRead(server.ServerTool{Tool: ListTagProtectionsTool, Handler: ListTagProtectionsFn})
Tool.RegisterRead(server.ServerTool{Tool: GetTagProtectionTool, Handler: GetTagProtectionFn})
Tool.RegisterWrite(server.ServerTool{Tool: CreateTagProtectionTool, Handler: CreateTagProtectionFn})
Tool.RegisterWrite(server.ServerTool{Tool: EditTagProtectionTool, Handler: EditTagProtectionFn})
Tool.RegisterWrite(server.ServerTool{Tool: DeleteTagProtectionTool, Handler: DeleteTagProtectionFn})
}
func ListTagProtectionsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ListTagProtectionsFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
if owner == "" || repo == "" {
return to.ErrorResult(errors.New("owner and repo are required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
tps, _, err := client.ListTagProtection(owner, repo, gitea_sdk.ListRepoTagProtectionsOptions{})
if err != nil {
return to.ErrorResult(fmt.Errorf("list tag protections err: %v", err))
}
return to.TextResult(tps)
}
func GetTagProtectionFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called GetTagProtectionFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
if owner == "" || repo == "" {
return to.ErrorResult(errors.New("owner and repo are required"))
}
id, err := params.GetIndex(req.GetArguments(), "id")
if err != nil {
return to.ErrorResult(err)
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
tp, _, err := client.GetTagProtection(owner, repo, id)
if err != nil {
return to.ErrorResult(fmt.Errorf("get tag protection err: %v", err))
}
return to.TextResult(tp)
}
func CreateTagProtectionFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called CreateTagProtectionFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
namePattern, _ := req.GetArguments()["name_pattern"].(string)
if owner == "" || repo == "" || namePattern == "" {
return to.ErrorResult(errors.New("owner, repo, and name_pattern are required"))
}
opt := gitea_sdk.CreateTagProtectionOption{
NamePattern: namePattern,
}
if v, ok := req.GetArguments()["whitelist_usernames"].(string); ok && v != "" {
opt.WhitelistUsernames = splitAndTrim(v)
}
if v, ok := req.GetArguments()["whitelist_teams"].(string); ok && v != "" {
opt.WhitelistTeams = splitAndTrim(v)
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
tp, _, err := client.CreateTagProtection(owner, repo, opt)
if err != nil {
return to.ErrorResult(fmt.Errorf("create tag protection err: %v", err))
}
return to.TextResult(tp)
}
func EditTagProtectionFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called EditTagProtectionFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
if owner == "" || repo == "" {
return to.ErrorResult(errors.New("owner and repo are required"))
}
id, err := params.GetIndex(req.GetArguments(), "id")
if err != nil {
return to.ErrorResult(err)
}
opt := gitea_sdk.EditTagProtectionOption{}
if v, ok := req.GetArguments()["name_pattern"].(string); ok {
opt.NamePattern = &v
}
if v, ok := req.GetArguments()["whitelist_usernames"].(string); ok {
opt.WhitelistUsernames = splitAndTrim(v)
}
if v, ok := req.GetArguments()["whitelist_teams"].(string); ok {
opt.WhitelistTeams = splitAndTrim(v)
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
tp, _, err := client.EditTagProtection(owner, repo, id, opt)
if err != nil {
return to.ErrorResult(fmt.Errorf("edit tag protection err: %v", err))
}
return to.TextResult(tp)
}
func DeleteTagProtectionFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called DeleteTagProtectionFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
if owner == "" || repo == "" {
return to.ErrorResult(errors.New("owner and repo are required"))
}
id, err := params.GetIndex(req.GetArguments(), "id")
if err != nil {
return to.ErrorResult(err)
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
_, err = client.DeleteTagProtection(owner, repo, id)
if err != nil {
return to.ErrorResult(fmt.Errorf("delete tag protection err: %v", err))
}
return to.TextResult(map[string]string{"status": "deleted"})
}

157
operation/repo/topic.go Normal file
View File

@@ -0,0 +1,157 @@
package repo
import (
"context"
"errors"
"fmt"
"strings"
"git.lethalbits.com/lethalbits/gitea-mcp/pkg/gitea"
"git.lethalbits.com/lethalbits/gitea-mcp/pkg/log"
"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 (
ListRepoTopicsToolName = "list_repo_topics"
SetRepoTopicsToolName = "set_repo_topics"
AddRepoTopicToolName = "add_repo_topic"
DeleteRepoTopicToolName = "delete_repo_topic"
)
var (
ListRepoTopicsTool = mcp.NewTool(
ListRepoTopicsToolName,
mcp.WithDescription("List a repository's topics"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
)
SetRepoTopicsTool = mcp.NewTool(
SetRepoTopicsToolName,
mcp.WithDescription("Replace all topics of a repository with a new list"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithString("topics", mcp.Required(), mcp.Description("comma-separated list of topics")),
)
AddRepoTopicTool = mcp.NewTool(
AddRepoTopicToolName,
mcp.WithDescription("Add a topic to a repository"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithString("topic", mcp.Required(), mcp.Description("topic name to add")),
)
DeleteRepoTopicTool = mcp.NewTool(
DeleteRepoTopicToolName,
mcp.WithDescription("Remove a topic from a repository"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithString("topic", mcp.Required(), mcp.Description("topic name to remove")),
)
)
func init() {
Tool.RegisterRead(server.ServerTool{Tool: ListRepoTopicsTool, Handler: ListRepoTopicsFn})
Tool.RegisterWrite(server.ServerTool{Tool: SetRepoTopicsTool, Handler: SetRepoTopicsFn})
Tool.RegisterWrite(server.ServerTool{Tool: AddRepoTopicTool, Handler: AddRepoTopicFn})
Tool.RegisterWrite(server.ServerTool{Tool: DeleteRepoTopicTool, Handler: DeleteRepoTopicFn})
}
func ListRepoTopicsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ListRepoTopicsFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
if owner == "" || repo == "" {
return to.ErrorResult(errors.New("owner and repo are required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
topics, _, err := client.ListRepoTopics(owner, repo, gitea_sdk.ListRepoTopicsOptions{})
if err != nil {
return to.ErrorResult(fmt.Errorf("list repo topics err: %v", err))
}
return to.TextResult(topics)
}
func SetRepoTopicsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called SetRepoTopicsFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
topicsStr, _ := req.GetArguments()["topics"].(string)
if owner == "" || repo == "" {
return to.ErrorResult(errors.New("owner and repo are required"))
}
var topics []string
if topicsStr != "" {
for _, t := range splitAndTrim(topicsStr) {
if t != "" {
topics = append(topics, t)
}
}
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
_, err = client.SetRepoTopics(owner, repo, topics)
if err != nil {
return to.ErrorResult(fmt.Errorf("set repo topics err: %v", err))
}
return to.TextResult(map[string]any{"status": "updated", "topics": topics})
}
func AddRepoTopicFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called AddRepoTopicFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
topic, _ := req.GetArguments()["topic"].(string)
if owner == "" || repo == "" || topic == "" {
return to.ErrorResult(errors.New("owner, repo, and topic are required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
_, err = client.AddRepoTopic(owner, repo, topic)
if err != nil {
return to.ErrorResult(fmt.Errorf("add repo topic err: %v", err))
}
return to.TextResult(map[string]string{"status": "added", "topic": topic})
}
func DeleteRepoTopicFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called DeleteRepoTopicFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
topic, _ := req.GetArguments()["topic"].(string)
if owner == "" || repo == "" || topic == "" {
return to.ErrorResult(errors.New("owner, repo, and topic are required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
_, err = client.DeleteRepoTopic(owner, repo, topic)
if err != nil {
return to.ErrorResult(fmt.Errorf("delete repo topic err: %v", err))
}
return to.TextResult(map[string]string{"status": "deleted", "topic": topic})
}
func splitAndTrim(s string) []string {
var result []string
for _, part := range strings.Split(s, ",") {
trimmed := strings.TrimSpace(part)
if trimmed != "" {
result = append(result, trimmed)
}
}
return result
}

167
operation/repo/transfer.go Normal file
View File

@@ -0,0 +1,167 @@
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/to"
gitea_sdk "code.gitea.io/sdk/gitea"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
const (
TransferRepoToolName = "transfer_repo"
AcceptRepoTransferToolName = "accept_repo_transfer"
RejectRepoTransferToolName = "reject_repo_transfer"
CreateFromTemplateToolName = "create_repo_from_template"
)
var (
TransferRepoTool = mcp.NewTool(
TransferRepoToolName,
mcp.WithDescription("Transfer a repository to a new owner"),
mcp.WithString("owner", mcp.Required(), mcp.Description("current repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithString("new_owner", mcp.Required(), mcp.Description("new owner username or org")),
)
AcceptRepoTransferTool = mcp.NewTool(
AcceptRepoTransferToolName,
mcp.WithDescription("Accept a pending repository transfer"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
)
RejectRepoTransferTool = mcp.NewTool(
RejectRepoTransferToolName,
mcp.WithDescription("Reject a pending repository transfer"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
)
CreateFromTemplateTool = mcp.NewTool(
CreateFromTemplateToolName,
mcp.WithDescription("Create a repository from a template"),
mcp.WithString("template_owner", mcp.Required(), mcp.Description("template repository owner")),
mcp.WithString("template_repo", mcp.Required(), mcp.Description("template repository name")),
mcp.WithString("name", mcp.Required(), mcp.Description("name for the new repository")),
mcp.WithString("owner", mcp.Required(), mcp.Description("owner of the new repository (user or org)")),
mcp.WithString("description", mcp.Description("description for the new repository")),
mcp.WithBoolean("private", mcp.Description("whether the new repository is private")),
mcp.WithBoolean("git_content", mcp.Description("copy git content from template (default: true)")),
mcp.WithBoolean("topics", mcp.Description("copy topics from template")),
mcp.WithBoolean("labels", mcp.Description("copy labels from template")),
mcp.WithBoolean("webhooks", mcp.Description("copy webhooks from template")),
)
)
func init() {
Tool.RegisterWrite(server.ServerTool{Tool: TransferRepoTool, Handler: TransferRepoFn})
Tool.RegisterWrite(server.ServerTool{Tool: AcceptRepoTransferTool, Handler: AcceptRepoTransferFn})
Tool.RegisterWrite(server.ServerTool{Tool: RejectRepoTransferTool, Handler: RejectRepoTransferFn})
Tool.RegisterWrite(server.ServerTool{Tool: CreateFromTemplateTool, Handler: CreateFromTemplateFn})
}
func TransferRepoFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called TransferRepoFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
newOwner, _ := req.GetArguments()["new_owner"].(string)
if owner == "" || repo == "" || newOwner == "" {
return to.ErrorResult(errors.New("owner, repo, and new_owner are required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
repository, _, err := client.TransferRepo(owner, repo, gitea_sdk.TransferRepoOption{
NewOwner: newOwner,
})
if err != nil {
return to.ErrorResult(fmt.Errorf("transfer repo err: %v", err))
}
return to.TextResult(repository)
}
func AcceptRepoTransferFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called AcceptRepoTransferFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
if owner == "" || repo == "" {
return to.ErrorResult(errors.New("owner and repo are required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
repository, _, err := client.AcceptRepoTransfer(owner, repo)
if err != nil {
return to.ErrorResult(fmt.Errorf("accept repo transfer err: %v", err))
}
return to.TextResult(repository)
}
func RejectRepoTransferFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called RejectRepoTransferFn")
owner, _ := req.GetArguments()["owner"].(string)
repo, _ := req.GetArguments()["repo"].(string)
if owner == "" || repo == "" {
return to.ErrorResult(errors.New("owner and repo are required"))
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
repository, _, err := client.RejectRepoTransfer(owner, repo)
if err != nil {
return to.ErrorResult(fmt.Errorf("reject repo transfer err: %v", err))
}
return to.TextResult(repository)
}
func CreateFromTemplateFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called CreateFromTemplateFn")
templateOwner, _ := req.GetArguments()["template_owner"].(string)
templateRepo, _ := req.GetArguments()["template_repo"].(string)
name, _ := req.GetArguments()["name"].(string)
owner, _ := req.GetArguments()["owner"].(string)
if templateOwner == "" || templateRepo == "" || name == "" || owner == "" {
return to.ErrorResult(errors.New("template_owner, template_repo, name, and owner are required"))
}
opt := gitea_sdk.CreateRepoFromTemplateOption{
Name: name,
Owner: owner,
}
if v, ok := req.GetArguments()["description"].(string); ok {
opt.Description = v
}
if v, ok := req.GetArguments()["private"].(bool); ok {
opt.Private = v
}
if v, ok := req.GetArguments()["git_content"].(bool); ok {
opt.GitContent = v
}
if v, ok := req.GetArguments()["topics"].(bool); ok {
opt.Topics = v
}
if v, ok := req.GetArguments()["labels"].(bool); ok {
opt.Labels = v
}
if v, ok := req.GetArguments()["webhooks"].(bool); ok {
opt.Webhooks = v
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
repository, _, err := client.CreateRepoFromTemplate(templateOwner, templateRepo, opt)
if err != nil {
return to.ErrorResult(fmt.Errorf("create from template err: %v", err))
}
return to.TextResult(repository)
}