4b4b5ee247
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>
305 lines
9.5 KiB
Go
305 lines
9.5 KiB
Go
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
|
|
}
|