feat: add missing administration tools

Add 20+ new MCP tools for repository administration:

- Repository: get_repo, repo_write (edit/delete/transfer), list_forks
- Branch Protection: read (list/get) and write (create/edit/delete)
- Webhooks: read (list/get) and write (create/edit/delete/test)
- Collaborators: read (list) and write (add/delete)
- Deploy Keys: read (list/get) and write (create/delete)
- Topics: read (list) and write (add/delete/set)
- Compare: get_compare for branch/tag/commit comparison
- Organization: org_read (get/list_teams/get_team/list_team_members/list_team_repos)
  and org_write (create_team/delete_team/add_team_member/remove_team_member/add_team_repo/remove_team_repo)
- PR enhancements: list_files and is_merged methods added to pull_request_read

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-04 08:41:36 +00:00
parent 08128b9471
commit 4b4b5ee247
13 changed files with 2120 additions and 2 deletions
+1
View File
@@ -3,3 +3,4 @@ gitea-mcp
gitea-mcp.exe
*.log
tmp
gitea-mcp-new
+4
View File
@@ -16,6 +16,7 @@ import (
"gitea.com/gitea/gitea-mcp/operation/label"
"gitea.com/gitea/gitea-mcp/operation/milestone"
"gitea.com/gitea/gitea-mcp/operation/notification"
"gitea.com/gitea/gitea-mcp/operation/org"
"gitea.com/gitea/gitea-mcp/operation/pull"
"gitea.com/gitea/gitea-mcp/operation/repo"
"gitea.com/gitea/gitea-mcp/operation/search"
@@ -69,6 +70,9 @@ func RegisterTool(s *server.MCPServer) {
// Time Tracking Tool
s.AddTools(timetracking.Tool.Tools()...)
// Organization Tool
s.AddTools(org.Tool.Tools()...)
s.DeleteTools("")
}
+379
View File
@@ -0,0 +1,379 @@
package org
import (
"context"
"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"
gitea_sdk "code.gitea.io/sdk/gitea"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
var Tool = tool.New()
const (
OrgReadToolName = "org_read"
OrgWriteToolName = "org_write"
)
var (
OrgReadTool = mcp.NewTool(
OrgReadToolName,
mcp.WithDescription("Read organization and team information. Methods: 'get' org details, 'list_teams', 'get_team', 'list_team_members', 'list_team_repos'."),
mcp.WithString("method", mcp.Required(), mcp.Description("operation to perform"), mcp.Enum("get", "list_teams", "get_team", "list_team_members", "list_team_repos")),
mcp.WithString("org", mcp.Description("organization name (required for 'get', 'list_teams')")),
mcp.WithNumber("team_id", mcp.Description("team ID (required for 'get_team', 'list_team_members', 'list_team_repos')")),
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)),
mcp.WithNumber("perPage", mcp.Description("results per page"), mcp.DefaultNumber(30)),
)
OrgWriteTool = mcp.NewTool(
OrgWriteToolName,
mcp.WithDescription("Manage organization teams and membership. Methods: 'create_team', 'delete_team', 'add_team_member', 'remove_team_member', 'add_team_repo', 'remove_team_repo'."),
mcp.WithString("method", mcp.Required(), mcp.Description("operation to perform"), mcp.Enum("create_team", "delete_team", "add_team_member", "remove_team_member", "add_team_repo", "remove_team_repo")),
mcp.WithString("org", mcp.Description("organization name (required for 'create_team', 'add_team_repo', 'remove_team_repo')")),
mcp.WithNumber("team_id", mcp.Description("team ID (required for 'delete_team', 'add_team_member', 'remove_team_member', 'add_team_repo', 'remove_team_repo')")),
mcp.WithString("username", mcp.Description("username (required for 'add_team_member', 'remove_team_member')")),
mcp.WithString("repo", mcp.Description("repository name (required for 'add_team_repo', 'remove_team_repo')")),
mcp.WithString("name", mcp.Description("team name (required for 'create_team')")),
mcp.WithString("description", mcp.Description("team description (for 'create_team')")),
mcp.WithString("permission", mcp.Description("team permission level (for 'create_team')"), mcp.Enum("read", "write", "admin")),
mcp.WithArray("units", mcp.Description("team units (for 'create_team'), e.g. repo.code, repo.issues, repo.pulls"), mcp.Items(map[string]any{"type": "string"})),
mcp.WithBoolean("includes_all_repositories", mcp.Description("include all org repos (for 'create_team')")),
)
)
func init() {
Tool.RegisterRead(server.ServerTool{
Tool: OrgReadTool,
Handler: orgReadFn,
})
Tool.RegisterWrite(server.ServerTool{
Tool: OrgWriteTool,
Handler: orgWriteFn,
})
}
func orgReadFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
args := req.GetArguments()
method, err := params.GetString(args, "method")
if err != nil {
return to.ErrorResult(err)
}
switch method {
case "get":
return getOrgFn(ctx, req)
case "list_teams":
return listTeamsFn(ctx, req)
case "get_team":
return getTeamFn(ctx, req)
case "list_team_members":
return listTeamMembersFn(ctx, req)
case "list_team_repos":
return listTeamReposFn(ctx, req)
default:
return to.ErrorResult(fmt.Errorf("unknown method: %s", method))
}
}
func getOrgFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called getOrgFn")
args := req.GetArguments()
orgName, err := params.GetString(args, "org")
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))
}
o, _, err := client.GetOrg(orgName)
if err != nil {
return to.ErrorResult(fmt.Errorf("get org %v err: %v", orgName, err))
}
return to.TextResult(slimOrg(o))
}
func listTeamsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called listTeamsFn")
args := req.GetArguments()
orgName, err := params.GetString(args, "org")
if err != nil {
return to.ErrorResult(err)
}
page, pageSize := params.GetPagination(args, 30)
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
teams, _, err := client.ListOrgTeams(orgName, gitea_sdk.ListTeamsOptions{
ListOptions: gitea_sdk.ListOptions{
Page: page,
PageSize: pageSize,
},
})
if err != nil {
return to.ErrorResult(fmt.Errorf("list teams for org %v err: %v", orgName, err))
}
return to.TextResult(slimTeams(teams))
}
func getTeamFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called getTeamFn")
args := req.GetArguments()
teamID, err := params.GetIndex(args, "team_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))
}
team, _, err := client.GetTeam(teamID)
if err != nil {
return to.ErrorResult(fmt.Errorf("get team %v err: %v", teamID, err))
}
return to.TextResult(slimTeam(team))
}
func listTeamMembersFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called listTeamMembersFn")
args := req.GetArguments()
teamID, err := params.GetIndex(args, "team_id")
if err != nil {
return to.ErrorResult(err)
}
page, pageSize := params.GetPagination(args, 30)
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
members, _, err := client.ListTeamMembers(teamID, gitea_sdk.ListTeamMembersOptions{
ListOptions: gitea_sdk.ListOptions{
Page: page,
PageSize: pageSize,
},
})
if err != nil {
return to.ErrorResult(fmt.Errorf("list team %v members err: %v", teamID, err))
}
return to.TextResult(slimUsers(members))
}
func listTeamReposFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called listTeamReposFn")
args := req.GetArguments()
teamID, err := params.GetIndex(args, "team_id")
if err != nil {
return to.ErrorResult(err)
}
page, pageSize := params.GetPagination(args, 30)
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
repos, _, err := client.ListTeamRepositories(teamID, gitea_sdk.ListTeamRepositoriesOptions{
ListOptions: gitea_sdk.ListOptions{
Page: page,
PageSize: pageSize,
},
})
if err != nil {
return to.ErrorResult(fmt.Errorf("list team %v repos err: %v", teamID, err))
}
return to.TextResult(slimRepos(repos))
}
func orgWriteFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
args := req.GetArguments()
method, err := params.GetString(args, "method")
if err != nil {
return to.ErrorResult(err)
}
switch method {
case "create_team":
return createTeamFn(ctx, req)
case "delete_team":
return deleteTeamFn(ctx, req)
case "add_team_member":
return addTeamMemberFn(ctx, req)
case "remove_team_member":
return removeTeamMemberFn(ctx, req)
case "add_team_repo":
return addTeamRepoFn(ctx, req)
case "remove_team_repo":
return removeTeamRepoFn(ctx, req)
default:
return to.ErrorResult(fmt.Errorf("unknown method: %s", method))
}
}
func createTeamFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called createTeamFn")
args := req.GetArguments()
orgName, err := params.GetString(args, "org")
if err != nil {
return to.ErrorResult(err)
}
name, err := params.GetString(args, "name")
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))
}
opt := gitea_sdk.CreateTeamOption{
Name: name,
}
if v, ok := args["description"].(string); ok {
opt.Description = v
}
if v, ok := args["permission"].(string); ok {
switch v {
case "read":
opt.Permission = gitea_sdk.AccessModeRead
case "write":
opt.Permission = gitea_sdk.AccessModeWrite
case "admin":
opt.Permission = gitea_sdk.AccessModeAdmin
}
}
if unitStrings := params.GetStringSlice(args, "units"); unitStrings != nil {
units := make([]gitea_sdk.RepoUnitType, 0, len(unitStrings))
for _, u := range unitStrings {
units = append(units, gitea_sdk.RepoUnitType(u))
}
opt.Units = units
}
if v, ok := args["includes_all_repositories"].(bool); ok {
opt.IncludesAllRepositories = v
}
team, _, err := client.CreateTeam(orgName, opt)
if err != nil {
return to.ErrorResult(fmt.Errorf("create team in org %v err: %v", orgName, err))
}
return to.TextResult(slimTeam(team))
}
func deleteTeamFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called deleteTeamFn")
args := req.GetArguments()
teamID, err := params.GetIndex(args, "team_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.DeleteTeam(teamID)
if err != nil {
return to.ErrorResult(fmt.Errorf("delete team %v err: %v", teamID, err))
}
return mcp.NewToolResultText(fmt.Sprintf("Team %d deleted", teamID)), nil
}
func addTeamMemberFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called addTeamMemberFn")
args := req.GetArguments()
teamID, err := params.GetIndex(args, "team_id")
if err != nil {
return to.ErrorResult(err)
}
username, err := params.GetString(args, "username")
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.AddTeamMember(teamID, username)
if err != nil {
return to.ErrorResult(fmt.Errorf("add %v to team %v err: %v", username, teamID, err))
}
return mcp.NewToolResultText(fmt.Sprintf("User %s added to team %d", username, teamID)), nil
}
func removeTeamMemberFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called removeTeamMemberFn")
args := req.GetArguments()
teamID, err := params.GetIndex(args, "team_id")
if err != nil {
return to.ErrorResult(err)
}
username, err := params.GetString(args, "username")
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.RemoveTeamMember(teamID, username)
if err != nil {
return to.ErrorResult(fmt.Errorf("remove %v from team %v err: %v", username, teamID, err))
}
return mcp.NewToolResultText(fmt.Sprintf("User %s removed from team %d", username, teamID)), nil
}
func addTeamRepoFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called addTeamRepoFn")
args := req.GetArguments()
teamID, err := params.GetIndex(args, "team_id")
if err != nil {
return to.ErrorResult(err)
}
orgName, err := params.GetString(args, "org")
if err != nil {
return to.ErrorResult(err)
}
repoName, err := params.GetString(args, "repo")
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.AddTeamRepository(teamID, orgName, repoName)
if err != nil {
return to.ErrorResult(fmt.Errorf("add repo %v/%v to team %v err: %v", orgName, repoName, teamID, err))
}
return mcp.NewToolResultText(fmt.Sprintf("Repo %s/%s added to team %d", orgName, repoName, teamID)), nil
}
func removeTeamRepoFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called removeTeamRepoFn")
args := req.GetArguments()
teamID, err := params.GetIndex(args, "team_id")
if err != nil {
return to.ErrorResult(err)
}
orgName, err := params.GetString(args, "org")
if err != nil {
return to.ErrorResult(err)
}
repoName, err := params.GetString(args, "repo")
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.RemoveTeamRepository(teamID, orgName, repoName)
if err != nil {
return to.ErrorResult(fmt.Errorf("remove repo %v/%v from team %v err: %v", orgName, repoName, teamID, err))
}
return mcp.NewToolResultText(fmt.Sprintf("Repo %s/%s removed from team %d", orgName, repoName, teamID)), nil
}
+85
View File
@@ -0,0 +1,85 @@
package org
import (
gitea_sdk "code.gitea.io/sdk/gitea"
)
func slimOrg(o *gitea_sdk.Organization) map[string]any {
if o == nil {
return nil
}
return map[string]any{
"id": o.ID,
"username": o.UserName,
"full_name": o.FullName,
"description": o.Description,
"website": o.Website,
"location": o.Location,
"visibility": o.Visibility,
"avatar_url": o.AvatarURL,
}
}
func slimTeam(t *gitea_sdk.Team) map[string]any {
if t == nil {
return nil
}
return map[string]any{
"id": t.ID,
"name": t.Name,
"description": t.Description,
"permission": t.Permission,
"units": t.Units,
"includes_all_repositories": t.IncludesAllRepositories,
"can_create_org_repo": t.CanCreateOrgRepo,
}
}
func slimTeams(teams []*gitea_sdk.Team) []map[string]any {
out := make([]map[string]any, 0, len(teams))
for _, t := range teams {
out = append(out, slimTeam(t))
}
return out
}
func slimUser(u *gitea_sdk.User) map[string]any {
if u == nil {
return nil
}
return map[string]any{
"id": u.ID,
"login": u.UserName,
"full_name": u.FullName,
"email": u.Email,
"avatar_url": u.AvatarURL,
}
}
func slimUsers(users []*gitea_sdk.User) []map[string]any {
out := make([]map[string]any, 0, len(users))
for _, u := range users {
out = append(out, slimUser(u))
}
return out
}
func slimRepos(repos []*gitea_sdk.Repository) []map[string]any {
out := make([]map[string]any, 0, len(repos))
for _, r := range repos {
if r == nil {
continue
}
m := map[string]any{
"id": r.ID,
"full_name": r.FullName,
"html_url": r.HTMLURL,
"private": r.Private,
}
if r.Owner != nil {
m["owner"] = r.Owner.UserName
}
out = append(out, m)
}
return out
}
+76 -2
View File
@@ -40,8 +40,8 @@ var (
PullRequestReadTool = mcp.NewTool(
PullRequestReadToolName,
mcp.WithDescription("Get pull request information. Use method 'get' for PR details, 'get_diff' for diff, 'get_reviews'/'get_review'/'get_review_comments' for review data."),
mcp.WithString("method", mcp.Required(), mcp.Description("operation to perform"), mcp.Enum("get", "get_diff", "get_reviews", "get_review", "get_review_comments")),
mcp.WithDescription("Get pull request information. Use method 'get' for PR details, 'get_diff' for diff, 'get_reviews'/'get_review'/'get_review_comments' for review data, 'list_files' for changed files, 'is_merged' to check merge status."),
mcp.WithString("method", mcp.Required(), mcp.Description("operation to perform"), mcp.Enum("get", "get_diff", "get_reviews", "get_review", "get_review_comments", "list_files", "is_merged")),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithNumber("index", mcp.Required(), mcp.Description("pull request index")),
@@ -140,11 +140,85 @@ func pullRequestReadFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallT
return getPullRequestReviewFn(ctx, req)
case "get_review_comments":
return listPullRequestReviewCommentsFn(ctx, req)
case "list_files":
return listPullRequestFilesFn(ctx, req)
case "is_merged":
return isPullRequestMergedFn(ctx, req)
default:
return to.ErrorResult(fmt.Errorf("unknown method: %s", method))
}
}
func listPullRequestFilesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called listPullRequestFilesFn")
args := req.GetArguments()
owner, err := params.GetString(args, "owner")
if err != nil {
return to.ErrorResult(err)
}
repoName, err := params.GetString(args, "repo")
if err != nil {
return to.ErrorResult(err)
}
index, err := params.GetIndex(args, "index")
if err != nil {
return to.ErrorResult(err)
}
page, pageSize := params.GetPagination(args, 30)
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
files, _, err := client.ListPullRequestFiles(owner, repoName, index, gitea_sdk.ListPullRequestFilesOptions{
ListOptions: gitea_sdk.ListOptions{
Page: page,
PageSize: pageSize,
},
})
if err != nil {
return to.ErrorResult(fmt.Errorf("list PR files %v/%v#%v err: %v", owner, repoName, index, err))
}
out := make([]map[string]any, 0, len(files))
for _, f := range files {
out = append(out, map[string]any{
"filename": f.Filename,
"status": f.Status,
"additions": f.Additions,
"deletions": f.Deletions,
"changes": f.Changes,
})
}
return to.TextResult(out)
}
func isPullRequestMergedFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called isPullRequestMergedFn")
args := req.GetArguments()
owner, err := params.GetString(args, "owner")
if err != nil {
return to.ErrorResult(err)
}
repoName, err := params.GetString(args, "repo")
if err != nil {
return to.ErrorResult(err)
}
index, err := params.GetIndex(args, "index")
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))
}
merged, _, err := client.IsPullRequestMerged(owner, repoName, index)
if err != nil {
return to.ErrorResult(fmt.Errorf("check PR merged %v/%v#%v err: %v", owner, repoName, index, err))
}
return to.TextResult(map[string]any{"merged": merged})
}
func pullRequestWriteFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
method, err := params.GetString(req.GetArguments(), "method")
if err != nil {
+309
View File
@@ -0,0 +1,309 @@
package repo
import (
"context"
"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_sdk "code.gitea.io/sdk/gitea"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
const (
BranchProtectionReadToolName = "branch_protection_read"
BranchProtectionWriteToolName = "branch_protection_write"
)
var (
BranchProtectionReadTool = mcp.NewTool(
BranchProtectionReadToolName,
mcp.WithDescription("Read branch protection rules. Use method 'list' to list all rules, 'get' to get a specific rule."),
mcp.WithString("method", mcp.Required(), mcp.Description("operation to perform"), mcp.Enum("list", "get")),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithString("name", mcp.Description("branch protection rule name (required for 'get')")),
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)),
mcp.WithNumber("perPage", mcp.Description("results per page"), mcp.DefaultNumber(30)),
)
BranchProtectionWriteTool = mcp.NewTool(
BranchProtectionWriteToolName,
mcp.WithDescription("Create, edit, or delete branch protection rules."),
mcp.WithString("method", mcp.Required(), mcp.Description("operation to perform"), mcp.Enum("create", "edit", "delete")),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithString("branch_name", mcp.Description("branch name or pattern (required for 'create')")),
mcp.WithString("rule_name", mcp.Description("rule name (required for 'edit', 'delete')")),
mcp.WithBoolean("enable_push", mcp.Description("enable push to protected branch")),
mcp.WithArray("push_whitelist_usernames", mcp.Description("usernames allowed to push"), mcp.Items(map[string]any{"type": "string"})),
mcp.WithArray("push_whitelist_teams", mcp.Description("team names allowed to push"), mcp.Items(map[string]any{"type": "string"})),
mcp.WithBoolean("enable_merge_whitelist", mcp.Description("enable merge whitelist")),
mcp.WithArray("merge_whitelist_usernames", mcp.Description("usernames allowed to merge"), mcp.Items(map[string]any{"type": "string"})),
mcp.WithArray("merge_whitelist_teams", mcp.Description("team names allowed to merge"), mcp.Items(map[string]any{"type": "string"})),
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("block_on_outdated_branch", mcp.Description("block merge on outdated branch")),
mcp.WithBoolean("dismiss_stale_approvals", mcp.Description("dismiss stale approvals on new commits")),
mcp.WithBoolean("require_signed_commits", mcp.Description("require signed commits")),
mcp.WithString("protected_file_patterns", mcp.Description("protected file patterns (semicolon separated)")),
mcp.WithString("unprotected_file_patterns", mcp.Description("unprotected file patterns (semicolon separated)")),
)
)
func init() {
Tool.RegisterRead(server.ServerTool{
Tool: BranchProtectionReadTool,
Handler: branchProtectionReadFn,
})
Tool.RegisterWrite(server.ServerTool{
Tool: BranchProtectionWriteTool,
Handler: branchProtectionWriteFn,
})
}
func branchProtectionReadFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
args := req.GetArguments()
method, err := params.GetString(args, "method")
if err != nil {
return to.ErrorResult(err)
}
switch method {
case "list":
return listBranchProtectionsFn(ctx, req)
case "get":
return getBranchProtectionFn(ctx, req)
default:
return to.ErrorResult(fmt.Errorf("unknown method: %s", method))
}
}
func listBranchProtectionsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called listBranchProtectionsFn")
args := req.GetArguments()
owner, err := params.GetString(args, "owner")
if err != nil {
return to.ErrorResult(err)
}
repo, err := params.GetString(args, "repo")
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))
}
bps, _, err := client.ListBranchProtections(owner, repo, gitea_sdk.ListBranchProtectionsOptions{})
if err != nil {
return to.ErrorResult(fmt.Errorf("list branch protections %v/%v err: %v", owner, repo, err))
}
return to.TextResult(slimBranchProtections(bps))
}
func getBranchProtectionFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called getBranchProtectionFn")
args := req.GetArguments()
owner, err := params.GetString(args, "owner")
if err != nil {
return to.ErrorResult(err)
}
repo, err := params.GetString(args, "repo")
if err != nil {
return to.ErrorResult(err)
}
name, err := params.GetString(args, "name")
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))
}
bp, _, err := client.GetBranchProtection(owner, repo, name)
if err != nil {
return to.ErrorResult(fmt.Errorf("get branch protection %v in %v/%v err: %v", name, owner, repo, err))
}
return to.TextResult(slimBranchProtection(bp))
}
func branchProtectionWriteFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
args := req.GetArguments()
method, err := params.GetString(args, "method")
if err != nil {
return to.ErrorResult(err)
}
switch method {
case "create":
return createBranchProtectionFn(ctx, req)
case "edit":
return editBranchProtectionFn(ctx, req)
case "delete":
return deleteBranchProtectionFn(ctx, req)
default:
return to.ErrorResult(fmt.Errorf("unknown method: %s", method))
}
}
func createBranchProtectionFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called createBranchProtectionFn")
args := req.GetArguments()
owner, err := params.GetString(args, "owner")
if err != nil {
return to.ErrorResult(err)
}
repo, err := params.GetString(args, "repo")
if err != nil {
return to.ErrorResult(err)
}
branchName, err := params.GetString(args, "branch_name")
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))
}
opt := gitea_sdk.CreateBranchProtectionOption{
BranchName: branchName,
}
if v, ok := args["enable_push"].(bool); ok {
opt.EnablePush = v
}
opt.PushWhitelistUsernames = params.GetStringSlice(args, "push_whitelist_usernames")
opt.PushWhitelistTeams = params.GetStringSlice(args, "push_whitelist_teams")
if v, ok := args["enable_merge_whitelist"].(bool); ok {
opt.EnableMergeWhitelist = v
}
opt.MergeWhitelistUsernames = params.GetStringSlice(args, "merge_whitelist_usernames")
opt.MergeWhitelistTeams = params.GetStringSlice(args, "merge_whitelist_teams")
if val, exists := args["required_approvals"]; exists {
if i, ok := params.ToInt64(val); ok {
opt.RequiredApprovals = i
}
}
if v, ok := args["block_on_rejected_reviews"].(bool); ok {
opt.BlockOnRejectedReviews = v
}
if v, ok := args["block_on_outdated_branch"].(bool); ok {
opt.BlockOnOutdatedBranch = v
}
if v, ok := args["dismiss_stale_approvals"].(bool); ok {
opt.DismissStaleApprovals = v
}
if v, ok := args["require_signed_commits"].(bool); ok {
opt.RequireSignedCommits = v
}
if v, ok := args["protected_file_patterns"].(string); ok {
opt.ProtectedFilePatterns = v
}
if v, ok := args["unprotected_file_patterns"].(string); ok {
opt.UnprotectedFilePatterns = v
}
bp, _, err := client.CreateBranchProtection(owner, repo, opt)
if err != nil {
return to.ErrorResult(fmt.Errorf("create branch protection in %v/%v err: %v", owner, repo, err))
}
return to.TextResult(slimBranchProtection(bp))
}
func editBranchProtectionFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called editBranchProtectionFn")
args := req.GetArguments()
owner, err := params.GetString(args, "owner")
if err != nil {
return to.ErrorResult(err)
}
repo, err := params.GetString(args, "repo")
if err != nil {
return to.ErrorResult(err)
}
ruleName, err := params.GetString(args, "rule_name")
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))
}
opt := gitea_sdk.EditBranchProtectionOption{}
if v, ok := args["enable_push"].(bool); ok {
opt.EnablePush = &v
}
if v := params.GetStringSlice(args, "push_whitelist_usernames"); v != nil {
opt.PushWhitelistUsernames = v
}
if v := params.GetStringSlice(args, "push_whitelist_teams"); v != nil {
opt.PushWhitelistTeams = v
}
if v, ok := args["enable_merge_whitelist"].(bool); ok {
opt.EnableMergeWhitelist = &v
}
if v := params.GetStringSlice(args, "merge_whitelist_usernames"); v != nil {
opt.MergeWhitelistUsernames = v
}
if v := params.GetStringSlice(args, "merge_whitelist_teams"); v != nil {
opt.MergeWhitelistTeams = v
}
if val, exists := args["required_approvals"]; exists {
if i, ok := params.ToInt64(val); ok {
opt.RequiredApprovals = &i
}
}
if v, ok := args["block_on_rejected_reviews"].(bool); ok {
opt.BlockOnRejectedReviews = &v
}
if v, ok := args["block_on_outdated_branch"].(bool); ok {
opt.BlockOnOutdatedBranch = &v
}
if v, ok := args["dismiss_stale_approvals"].(bool); ok {
opt.DismissStaleApprovals = &v
}
if v, ok := args["require_signed_commits"].(bool); ok {
opt.RequireSignedCommits = &v
}
if v, ok := args["protected_file_patterns"].(string); ok {
opt.ProtectedFilePatterns = &v
}
if v, ok := args["unprotected_file_patterns"].(string); ok {
opt.UnprotectedFilePatterns = &v
}
bp, _, err := client.EditBranchProtection(owner, repo, ruleName, opt)
if err != nil {
return to.ErrorResult(fmt.Errorf("edit branch protection %v in %v/%v err: %v", ruleName, owner, repo, err))
}
return to.TextResult(slimBranchProtection(bp))
}
func deleteBranchProtectionFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called deleteBranchProtectionFn")
args := req.GetArguments()
owner, err := params.GetString(args, "owner")
if err != nil {
return to.ErrorResult(err)
}
repo, err := params.GetString(args, "repo")
if err != nil {
return to.ErrorResult(err)
}
ruleName, err := params.GetString(args, "rule_name")
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.DeleteBranchProtection(owner, repo, ruleName)
if err != nil {
return to.ErrorResult(fmt.Errorf("delete branch protection %v from %v/%v err: %v", ruleName, owner, repo, err))
}
return mcp.NewToolResultText(fmt.Sprintf("Branch protection rule '%s' deleted from %s/%s", ruleName, owner, repo)), nil
}
+168
View File
@@ -0,0 +1,168 @@
package repo
import (
"context"
"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_sdk "code.gitea.io/sdk/gitea"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
const (
CollaboratorReadToolName = "collaborator_read"
CollaboratorWriteToolName = "collaborator_write"
)
var (
CollaboratorReadTool = mcp.NewTool(
CollaboratorReadToolName,
mcp.WithDescription("List repository collaborators."),
mcp.WithString("method", mcp.Required(), mcp.Description("operation to perform"), mcp.Enum("list")),
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("perPage", mcp.Description("results per page"), mcp.DefaultNumber(30)),
)
CollaboratorWriteTool = mcp.NewTool(
CollaboratorWriteToolName,
mcp.WithDescription("Add or remove repository collaborators. Use method 'add' to grant access, 'delete' to revoke."),
mcp.WithString("method", mcp.Required(), mcp.Description("operation to perform"), mcp.Enum("add", "delete")),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithString("username", mcp.Required(), mcp.Description("collaborator username")),
mcp.WithString("permission", mcp.Description("permission level (for 'add')"), mcp.Enum("read", "write", "admin")),
)
)
func init() {
Tool.RegisterRead(server.ServerTool{
Tool: CollaboratorReadTool,
Handler: collaboratorReadFn,
})
Tool.RegisterWrite(server.ServerTool{
Tool: CollaboratorWriteTool,
Handler: collaboratorWriteFn,
})
}
func collaboratorReadFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
args := req.GetArguments()
method, err := params.GetString(args, "method")
if err != nil {
return to.ErrorResult(err)
}
switch method {
case "list":
return listCollaboratorsFn(ctx, req)
default:
return to.ErrorResult(fmt.Errorf("unknown method: %s", method))
}
}
func listCollaboratorsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called listCollaboratorsFn")
args := req.GetArguments()
owner, err := params.GetString(args, "owner")
if err != nil {
return to.ErrorResult(err)
}
repo, err := params.GetString(args, "repo")
if err != nil {
return to.ErrorResult(err)
}
page, pageSize := params.GetPagination(args, 30)
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: page,
PageSize: pageSize,
},
})
if err != nil {
return to.ErrorResult(fmt.Errorf("list collaborators %v/%v err: %v", owner, repo, err))
}
return to.TextResult(slimUsers(users))
}
func collaboratorWriteFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
args := req.GetArguments()
method, err := params.GetString(args, "method")
if err != nil {
return to.ErrorResult(err)
}
switch method {
case "add":
return addCollaboratorFn(ctx, req)
case "delete":
return deleteCollaboratorFn(ctx, req)
default:
return to.ErrorResult(fmt.Errorf("unknown method: %s", method))
}
}
func addCollaboratorFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called addCollaboratorFn")
args := req.GetArguments()
owner, err := params.GetString(args, "owner")
if err != nil {
return to.ErrorResult(err)
}
repo, err := params.GetString(args, "repo")
if err != nil {
return to.ErrorResult(err)
}
username, err := params.GetString(args, "username")
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))
}
opt := gitea_sdk.AddCollaboratorOption{}
if perm, ok := args["permission"].(string); ok {
am := gitea_sdk.AccessMode(perm)
opt.Permission = &am
}
_, err = client.AddCollaborator(owner, repo, username, opt)
if err != nil {
return to.ErrorResult(fmt.Errorf("add collaborator %v to %v/%v err: %v", username, owner, repo, err))
}
return mcp.NewToolResultText(fmt.Sprintf("Collaborator %s added to %s/%s", username, owner, repo)), nil
}
func deleteCollaboratorFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called deleteCollaboratorFn")
args := req.GetArguments()
owner, err := params.GetString(args, "owner")
if err != nil {
return to.ErrorResult(err)
}
repo, err := params.GetString(args, "repo")
if err != nil {
return to.ErrorResult(err)
}
username, err := params.GetString(args, "username")
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.DeleteCollaborator(owner, repo, username)
if err != nil {
return to.ErrorResult(fmt.Errorf("delete collaborator %v from %v/%v err: %v", username, owner, repo, err))
}
return mcp.NewToolResultText(fmt.Sprintf("Collaborator %s removed from %s/%s", username, owner, repo)), nil
}
+59
View File
@@ -0,0 +1,59 @@
package repo
import (
"context"
"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"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
const (
GetCompareToolName = "get_compare"
)
var (
GetCompareTool = mcp.NewTool(
GetCompareToolName,
mcp.WithDescription("Compare two branches, tags, or commits. Returns commits and diff stats between the base and head."),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithString("basehead", mcp.Required(), mcp.Description("comparison in format 'base...head' (e.g. 'main...feature-branch')")),
)
)
func init() {
Tool.RegisterRead(server.ServerTool{
Tool: GetCompareTool,
Handler: getCompareFn,
})
}
func getCompareFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called getCompareFn")
args := req.GetArguments()
owner, err := params.GetString(args, "owner")
if err != nil {
return to.ErrorResult(err)
}
repo, err := params.GetString(args, "repo")
if err != nil {
return to.ErrorResult(err)
}
basehead, err := params.GetString(args, "basehead")
if err != nil {
return to.ErrorResult(err)
}
var result map[string]any
_, err = gitea.DoJSON(ctx, "GET", fmt.Sprintf("repos/%s/%s/compare/%s", owner, repo, basehead), nil, nil, &result)
if err != nil {
return to.ErrorResult(fmt.Errorf("compare %v in %v/%v err: %v", basehead, owner, repo, err))
}
return to.TextResult(result)
}
+206
View File
@@ -0,0 +1,206 @@
package repo
import (
"context"
"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_sdk "code.gitea.io/sdk/gitea"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
const (
DeployKeyReadToolName = "deploy_key_read"
DeployKeyWriteToolName = "deploy_key_write"
)
var (
DeployKeyReadTool = mcp.NewTool(
DeployKeyReadToolName,
mcp.WithDescription("Read deploy keys. Use method 'list' to list all, 'get' to get a specific key."),
mcp.WithString("method", mcp.Required(), mcp.Description("operation to perform"), mcp.Enum("list", "get")),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithNumber("id", mcp.Description("deploy key ID (required for 'get')")),
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)),
mcp.WithNumber("perPage", mcp.Description("results per page"), mcp.DefaultNumber(30)),
)
DeployKeyWriteTool = mcp.NewTool(
DeployKeyWriteToolName,
mcp.WithDescription("Create or delete deploy keys. Use method 'create' to add a new key, 'delete' to remove."),
mcp.WithString("method", mcp.Required(), mcp.Description("operation to perform"), mcp.Enum("create", "delete")),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithNumber("id", mcp.Description("deploy key ID (required for 'delete')")),
mcp.WithString("title", mcp.Description("key title (required for 'create')")),
mcp.WithString("key", mcp.Description("SSH public key content (required for 'create')")),
mcp.WithBoolean("read_only", mcp.Description("whether the key has read-only access (default true)")),
)
)
func init() {
Tool.RegisterRead(server.ServerTool{
Tool: DeployKeyReadTool,
Handler: deployKeyReadFn,
})
Tool.RegisterWrite(server.ServerTool{
Tool: DeployKeyWriteTool,
Handler: deployKeyWriteFn,
})
}
func deployKeyReadFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
args := req.GetArguments()
method, err := params.GetString(args, "method")
if err != nil {
return to.ErrorResult(err)
}
switch method {
case "list":
return listDeployKeysFn(ctx, req)
case "get":
return getDeployKeyFn(ctx, req)
default:
return to.ErrorResult(fmt.Errorf("unknown method: %s", method))
}
}
func listDeployKeysFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called listDeployKeysFn")
args := req.GetArguments()
owner, err := params.GetString(args, "owner")
if err != nil {
return to.ErrorResult(err)
}
repo, err := params.GetString(args, "repo")
if err != nil {
return to.ErrorResult(err)
}
page, pageSize := params.GetPagination(args, 30)
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: page,
PageSize: pageSize,
},
})
if err != nil {
return to.ErrorResult(fmt.Errorf("list deploy keys %v/%v err: %v", owner, repo, err))
}
return to.TextResult(slimDeployKeys(keys))
}
func getDeployKeyFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called getDeployKeyFn")
args := req.GetArguments()
owner, err := params.GetString(args, "owner")
if err != nil {
return to.ErrorResult(err)
}
repo, err := params.GetString(args, "repo")
if err != nil {
return to.ErrorResult(err)
}
id, err := params.GetIndex(args, "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, id)
if err != nil {
return to.ErrorResult(fmt.Errorf("get deploy key %v in %v/%v err: %v", id, owner, repo, err))
}
return to.TextResult(slimDeployKey(key))
}
func deployKeyWriteFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
args := req.GetArguments()
method, err := params.GetString(args, "method")
if err != nil {
return to.ErrorResult(err)
}
switch method {
case "create":
return createDeployKeyFn(ctx, req)
case "delete":
return deleteDeployKeyFn(ctx, req)
default:
return to.ErrorResult(fmt.Errorf("unknown method: %s", method))
}
}
func createDeployKeyFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called createDeployKeyFn")
args := req.GetArguments()
owner, err := params.GetString(args, "owner")
if err != nil {
return to.ErrorResult(err)
}
repo, err := params.GetString(args, "repo")
if err != nil {
return to.ErrorResult(err)
}
title, err := params.GetString(args, "title")
if err != nil {
return to.ErrorResult(err)
}
key, err := params.GetString(args, "key")
if err != nil {
return to.ErrorResult(err)
}
readOnly := true
if v, ok := args["read_only"].(bool); ok {
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, gitea_sdk.CreateKeyOption{
Title: title,
Key: key,
ReadOnly: readOnly,
})
if err != nil {
return to.ErrorResult(fmt.Errorf("create deploy key in %v/%v err: %v", owner, repo, err))
}
return to.TextResult(slimDeployKey(dk))
}
func deleteDeployKeyFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called deleteDeployKeyFn")
args := req.GetArguments()
owner, err := params.GetString(args, "owner")
if err != nil {
return to.ErrorResult(err)
}
repo, err := params.GetString(args, "repo")
if err != nil {
return to.ErrorResult(err)
}
id, err := params.GetIndex(args, "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, id)
if err != nil {
return to.ErrorResult(fmt.Errorf("delete deploy key %v from %v/%v err: %v", id, owner, repo, err))
}
return mcp.NewToolResultText(fmt.Sprintf("Deploy key %d deleted from %s/%s", id, owner, repo)), nil
}
+220
View File
@@ -23,6 +23,9 @@ const (
ForkRepoToolName = "fork_repo"
ListMyReposToolName = "list_my_repos"
ListOrgReposToolName = "list_org_repos"
GetRepoToolName = "get_repo"
RepoWriteToolName = "repo_write"
ListForksToolName = "list_forks"
)
var (
@@ -67,6 +70,40 @@ var (
mcp.WithNumber("page", mcp.Required(), mcp.Description("Page number"), mcp.DefaultNumber(1), mcp.Min(1)),
mcp.WithNumber("pageSize", mcp.Required(), mcp.Description("Page size number"), mcp.DefaultNumber(100), mcp.Min(1)),
)
GetRepoTool = mcp.NewTool(
GetRepoToolName,
mcp.WithDescription("Get repository details including settings, permissions, and metadata"),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
)
RepoWriteTool = mcp.NewTool(
RepoWriteToolName,
mcp.WithDescription("Edit, delete, or transfer a repository. Use method 'edit' to change settings, 'delete' to remove, 'transfer' to change ownership."),
mcp.WithString("method", mcp.Required(), mcp.Description("operation to perform"), mcp.Enum("edit", "delete", "transfer")),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithString("description", mcp.Description("repository description (for 'edit')")),
mcp.WithString("website", mcp.Description("repository website URL (for 'edit')")),
mcp.WithBoolean("private", mcp.Description("whether the repository is private (for 'edit')")),
mcp.WithBoolean("has_issues", mcp.Description("enable issues (for 'edit')")),
mcp.WithBoolean("has_wiki", mcp.Description("enable wiki (for 'edit')")),
mcp.WithBoolean("has_pull_requests", mcp.Description("enable pull requests (for 'edit')")),
mcp.WithString("default_branch", mcp.Description("default branch name (for 'edit')")),
mcp.WithBoolean("archived", mcp.Description("archive/unarchive the repository (for 'edit')")),
mcp.WithString("new_owner", mcp.Description("new owner username or org (required for 'transfer')")),
mcp.WithArray("team_ids", mcp.Description("team IDs to grant access to (for 'transfer')"), mcp.Items(map[string]any{"type": "number"})),
)
ListForksTool = mcp.NewTool(
ListForksToolName,
mcp.WithDescription("List forks of 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.Min(1)),
mcp.WithNumber("perPage", mcp.Description("results per page"), mcp.DefaultNumber(30), mcp.Min(1)),
)
)
func init() {
@@ -86,6 +123,18 @@ func init() {
Tool: ListOrgReposTool,
Handler: ListOrgReposFn,
})
Tool.RegisterRead(server.ServerTool{
Tool: GetRepoTool,
Handler: GetRepoFn,
})
Tool.RegisterWrite(server.ServerTool{
Tool: RepoWriteTool,
Handler: repoWriteFn,
})
Tool.RegisterRead(server.ServerTool{
Tool: ListForksTool,
Handler: ListForksFn,
})
}
func CreateRepoFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
@@ -229,3 +278,174 @@ func ListOrgReposFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallTool
}
return to.TextResult(repos)
}
func GetRepoFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called GetRepoFn")
args := req.GetArguments()
owner, err := params.GetString(args, "owner")
if err != nil {
return to.ErrorResult(err)
}
repo, err := params.GetString(args, "repo")
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))
}
r, _, err := client.GetRepo(owner, repo)
if err != nil {
return to.ErrorResult(fmt.Errorf("get repo %v/%v err: %v", owner, repo, err))
}
return to.TextResult(slimRepoDetail(r))
}
func repoWriteFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
args := req.GetArguments()
method, err := params.GetString(args, "method")
if err != nil {
return to.ErrorResult(err)
}
switch method {
case "edit":
return editRepoFn(ctx, req)
case "delete":
return deleteRepoFn(ctx, req)
case "transfer":
return transferRepoFn(ctx, req)
default:
return to.ErrorResult(fmt.Errorf("unknown method: %s", method))
}
}
func editRepoFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called editRepoFn")
args := req.GetArguments()
owner, err := params.GetString(args, "owner")
if err != nil {
return to.ErrorResult(err)
}
repo, err := params.GetString(args, "repo")
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))
}
opt := gitea_sdk.EditRepoOption{}
if v, ok := args["description"].(string); ok {
opt.Description = &v
}
if v, ok := args["website"].(string); ok {
opt.Website = &v
}
if v, ok := args["private"].(bool); ok {
opt.Private = &v
}
if v, ok := args["has_issues"].(bool); ok {
opt.HasIssues = &v
}
if v, ok := args["has_wiki"].(bool); ok {
opt.HasWiki = &v
}
if v, ok := args["has_pull_requests"].(bool); ok {
opt.HasPullRequests = &v
}
if v, ok := args["default_branch"].(string); ok {
opt.DefaultBranch = &v
}
if v, ok := args["archived"].(bool); ok {
opt.Archived = &v
}
r, _, err := client.EditRepo(owner, repo, opt)
if err != nil {
return to.ErrorResult(fmt.Errorf("edit repo %v/%v err: %v", owner, repo, err))
}
return to.TextResult(slimRepo(r))
}
func deleteRepoFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called deleteRepoFn")
args := req.GetArguments()
owner, err := params.GetString(args, "owner")
if err != nil {
return to.ErrorResult(err)
}
repo, err := params.GetString(args, "repo")
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.DeleteRepo(owner, repo)
if err != nil {
return to.ErrorResult(fmt.Errorf("delete repo %v/%v err: %v", owner, repo, err))
}
return mcp.NewToolResultText(fmt.Sprintf("Repository %s/%s deleted successfully", owner, repo)), nil
}
func transferRepoFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called transferRepoFn")
args := req.GetArguments()
owner, err := params.GetString(args, "owner")
if err != nil {
return to.ErrorResult(err)
}
repo, err := params.GetString(args, "repo")
if err != nil {
return to.ErrorResult(err)
}
newOwner, err := params.GetString(args, "new_owner")
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))
}
opt := gitea_sdk.TransferRepoOption{
NewOwner: newOwner,
}
if teamIDs, err := params.GetInt64Slice(args, "team_ids"); err == nil {
opt.TeamIDs = &teamIDs
}
r, _, err := client.TransferRepo(owner, repo, opt)
if err != nil {
return to.ErrorResult(fmt.Errorf("transfer repo %v/%v err: %v", owner, repo, err))
}
return to.TextResult(slimRepo(r))
}
func ListForksFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ListForksFn")
args := req.GetArguments()
owner, err := params.GetString(args, "owner")
if err != nil {
return to.ErrorResult(err)
}
repo, err := params.GetString(args, "repo")
if err != nil {
return to.ErrorResult(err)
}
page, pageSize := params.GetPagination(args, 30)
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
forks, _, err := client.ListForks(owner, repo, gitea_sdk.ListForksOptions{
ListOptions: gitea_sdk.ListOptions{
Page: page,
PageSize: pageSize,
},
})
if err != nil {
return to.ErrorResult(fmt.Errorf("list forks %v/%v err: %v", owner, repo, err))
}
return to.TextResult(slimRepos(forks))
}
+140
View File
@@ -206,6 +206,146 @@ func slimTree(t *gitea_sdk.GitTreeResponse) map[string]any {
}
}
func slimRepoDetail(r *gitea_sdk.Repository) map[string]any {
if r == nil {
return nil
}
m := slimRepo(r)
m["website"] = r.Website
m["has_issues"] = r.HasIssues
m["has_wiki"] = r.HasWiki
m["has_pull_requests"] = r.HasPullRequests
m["has_projects"] = r.HasProjects
m["mirror"] = r.Mirror
m["internal"] = r.Internal
m["empty"] = r.Empty
m["size"] = r.Size
if r.Permissions != nil {
m["permissions"] = map[string]any{
"admin": r.Permissions.Admin,
"push": r.Permissions.Push,
"pull": r.Permissions.Pull,
}
}
if r.Parent != nil {
m["parent"] = map[string]any{
"full_name": r.Parent.FullName,
"html_url": r.Parent.HTMLURL,
}
}
return m
}
func slimUser(u *gitea_sdk.User) map[string]any {
if u == nil {
return nil
}
return map[string]any{
"id": u.ID,
"login": u.UserName,
"full_name": u.FullName,
"email": u.Email,
"avatar_url": u.AvatarURL,
"is_admin": u.IsAdmin,
}
}
func slimUsers(users []*gitea_sdk.User) []map[string]any {
out := make([]map[string]any, 0, len(users))
for _, u := range users {
out = append(out, slimUser(u))
}
return out
}
func slimDeployKey(k *gitea_sdk.DeployKey) map[string]any {
if k == nil {
return nil
}
return map[string]any{
"id": k.ID,
"title": k.Title,
"fingerprint": k.Fingerprint,
"key": k.Key,
"url": k.URL,
"read_only": k.ReadOnly,
"created_at": k.Created,
}
}
func slimDeployKeys(keys []*gitea_sdk.DeployKey) []map[string]any {
out := make([]map[string]any, 0, len(keys))
for _, k := range keys {
out = append(out, slimDeployKey(k))
}
return out
}
func slimBranchProtection(bp *gitea_sdk.BranchProtection) map[string]any {
if bp == nil {
return nil
}
return map[string]any{
"branch_name": bp.BranchName,
"rule_name": bp.RuleName,
"enable_push": bp.EnablePush,
"push_whitelist_usernames": bp.PushWhitelistUsernames,
"push_whitelist_teams": bp.PushWhitelistTeams,
"enable_merge_whitelist": bp.EnableMergeWhitelist,
"merge_whitelist_usernames": bp.MergeWhitelistUsernames,
"merge_whitelist_teams": bp.MergeWhitelistTeams,
"required_approvals": bp.RequiredApprovals,
"enable_approvals_whitelist": bp.EnableApprovalsWhitelist,
"block_on_rejected_reviews": bp.BlockOnRejectedReviews,
"block_on_outdated_branch": bp.BlockOnOutdatedBranch,
"dismiss_stale_approvals": bp.DismissStaleApprovals,
"require_signed_commits": bp.RequireSignedCommits,
"protected_file_patterns": bp.ProtectedFilePatterns,
"unprotected_file_patterns": bp.UnprotectedFilePatterns,
"created_at": bp.Created,
"updated_at": bp.Updated,
}
}
func slimBranchProtections(bps []*gitea_sdk.BranchProtection) []map[string]any {
out := make([]map[string]any, 0, len(bps))
for _, bp := range bps {
out = append(out, slimBranchProtection(bp))
}
return out
}
func slimHook(h *gitea_sdk.Hook) map[string]any {
if h == nil {
return nil
}
m := map[string]any{
"id": h.ID,
"type": h.Type,
"active": h.Active,
"events": h.Events,
"created_at": h.Created,
"updated_at": h.Updated,
}
if h.Config != nil {
if url, ok := h.Config["url"]; ok {
m["url"] = url
}
if ct, ok := h.Config["content_type"]; ok {
m["content_type"] = ct
}
}
return m
}
func slimHooks(hooks []*gitea_sdk.Hook) []map[string]any {
out := make([]map[string]any, 0, len(hooks))
for _, h := range hooks {
out = append(out, slimHook(h))
}
return out
}
func slimDirEntries(entries []*gitea_sdk.ContentsResponse) []map[string]any {
out := make([]map[string]any, 0, len(entries))
for _, c := range entries {
+169
View File
@@ -0,0 +1,169 @@
package repo
import (
"context"
"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_sdk "code.gitea.io/sdk/gitea"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
const (
TopicReadToolName = "topic_read"
TopicWriteToolName = "topic_write"
)
var (
TopicReadTool = mcp.NewTool(
TopicReadToolName,
mcp.WithDescription("List repository topics."),
mcp.WithString("method", mcp.Required(), mcp.Description("operation to perform"), mcp.Enum("list")),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
)
TopicWriteTool = mcp.NewTool(
TopicWriteToolName,
mcp.WithDescription("Manage repository topics. Use 'add' to add one, 'delete' to remove one, 'set' to replace all topics."),
mcp.WithString("method", mcp.Required(), mcp.Description("operation to perform"), mcp.Enum("add", "delete", "set")),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithString("topic", mcp.Description("single topic name (required for 'add', 'delete')")),
mcp.WithArray("topics", mcp.Description("list of topic names (required for 'set')"), mcp.Items(map[string]any{"type": "string"})),
)
)
func init() {
Tool.RegisterRead(server.ServerTool{
Tool: TopicReadTool,
Handler: topicReadFn,
})
Tool.RegisterWrite(server.ServerTool{
Tool: TopicWriteTool,
Handler: topicWriteFn,
})
}
func topicReadFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called topicReadFn")
args := req.GetArguments()
owner, err := params.GetString(args, "owner")
if err != nil {
return to.ErrorResult(err)
}
repo, err := params.GetString(args, "repo")
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))
}
topics, _, err := client.ListRepoTopics(owner, repo, gitea_sdk.ListRepoTopicsOptions{})
if err != nil {
return to.ErrorResult(fmt.Errorf("list topics %v/%v err: %v", owner, repo, err))
}
return to.TextResult(topics)
}
func topicWriteFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
args := req.GetArguments()
method, err := params.GetString(args, "method")
if err != nil {
return to.ErrorResult(err)
}
switch method {
case "add":
return addTopicFn(ctx, req)
case "delete":
return deleteTopicFn(ctx, req)
case "set":
return setTopicsFn(ctx, req)
default:
return to.ErrorResult(fmt.Errorf("unknown method: %s", method))
}
}
func addTopicFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called addTopicFn")
args := req.GetArguments()
owner, err := params.GetString(args, "owner")
if err != nil {
return to.ErrorResult(err)
}
repo, err := params.GetString(args, "repo")
if err != nil {
return to.ErrorResult(err)
}
topic, err := params.GetString(args, "topic")
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.AddRepoTopic(owner, repo, topic)
if err != nil {
return to.ErrorResult(fmt.Errorf("add topic %v to %v/%v err: %v", topic, owner, repo, err))
}
return mcp.NewToolResultText(fmt.Sprintf("Topic '%s' added to %s/%s", topic, owner, repo)), nil
}
func deleteTopicFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called deleteTopicFn")
args := req.GetArguments()
owner, err := params.GetString(args, "owner")
if err != nil {
return to.ErrorResult(err)
}
repo, err := params.GetString(args, "repo")
if err != nil {
return to.ErrorResult(err)
}
topic, err := params.GetString(args, "topic")
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.DeleteRepoTopic(owner, repo, topic)
if err != nil {
return to.ErrorResult(fmt.Errorf("delete topic %v from %v/%v err: %v", topic, owner, repo, err))
}
return mcp.NewToolResultText(fmt.Sprintf("Topic '%s' removed from %s/%s", topic, owner, repo)), nil
}
func setTopicsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called setTopicsFn")
args := req.GetArguments()
owner, err := params.GetString(args, "owner")
if err != nil {
return to.ErrorResult(err)
}
repo, err := params.GetString(args, "repo")
if err != nil {
return to.ErrorResult(err)
}
topics := params.GetStringSlice(args, "topics")
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 topics for %v/%v err: %v", owner, repo, err))
}
return to.TextResult(map[string]any{
"message": fmt.Sprintf("Topics set for %s/%s", owner, repo),
"topics": topics,
})
}
+304
View File
@@ -0,0 +1,304 @@
package repo
import (
"context"
"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_sdk "code.gitea.io/sdk/gitea"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
const (
WebhookReadToolName = "webhook_read"
WebhookWriteToolName = "webhook_write"
)
var (
WebhookReadTool = mcp.NewTool(
WebhookReadToolName,
mcp.WithDescription("Read repository webhooks. Use method 'list' to list all, 'get' to get a specific webhook."),
mcp.WithString("method", mcp.Required(), mcp.Description("operation to perform"), mcp.Enum("list", "get")),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithNumber("id", mcp.Description("webhook ID (required for 'get')")),
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)),
mcp.WithNumber("perPage", mcp.Description("results per page"), mcp.DefaultNumber(30)),
)
WebhookWriteTool = mcp.NewTool(
WebhookWriteToolName,
mcp.WithDescription("Create, edit, delete, or test repository webhooks."),
mcp.WithString("method", mcp.Required(), mcp.Description("operation to perform"), mcp.Enum("create", "edit", "delete", "test")),
mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")),
mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")),
mcp.WithNumber("id", mcp.Description("webhook ID (required for 'edit', 'delete', 'test')")),
mcp.WithString("type", mcp.Description("webhook type (for 'create')"), mcp.Enum("gitea", "slack", "discord", "dingtalk", "telegram", "msteams", "feishu", "wechatwork", "packagist")),
mcp.WithString("url", mcp.Description("target URL (required for 'create', optional for 'edit')")),
mcp.WithString("content_type", mcp.Description("content type"), mcp.Enum("json", "form")),
mcp.WithString("secret", mcp.Description("webhook secret")),
mcp.WithArray("events", mcp.Description("event types to trigger on (e.g. push, pull_request, issues)"), mcp.Items(map[string]any{"type": "string"})),
mcp.WithBoolean("active", mcp.Description("whether the webhook is active")),
mcp.WithString("branch_filter", mcp.Description("branch filter pattern")),
)
)
func init() {
Tool.RegisterRead(server.ServerTool{
Tool: WebhookReadTool,
Handler: webhookReadFn,
})
Tool.RegisterWrite(server.ServerTool{
Tool: WebhookWriteTool,
Handler: webhookWriteFn,
})
}
func webhookReadFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
args := req.GetArguments()
method, err := params.GetString(args, "method")
if err != nil {
return to.ErrorResult(err)
}
switch method {
case "list":
return listWebhooksFn(ctx, req)
case "get":
return getWebhookFn(ctx, req)
default:
return to.ErrorResult(fmt.Errorf("unknown method: %s", method))
}
}
func listWebhooksFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called listWebhooksFn")
args := req.GetArguments()
owner, err := params.GetString(args, "owner")
if err != nil {
return to.ErrorResult(err)
}
repo, err := params.GetString(args, "repo")
if err != nil {
return to.ErrorResult(err)
}
page, pageSize := params.GetPagination(args, 30)
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: page,
PageSize: pageSize,
},
})
if err != nil {
return to.ErrorResult(fmt.Errorf("list webhooks %v/%v err: %v", owner, repo, err))
}
return to.TextResult(slimHooks(hooks))
}
func getWebhookFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called getWebhookFn")
args := req.GetArguments()
owner, err := params.GetString(args, "owner")
if err != nil {
return to.ErrorResult(err)
}
repo, err := params.GetString(args, "repo")
if err != nil {
return to.ErrorResult(err)
}
id, err := params.GetIndex(args, "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 webhook %v in %v/%v err: %v", id, owner, repo, err))
}
return to.TextResult(slimHook(hook))
}
func webhookWriteFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
args := req.GetArguments()
method, err := params.GetString(args, "method")
if err != nil {
return to.ErrorResult(err)
}
switch method {
case "create":
return createWebhookFn(ctx, req)
case "edit":
return editWebhookFn(ctx, req)
case "delete":
return deleteWebhookFn(ctx, req)
case "test":
return testWebhookFn(ctx, req)
default:
return to.ErrorResult(fmt.Errorf("unknown method: %s", method))
}
}
func createWebhookFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called createWebhookFn")
args := req.GetArguments()
owner, err := params.GetString(args, "owner")
if err != nil {
return to.ErrorResult(err)
}
repo, err := params.GetString(args, "repo")
if err != nil {
return to.ErrorResult(err)
}
hookType := params.GetOptionalString(args, "type", "gitea")
url, err := params.GetString(args, "url")
if err != nil {
return to.ErrorResult(err)
}
contentType := params.GetOptionalString(args, "content_type", "json")
secret, _ := args["secret"].(string)
events := params.GetStringSlice(args, "events")
active := true
if v, ok := args["active"].(bool); ok {
active = v
}
branchFilter, _ := args["branch_filter"].(string)
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
opt := gitea_sdk.CreateHookOption{
Type: gitea_sdk.HookType(hookType),
Config: map[string]string{
"url": url,
"content_type": contentType,
"secret": secret,
},
Events: events,
Active: active,
BranchFilter: branchFilter,
}
hook, _, err := client.CreateRepoHook(owner, repo, opt)
if err != nil {
return to.ErrorResult(fmt.Errorf("create webhook in %v/%v err: %v", owner, repo, err))
}
return to.TextResult(slimHook(hook))
}
func editWebhookFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called editWebhookFn")
args := req.GetArguments()
owner, err := params.GetString(args, "owner")
if err != nil {
return to.ErrorResult(err)
}
repo, err := params.GetString(args, "repo")
if err != nil {
return to.ErrorResult(err)
}
id, err := params.GetIndex(args, "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))
}
opt := gitea_sdk.EditHookOption{}
config := map[string]string{}
if v, ok := args["url"].(string); ok {
config["url"] = v
}
if v, ok := args["content_type"].(string); ok {
config["content_type"] = v
}
if v, ok := args["secret"].(string); ok {
config["secret"] = v
}
if len(config) > 0 {
opt.Config = config
}
if events := params.GetStringSlice(args, "events"); events != nil {
opt.Events = events
}
if v, ok := args["active"].(bool); ok {
opt.Active = &v
}
if v, ok := args["branch_filter"].(string); ok {
opt.BranchFilter = v
}
_, err = client.EditRepoHook(owner, repo, id, opt)
if err != nil {
return to.ErrorResult(fmt.Errorf("edit webhook %v in %v/%v err: %v", id, owner, repo, err))
}
// EditRepoHook doesn't return the hook, fetch it
hook, _, err := client.GetRepoHook(owner, repo, id)
if err != nil {
return to.ErrorResult(fmt.Errorf("get webhook %v in %v/%v after edit err: %v", id, owner, repo, err))
}
return to.TextResult(slimHook(hook))
}
func deleteWebhookFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called deleteWebhookFn")
args := req.GetArguments()
owner, err := params.GetString(args, "owner")
if err != nil {
return to.ErrorResult(err)
}
repo, err := params.GetString(args, "repo")
if err != nil {
return to.ErrorResult(err)
}
id, err := params.GetIndex(args, "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 webhook %v from %v/%v err: %v", id, owner, repo, err))
}
return mcp.NewToolResultText(fmt.Sprintf("Webhook %d deleted from %s/%s", id, owner, repo)), nil
}
func testWebhookFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called testWebhookFn")
args := req.GetArguments()
owner, err := params.GetString(args, "owner")
if err != nil {
return to.ErrorResult(err)
}
repo, err := params.GetString(args, "repo")
if err != nil {
return to.ErrorResult(err)
}
id, err := params.GetIndex(args, "id")
if err != nil {
return to.ErrorResult(err)
}
// TestRepoHook is not in the SDK - use raw REST
_, err = gitea.DoJSON(ctx, "POST", fmt.Sprintf("repos/%s/%s/hooks/%d/tests", owner, repo, id), nil, nil, nil)
if err != nil {
return to.ErrorResult(fmt.Errorf("test webhook %v in %v/%v err: %v", id, owner, repo, err))
}
return mcp.NewToolResultText(fmt.Sprintf("Webhook %d test triggered for %s/%s", id, owner, repo)), nil
}