346 lines
10 KiB
Go
346 lines
10 KiB
Go
package webhook
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"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 (
|
|
WebhookReadToolName = "webhook_read"
|
|
WebhookWriteToolName = "webhook_write"
|
|
)
|
|
|
|
var (
|
|
WebhookReadTool = mcp.NewTool(
|
|
WebhookReadToolName,
|
|
mcp.WithDescription("Read webhooks. Use method 'list_repo' to list repo webhooks, 'list_org' for org webhooks, 'get' to get a specific webhook."),
|
|
mcp.WithString("method", mcp.Required(), mcp.Description("operation to perform"), mcp.Enum("list_repo", "list_org", "get")),
|
|
mcp.WithString("owner", mcp.Description("repository or organization owner (required for 'list_repo', 'get')")),
|
|
mcp.WithString("repo", mcp.Description("repository name (required for 'list_repo', 'get')")),
|
|
mcp.WithNumber("id", mcp.Description("webhook ID (required for 'get')")),
|
|
)
|
|
|
|
WebhookWriteTool = mcp.NewTool(
|
|
WebhookWriteToolName,
|
|
mcp.WithDescription("Create, update, or delete webhooks."),
|
|
mcp.WithString("method", mcp.Required(), mcp.Description("operation to perform"), mcp.Enum("create_repo", "create_org", "edit_repo", "edit_org", "delete")),
|
|
mcp.WithString("owner", mcp.Required(), mcp.Description("repository or organization owner")),
|
|
mcp.WithString("repo", mcp.Description("repository name (required for repo operations)")),
|
|
mcp.WithNumber("id", mcp.Description("webhook ID (required for 'edit', 'delete')")),
|
|
mcp.WithString("url", mcp.Required(), mcp.Description("webhook URL")),
|
|
mcp.WithString("secret", mcp.Description("webhook secret")),
|
|
mcp.WithBoolean("active", mcp.Description("whether webhook is active")),
|
|
mcp.WithArray("events", mcp.Description("events to trigger webhook"), mcp.Items(map[string]any{"type": "string"})),
|
|
)
|
|
)
|
|
|
|
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) {
|
|
method, err := params.GetString(req.GetArguments(), "method")
|
|
if err != nil {
|
|
return to.ErrorResult(err)
|
|
}
|
|
switch method {
|
|
case "list_repo":
|
|
return listRepoHooksFn(ctx, req)
|
|
case "list_org":
|
|
return listOrgHooksFn(ctx, req)
|
|
case "get":
|
|
return getHookFn(ctx, req)
|
|
default:
|
|
return to.ErrorResult(fmt.Errorf("unknown method: %s", method))
|
|
}
|
|
}
|
|
|
|
func webhookWriteFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
method, err := params.GetString(req.GetArguments(), "method")
|
|
if err != nil {
|
|
return to.ErrorResult(err)
|
|
}
|
|
switch method {
|
|
case "create_repo":
|
|
return createRepoHookFn(ctx, req)
|
|
case "create_org":
|
|
return createOrgHookFn(ctx, req)
|
|
case "edit_repo":
|
|
return editRepoHookFn(ctx, req)
|
|
case "edit_org":
|
|
return editOrgHookFn(ctx, req)
|
|
case "delete":
|
|
return deleteHookFn(ctx, req)
|
|
default:
|
|
return to.ErrorResult(fmt.Errorf("unknown method: %s", method))
|
|
}
|
|
}
|
|
|
|
func listRepoHooksFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
log.Debugf("Called listRepoHooksFn")
|
|
owner, err := params.GetString(req.GetArguments(), "owner")
|
|
if err != nil {
|
|
return to.ErrorResult(err)
|
|
}
|
|
repo, err := params.GetString(req.GetArguments(), "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))
|
|
}
|
|
hooks, _, err := client.ListRepoHooks(owner, repo, gitea_sdk.ListHooksOptions{})
|
|
if err != nil {
|
|
return to.ErrorResult(fmt.Errorf("list repo hooks err: %v", err))
|
|
}
|
|
return to.TextResult(slimHooks(hooks))
|
|
}
|
|
|
|
func listOrgHooksFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
log.Debugf("Called listOrgHooksFn")
|
|
owner, err := params.GetString(req.GetArguments(), "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))
|
|
}
|
|
hooks, _, err := client.ListOrgHooks(owner, gitea_sdk.ListHooksOptions{})
|
|
if err != nil {
|
|
return to.ErrorResult(fmt.Errorf("list org hooks err: %v", err))
|
|
}
|
|
return to.TextResult(slimHooks(hooks))
|
|
}
|
|
|
|
func getHookFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
log.Debugf("Called getHookFn")
|
|
owner, err := params.GetString(req.GetArguments(), "owner")
|
|
if err != nil {
|
|
return to.ErrorResult(err)
|
|
}
|
|
repo, _ := req.GetArguments()["repo"].(string)
|
|
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))
|
|
}
|
|
var hook *gitea_sdk.Hook
|
|
if repo != "" {
|
|
hook, _, err = client.GetRepoHook(owner, repo, id)
|
|
} else {
|
|
hook, _, err = client.GetOrgHook(owner, id)
|
|
}
|
|
if err != nil {
|
|
return to.ErrorResult(fmt.Errorf("get hook err: %v", err))
|
|
}
|
|
return to.TextResult(slimHook(hook))
|
|
}
|
|
|
|
func createRepoHookFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
log.Debugf("Called createRepoHookFn")
|
|
owner, err := params.GetString(req.GetArguments(), "owner")
|
|
if err != nil {
|
|
return to.ErrorResult(err)
|
|
}
|
|
repo, err := params.GetString(req.GetArguments(), "repo")
|
|
if err != nil {
|
|
return to.ErrorResult(err)
|
|
}
|
|
url, err := params.GetString(req.GetArguments(), "url")
|
|
if err != nil {
|
|
return to.ErrorResult(err)
|
|
}
|
|
return createHook(ctx, owner, repo, url, req.GetArguments(), false)
|
|
}
|
|
|
|
func createOrgHookFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
log.Debugf("Called createOrgHookFn")
|
|
owner, err := params.GetString(req.GetArguments(), "owner")
|
|
if err != nil {
|
|
return to.ErrorResult(err)
|
|
}
|
|
url, err := params.GetString(req.GetArguments(), "url")
|
|
if err != nil {
|
|
return to.ErrorResult(err)
|
|
}
|
|
return createHook(ctx, owner, "", url, req.GetArguments(), true)
|
|
}
|
|
|
|
func createHook(ctx context.Context, owner, repo, url string, args map[string]any, isOrg bool) (*mcp.CallToolResult, error) {
|
|
secret, _ := args["secret"].(string)
|
|
active, _ := args["active"].(bool)
|
|
events, _ := args["events"].([]any)
|
|
|
|
eventStr := "push"
|
|
if len(events) > 0 {
|
|
eventsJson, _ := json.Marshal(events)
|
|
eventStr = string(eventsJson)
|
|
}
|
|
|
|
opt := gitea_sdk.CreateHookOption{
|
|
Type: "gitea",
|
|
Active: active,
|
|
Events: []string{eventStr},
|
|
Config: map[string]string{
|
|
"url": url,
|
|
"content_type": "json",
|
|
"secret": secret,
|
|
},
|
|
}
|
|
|
|
client, err := gitea.ClientFromContext(ctx)
|
|
if err != nil {
|
|
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
|
}
|
|
|
|
var hook *gitea_sdk.Hook
|
|
if isOrg {
|
|
hook, _, err = client.CreateOrgHook(owner, opt)
|
|
} else {
|
|
hook, _, err = client.CreateRepoHook(owner, repo, opt)
|
|
}
|
|
if err != nil {
|
|
return to.ErrorResult(fmt.Errorf("create hook err: %v", err))
|
|
}
|
|
return to.TextResult(slimHook(hook))
|
|
}
|
|
|
|
func editRepoHookFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
log.Debugf("Called editRepoHookFn")
|
|
owner, err := params.GetString(req.GetArguments(), "owner")
|
|
if err != nil {
|
|
return to.ErrorResult(err)
|
|
}
|
|
repo, err := params.GetString(req.GetArguments(), "repo")
|
|
if err != nil {
|
|
return to.ErrorResult(err)
|
|
}
|
|
id, err := params.GetIndex(req.GetArguments(), "id")
|
|
if err != nil {
|
|
return to.ErrorResult(err)
|
|
}
|
|
return editHook(ctx, owner, repo, id, req.GetArguments(), false)
|
|
}
|
|
|
|
func editOrgHookFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
log.Debugf("Called editOrgHookFn")
|
|
owner, err := params.GetString(req.GetArguments(), "owner")
|
|
if err != nil {
|
|
return to.ErrorResult(err)
|
|
}
|
|
id, err := params.GetIndex(req.GetArguments(), "id")
|
|
if err != nil {
|
|
return to.ErrorResult(err)
|
|
}
|
|
return editHook(ctx, owner, "", id, req.GetArguments(), true)
|
|
}
|
|
|
|
func editHook(ctx context.Context, owner, repo string, id int64, args map[string]any, isOrg bool) (*mcp.CallToolResult, error) {
|
|
url, _ := args["url"].(string)
|
|
secret, _ := args["secret"].(string)
|
|
active, hasActive := args["active"].(bool)
|
|
|
|
opt := gitea_sdk.EditHookOption{}
|
|
if url != "" {
|
|
opt.Config = map[string]string{"url": url}
|
|
}
|
|
if secret != "" {
|
|
if opt.Config == nil {
|
|
opt.Config = map[string]string{}
|
|
}
|
|
opt.Config["secret"] = secret
|
|
}
|
|
if hasActive {
|
|
opt.Active = &active
|
|
}
|
|
|
|
client, err := gitea.ClientFromContext(ctx)
|
|
if err != nil {
|
|
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
|
|
}
|
|
|
|
if isOrg {
|
|
_, err = client.EditOrgHook(owner, id, opt)
|
|
} else {
|
|
_, err = client.EditRepoHook(owner, repo, id, opt)
|
|
}
|
|
if err != nil {
|
|
return to.ErrorResult(fmt.Errorf("edit hook err: %v", err))
|
|
}
|
|
return to.TextResult("Webhook updated successfully")
|
|
}
|
|
|
|
func deleteHookFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
log.Debugf("Called deleteHookFn")
|
|
owner, err := params.GetString(req.GetArguments(), "owner")
|
|
if err != nil {
|
|
return to.ErrorResult(err)
|
|
}
|
|
repo, hasRepo := req.GetArguments()["repo"].(string)
|
|
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))
|
|
}
|
|
|
|
if hasRepo && repo != "" {
|
|
_, err = client.DeleteRepoHook(owner, repo, id)
|
|
} else {
|
|
_, err = client.DeleteOrgHook(owner, id)
|
|
}
|
|
if err != nil {
|
|
return to.ErrorResult(fmt.Errorf("delete hook err: %v", err))
|
|
}
|
|
return to.TextResult("Webhook deleted successfully")
|
|
}
|
|
|
|
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 slimHook(h *gitea_sdk.Hook) map[string]any {
|
|
if h == nil {
|
|
return nil
|
|
}
|
|
return map[string]any{
|
|
"id": h.ID,
|
|
"type": h.Type,
|
|
"url": h.URL,
|
|
"active": h.Active,
|
|
"events": h.Events,
|
|
"created": h.Created,
|
|
}
|
|
}
|