diff --git a/README.md b/README.md index fa3ccdd..1631bb8 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ This method uses `go run` and requires [Go](https://go.dev) to be installed. claude mcp add --transport stdio --scope user gitea \ --env GITEA_ACCESS_TOKEN=token \ --env GITEA_HOST=https://gitea.com \ - -- go run gitea.com/gitea/gitea-mcp@latest -t stdio + -- go run git.lethalbits.com/lethalbits/gitea-mcp@latest -t stdio ``` ### Usage with VS Code @@ -87,14 +87,14 @@ Optionally, you can add it to a file called `.vscode/mcp.json` in your workspace ### 📥 Download the official binary release -You can download the official release from [official Gitea MCP binary releases](https://gitea.com/gitea/gitea-mcp/releases). +You can download the official release from [official Gitea MCP binary releases](https://git.lethalbits.com/lethalbits/gitea-mcp/releases). ### 🔧 Build from Source You can download the source code by cloning the repository using Git: ```bash -git clone https://gitea.com/gitea/gitea-mcp.git +git clone https://git.lethalbits.com/lethalbits/gitea-mcp.git ``` Before building, make sure you have the following installed: diff --git a/README.zh-cn.md b/README.zh-cn.md index 86485ae..8c6b3ea 100644 --- a/README.zh-cn.md +++ b/README.zh-cn.md @@ -41,7 +41,7 @@ Model Context Protocol (MCP) 是一种协议,允许通过聊天界面整合各 claude mcp add --transport stdio --scope user gitea \ --env GITEA_ACCESS_TOKEN=token \ --env GITEA_HOST=https://gitea.com \ - -- go run gitea.com/gitea/gitea-mcp@latest -t stdio + -- go run git.lethalbits.com/lethalbits/gitea-mcp@latest -t stdio ``` ### 在 VS Code 中使用 @@ -87,14 +87,14 @@ claude mcp add --transport stdio --scope user gitea \ ### 📥 下载官方二进制版本 -可在 [官方 Gitea MCP 二进制版本](https://gitea.com/gitea/gitea-mcp/releases) 下载。 +可在 [官方 Gitea MCP 二进制版本](https://git.lethalbits.com/lethalbits/gitea-mcp/releases) 下载。 ### 🔧 从源码构建 可用 Git 下载源码: ```bash -git clone https://gitea.com/gitea/gitea-mcp.git +git clone https://git.lethalbits.com/lethalbits/gitea-mcp.git ``` 构建前请先安装: diff --git a/README.zh-tw.md b/README.zh-tw.md index 2dac558..66227a7 100644 --- a/README.zh-tw.md +++ b/README.zh-tw.md @@ -41,7 +41,7 @@ Model Context Protocol (MCP) 是一種協議,允許透過聊天介面整合各 claude mcp add --transport stdio --scope user gitea \ --env GITEA_ACCESS_TOKEN=token \ --env GITEA_HOST=https://gitea.com \ - -- go run gitea.com/gitea/gitea-mcp@latest -t stdio + -- go run git.lethalbits.com/lethalbits/gitea-mcp@latest -t stdio ``` ### 在 VS Code 中使用 @@ -87,14 +87,14 @@ claude mcp add --transport stdio --scope user gitea \ ### 📥 下載官方二進位版本 -可至 [官方 Gitea MCP 二進位版本](https://gitea.com/gitea/gitea-mcp/releases) 下載。 +可至 [官方 Gitea MCP 二進位版本](https://git.lethalbits.com/lethalbits/gitea-mcp/releases) 下載。 ### 🔧 從原始碼建置 可用 Git 下載原始碼: ```bash -git clone https://gitea.com/gitea/gitea-mcp.git +git clone https://git.lethalbits.com/lethalbits/gitea-mcp.git ``` 建置前請先安裝: diff --git a/cmd/cmd.go b/cmd/cmd.go index 83cf7eb..d13917f 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -7,9 +7,9 @@ import ( "os" "text/tabwriter" - "gitea.com/gitea/gitea-mcp/operation" - flagPkg "gitea.com/gitea/gitea-mcp/pkg/flag" - "gitea.com/gitea/gitea-mcp/pkg/log" + "git.lethalbits.com/lethalbits/gitea-mcp/operation" + flagPkg "git.lethalbits.com/lethalbits/gitea-mcp/pkg/flag" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/log" ) var ( diff --git a/go.mod b/go.mod index 911bed0..c27b004 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module gitea.com/gitea/gitea-mcp +module git.lethalbits.com/lethalbits/gitea-mcp go 1.26.0 diff --git a/main.go b/main.go index fa5c489..d319b78 100644 --- a/main.go +++ b/main.go @@ -1,8 +1,8 @@ package main import ( - "gitea.com/gitea/gitea-mcp/cmd" - "gitea.com/gitea/gitea-mcp/pkg/flag" + "git.lethalbits.com/lethalbits/gitea-mcp/cmd" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/flag" ) var Version = "dev" diff --git a/operation/actions/actions.go b/operation/actions/actions.go index 3c314cc..e1f8d20 100644 --- a/operation/actions/actions.go +++ b/operation/actions/actions.go @@ -1,7 +1,7 @@ package actions import ( - "gitea.com/gitea/gitea-mcp/pkg/tool" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/tool" ) // Tool is the registry for all Actions-related MCP tools. diff --git a/operation/actions/logs.go b/operation/actions/logs.go index cd99efd..5578619 100644 --- a/operation/actions/logs.go +++ b/operation/actions/logs.go @@ -9,10 +9,10 @@ import ( "os" "path/filepath" - "gitea.com/gitea/gitea-mcp/pkg/gitea" - "gitea.com/gitea/gitea-mcp/pkg/log" - "gitea.com/gitea/gitea-mcp/pkg/params" - "gitea.com/gitea/gitea-mcp/pkg/to" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/gitea" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/log" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/params" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/to" "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" diff --git a/operation/actions/runs.go b/operation/actions/runs.go index 1fb632b..eddc66d 100644 --- a/operation/actions/runs.go +++ b/operation/actions/runs.go @@ -9,10 +9,10 @@ import ( "net/url" "strconv" - "gitea.com/gitea/gitea-mcp/pkg/gitea" - "gitea.com/gitea/gitea-mcp/pkg/log" - "gitea.com/gitea/gitea-mcp/pkg/params" - "gitea.com/gitea/gitea-mcp/pkg/to" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/gitea" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/log" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/params" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/to" "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" diff --git a/operation/actions/secrets.go b/operation/actions/secrets.go index 586cda1..dd87ced 100644 --- a/operation/actions/secrets.go +++ b/operation/actions/secrets.go @@ -7,10 +7,10 @@ import ( "net/url" "time" - "gitea.com/gitea/gitea-mcp/pkg/gitea" - "gitea.com/gitea/gitea-mcp/pkg/log" - "gitea.com/gitea/gitea-mcp/pkg/params" - "gitea.com/gitea/gitea-mcp/pkg/to" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/gitea" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/log" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/params" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/to" gitea_sdk "code.gitea.io/sdk/gitea" "github.com/mark3labs/mcp-go/mcp" diff --git a/operation/actions/variables.go b/operation/actions/variables.go index dfeacfc..c969921 100644 --- a/operation/actions/variables.go +++ b/operation/actions/variables.go @@ -7,10 +7,10 @@ import ( "net/url" "strconv" - "gitea.com/gitea/gitea-mcp/pkg/gitea" - "gitea.com/gitea/gitea-mcp/pkg/log" - "gitea.com/gitea/gitea-mcp/pkg/params" - "gitea.com/gitea/gitea-mcp/pkg/to" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/gitea" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/log" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/params" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/to" gitea_sdk "code.gitea.io/sdk/gitea" "github.com/mark3labs/mcp-go/mcp" diff --git a/operation/admin/admin.go b/operation/admin/admin.go new file mode 100644 index 0000000..71e6275 --- /dev/null +++ b/operation/admin/admin.go @@ -0,0 +1,805 @@ +package admin + +import ( + "context" + "errors" + "fmt" + + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/gitea" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/log" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/params" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/to" + "git.lethalbits.com/lethalbits/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 ( + AdminListUsersToolName = "admin_list_users" + AdminCreateUserToolName = "admin_create_user" + AdminEditUserToolName = "admin_edit_user" + AdminDeleteUserToolName = "admin_delete_user" + AdminRenameUserToolName = "admin_rename_user" + AdminListOrgsToolName = "admin_list_orgs" + AdminCreateOrgToolName = "admin_create_org" + AdminCreateRepoToolName = "admin_create_repo" + AdminListHooksToolName = "admin_list_hooks" + AdminGetHookToolName = "admin_get_hook" + AdminCreateHookToolName = "admin_create_hook" + AdminEditHookToolName = "admin_edit_hook" + AdminDeleteHookToolName = "admin_delete_hook" + ListCronTasksToolName = "admin_list_cron_tasks" + RunCronTaskToolName = "admin_run_cron_task" + ListUnadoptedReposToolName = "admin_list_unadopted_repos" + AdoptUnadoptedRepoToolName = "admin_adopt_repo" + DeleteUnadoptedRepoToolName = "admin_delete_unadopted_repo" + AdminListEmailsToolName = "admin_list_emails" + AdminSearchEmailsToolName = "admin_search_emails" + ListUserBadgesToolName = "admin_list_user_badges" + AddUserBadgesToolName = "admin_add_user_badges" + DeleteUserBadgeToolName = "admin_delete_user_badge" +) + +var ( + AdminListUsersTool = mcp.NewTool( + AdminListUsersToolName, + mcp.WithDescription("Admin: List all users"), + mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)), + mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)), + ) + + AdminCreateUserTool = mcp.NewTool( + AdminCreateUserToolName, + mcp.WithDescription("Admin: Create a new user"), + mcp.WithString("username", mcp.Required(), mcp.Description("username")), + mcp.WithString("email", mcp.Required(), mcp.Description("email address")), + mcp.WithString("password", mcp.Required(), mcp.Description("password")), + mcp.WithString("full_name", mcp.Description("full name")), + mcp.WithBoolean("must_change_password", mcp.Description("require password change on first login (default: true)")), + mcp.WithBoolean("send_notify", mcp.Description("send notification email")), + ) + + AdminEditUserTool = mcp.NewTool( + AdminEditUserToolName, + mcp.WithDescription("Admin: Edit a user account"), + mcp.WithString("username", mcp.Required(), mcp.Description("username to edit")), + mcp.WithString("email", mcp.Description("new email")), + mcp.WithString("password", mcp.Description("new password")), + mcp.WithString("full_name", mcp.Description("new full name")), + mcp.WithBoolean("must_change_password", mcp.Description("require password change")), + mcp.WithBoolean("admin", mcp.Description("set admin status")), + mcp.WithBoolean("active", mcp.Description("set active status")), + mcp.WithBoolean("prohibit_login", mcp.Description("prohibit login")), + mcp.WithBoolean("restricted", mcp.Description("set restricted status")), + mcp.WithBoolean("allow_create_organization", mcp.Description("allow creating organizations")), + ) + + AdminDeleteUserTool = mcp.NewTool( + AdminDeleteUserToolName, + mcp.WithDescription("Admin: Delete a user account"), + mcp.WithString("username", mcp.Required(), mcp.Description("username to delete")), + ) + + AdminRenameUserTool = mcp.NewTool( + AdminRenameUserToolName, + mcp.WithDescription("Admin: Rename a user"), + mcp.WithString("username", mcp.Required(), mcp.Description("current username")), + mcp.WithString("new_username", mcp.Required(), mcp.Description("new username")), + ) + + AdminListOrgsTool = mcp.NewTool( + AdminListOrgsToolName, + mcp.WithDescription("Admin: List all organizations"), + mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)), + mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)), + ) + + AdminCreateOrgTool = mcp.NewTool( + AdminCreateOrgToolName, + mcp.WithDescription("Admin: Create an organization for a user"), + mcp.WithString("username", mcp.Required(), mcp.Description("owner username")), + mcp.WithString("name", mcp.Required(), mcp.Description("organization name")), + mcp.WithString("full_name", mcp.Description("organization full name")), + mcp.WithString("description", mcp.Description("organization description")), + mcp.WithString("visibility", mcp.Description("visibility: public, limited, private (default: public)")), + ) + + AdminCreateRepoTool = mcp.NewTool( + AdminCreateRepoToolName, + mcp.WithDescription("Admin: Create a repository for a user"), + mcp.WithString("username", mcp.Required(), mcp.Description("owner username")), + mcp.WithString("name", mcp.Required(), mcp.Description("repository name")), + mcp.WithString("description", mcp.Description("repository description")), + mcp.WithBoolean("private", mcp.Description("make repository private")), + mcp.WithBoolean("auto_init", mcp.Description("auto-initialize with README")), + ) + + AdminListHooksTool = mcp.NewTool( + AdminListHooksToolName, + mcp.WithDescription("Admin: List system webhooks"), + mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)), + mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)), + ) + + AdminGetHookTool = mcp.NewTool( + AdminGetHookToolName, + mcp.WithDescription("Admin: Get a system webhook by ID"), + mcp.WithNumber("id", mcp.Required(), mcp.Description("webhook ID")), + ) + + AdminCreateHookTool = mcp.NewTool( + AdminCreateHookToolName, + mcp.WithDescription("Admin: Create a system webhook"), + mcp.WithString("type", mcp.Required(), mcp.Description("hook type: gitea, gogs, slack, discord, dingtalk, telegram, msteams, feishu, wechatwork, packagist")), + mcp.WithString("url", mcp.Required(), mcp.Description("target URL")), + mcp.WithString("content_type", mcp.Description("content type: json or form (default: json)")), + mcp.WithString("secret", mcp.Description("webhook secret")), + mcp.WithBoolean("active", mcp.Description("whether the webhook is active (default: true)")), + ) + + AdminEditHookTool = mcp.NewTool( + AdminEditHookToolName, + mcp.WithDescription("Admin: Edit a system webhook"), + mcp.WithNumber("id", mcp.Required(), mcp.Description("webhook ID")), + mcp.WithString("url", mcp.Description("target URL")), + mcp.WithString("content_type", mcp.Description("content type: json or form")), + mcp.WithString("secret", mcp.Description("webhook secret")), + mcp.WithBoolean("active", mcp.Description("whether the webhook is active")), + ) + + AdminDeleteHookTool = mcp.NewTool( + AdminDeleteHookToolName, + mcp.WithDescription("Admin: Delete a system webhook"), + mcp.WithNumber("id", mcp.Required(), mcp.Description("webhook ID")), + ) + + ListCronTasksTool = mcp.NewTool( + ListCronTasksToolName, + mcp.WithDescription("Admin: List cron tasks"), + mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)), + mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)), + ) + + RunCronTaskTool = mcp.NewTool( + RunCronTaskToolName, + mcp.WithDescription("Admin: Run a cron task manually"), + mcp.WithString("task", mcp.Required(), mcp.Description("cron task name")), + ) + + ListUnadoptedReposTool = mcp.NewTool( + ListUnadoptedReposToolName, + mcp.WithDescription("Admin: List unadopted repositories"), + mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)), + mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)), + ) + + AdoptUnadoptedRepoTool = mcp.NewTool( + AdoptUnadoptedRepoToolName, + mcp.WithDescription("Admin: Adopt an unadopted repository"), + mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), + mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), + ) + + DeleteUnadoptedRepoTool = mcp.NewTool( + DeleteUnadoptedRepoToolName, + mcp.WithDescription("Admin: Delete an unadopted repository"), + mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), + mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), + ) + + AdminListEmailsTool = mcp.NewTool( + AdminListEmailsToolName, + mcp.WithDescription("Admin: List all emails"), + mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)), + mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)), + ) + + AdminSearchEmailsTool = mcp.NewTool( + AdminSearchEmailsToolName, + mcp.WithDescription("Admin: Search emails"), + mcp.WithString("query", mcp.Required(), mcp.Description("search query")), + mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)), + mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)), + ) + + ListUserBadgesTool = mcp.NewTool( + ListUserBadgesToolName, + mcp.WithDescription("Admin: List badges for a user"), + mcp.WithString("username", mcp.Required(), mcp.Description("username")), + ) + + AddUserBadgesTool = mcp.NewTool( + AddUserBadgesToolName, + mcp.WithDescription("Admin: Add badges to a user"), + mcp.WithString("username", mcp.Required(), mcp.Description("username")), + mcp.WithString("slugs", mcp.Required(), mcp.Description("comma-separated badge slugs to add")), + ) + + DeleteUserBadgeTool = mcp.NewTool( + DeleteUserBadgeToolName, + mcp.WithDescription("Admin: Remove badges from a user"), + mcp.WithString("username", mcp.Required(), mcp.Description("username")), + mcp.WithString("slugs", mcp.Required(), mcp.Description("comma-separated badge slugs to remove")), + ) +) + +func init() { + Tool.RegisterRead(server.ServerTool{Tool: AdminListUsersTool, Handler: AdminListUsersFn}) + Tool.RegisterRead(server.ServerTool{Tool: AdminListOrgsTool, Handler: AdminListOrgsFn}) + Tool.RegisterRead(server.ServerTool{Tool: AdminListHooksTool, Handler: AdminListHooksFn}) + Tool.RegisterRead(server.ServerTool{Tool: AdminGetHookTool, Handler: AdminGetHookFn}) + Tool.RegisterRead(server.ServerTool{Tool: ListCronTasksTool, Handler: ListCronTasksFn}) + Tool.RegisterRead(server.ServerTool{Tool: ListUnadoptedReposTool, Handler: ListUnadoptedReposFn}) + Tool.RegisterRead(server.ServerTool{Tool: AdminListEmailsTool, Handler: AdminListEmailsFn}) + Tool.RegisterRead(server.ServerTool{Tool: AdminSearchEmailsTool, Handler: AdminSearchEmailsFn}) + Tool.RegisterRead(server.ServerTool{Tool: ListUserBadgesTool, Handler: ListUserBadgesFn}) + + Tool.RegisterWrite(server.ServerTool{Tool: AdminCreateUserTool, Handler: AdminCreateUserFn}) + Tool.RegisterWrite(server.ServerTool{Tool: AdminEditUserTool, Handler: AdminEditUserFn}) + Tool.RegisterWrite(server.ServerTool{Tool: AdminDeleteUserTool, Handler: AdminDeleteUserFn}) + Tool.RegisterWrite(server.ServerTool{Tool: AdminRenameUserTool, Handler: AdminRenameUserFn}) + Tool.RegisterWrite(server.ServerTool{Tool: AdminCreateOrgTool, Handler: AdminCreateOrgFn}) + Tool.RegisterWrite(server.ServerTool{Tool: AdminCreateRepoTool, Handler: AdminCreateRepoFn}) + Tool.RegisterWrite(server.ServerTool{Tool: AdminCreateHookTool, Handler: AdminCreateHookFn}) + Tool.RegisterWrite(server.ServerTool{Tool: AdminEditHookTool, Handler: AdminEditHookFn}) + Tool.RegisterWrite(server.ServerTool{Tool: AdminDeleteHookTool, Handler: AdminDeleteHookFn}) + Tool.RegisterWrite(server.ServerTool{Tool: RunCronTaskTool, Handler: RunCronTaskFn}) + Tool.RegisterWrite(server.ServerTool{Tool: AdoptUnadoptedRepoTool, Handler: AdoptUnadoptedRepoFn}) + Tool.RegisterWrite(server.ServerTool{Tool: DeleteUnadoptedRepoTool, Handler: DeleteUnadoptedRepoFn}) + Tool.RegisterWrite(server.ServerTool{Tool: AddUserBadgesTool, Handler: AddUserBadgesFn}) + Tool.RegisterWrite(server.ServerTool{Tool: DeleteUserBadgeTool, Handler: DeleteUserBadgeFn}) +} + +func AdminListUsersFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called AdminListUsersFn") + page := params.GetOptionalInt(req.GetArguments(), "page", 1) + pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100) + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + users, _, err := client.AdminListUsers(gitea_sdk.AdminListUsersOptions{ + ListOptions: gitea_sdk.ListOptions{Page: int(page), PageSize: int(pageSize)}, + }) + if err != nil { + return to.ErrorResult(fmt.Errorf("admin list users err: %v", err)) + } + return to.TextResult(users) +} + +func AdminCreateUserFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called AdminCreateUserFn") + username, _ := req.GetArguments()["username"].(string) + email, _ := req.GetArguments()["email"].(string) + password, _ := req.GetArguments()["password"].(string) + if username == "" || email == "" || password == "" { + return to.ErrorResult(errors.New("username, email, and password are required")) + } + mustChange := true + if v, ok := req.GetArguments()["must_change_password"].(bool); ok { + mustChange = v + } + opt := gitea_sdk.CreateUserOption{ + LoginName: username, + Username: username, + Email: email, + Password: password, + MustChangePassword: &mustChange, + } + if v, ok := req.GetArguments()["full_name"].(string); ok { + opt.FullName = v + } + if v, ok := req.GetArguments()["send_notify"].(bool); ok { + opt.SendNotify = v + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + user, _, err := client.AdminCreateUser(opt) + if err != nil { + return to.ErrorResult(fmt.Errorf("admin create user err: %v", err)) + } + return to.TextResult(user) +} + +func AdminEditUserFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called AdminEditUserFn") + username, _ := req.GetArguments()["username"].(string) + if username == "" { + return to.ErrorResult(errors.New("username is required")) + } + opt := gitea_sdk.EditUserOption{} + if v, ok := req.GetArguments()["email"].(string); ok { + opt.Email = &v + } + if v, ok := req.GetArguments()["password"].(string); ok { + opt.Password = v + } + if v, ok := req.GetArguments()["full_name"].(string); ok { + opt.FullName = &v + } + if v, ok := req.GetArguments()["must_change_password"].(bool); ok { + opt.MustChangePassword = &v + } + if v, ok := req.GetArguments()["admin"].(bool); ok { + opt.Admin = &v + } + if v, ok := req.GetArguments()["active"].(bool); ok { + opt.Active = &v + } + if v, ok := req.GetArguments()["prohibit_login"].(bool); ok { + opt.ProhibitLogin = &v + } + if v, ok := req.GetArguments()["restricted"].(bool); ok { + opt.Restricted = &v + } + if v, ok := req.GetArguments()["allow_create_organization"].(bool); ok { + opt.AllowCreateOrganization = &v + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + _, err = client.AdminEditUser(username, opt) + if err != nil { + return to.ErrorResult(fmt.Errorf("admin edit user err: %v", err)) + } + return to.TextResult(map[string]string{"status": "updated", "username": username}) +} + +func AdminDeleteUserFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called AdminDeleteUserFn") + username, _ := req.GetArguments()["username"].(string) + if username == "" { + return to.ErrorResult(errors.New("username is required")) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + _, err = client.AdminDeleteUser(username) + if err != nil { + return to.ErrorResult(fmt.Errorf("admin delete user err: %v", err)) + } + return to.TextResult(map[string]string{"status": "deleted", "username": username}) +} + +func AdminRenameUserFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called AdminRenameUserFn") + username, _ := req.GetArguments()["username"].(string) + newUsername, _ := req.GetArguments()["new_username"].(string) + if username == "" || newUsername == "" { + return to.ErrorResult(errors.New("username and new_username are required")) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + _, err = client.AdminRenameUser(username, gitea_sdk.RenameUserOption{NewUsername: newUsername}) + if err != nil { + return to.ErrorResult(fmt.Errorf("admin rename user err: %v", err)) + } + return to.TextResult(map[string]string{"status": "renamed", "old": username, "new": newUsername}) +} + +func AdminListOrgsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called AdminListOrgsFn") + page := params.GetOptionalInt(req.GetArguments(), "page", 1) + pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100) + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + orgs, _, err := client.AdminListOrgs(gitea_sdk.AdminListOrgsOptions{ + ListOptions: gitea_sdk.ListOptions{Page: int(page), PageSize: int(pageSize)}, + }) + if err != nil { + return to.ErrorResult(fmt.Errorf("admin list orgs err: %v", err)) + } + return to.TextResult(orgs) +} + +func AdminCreateOrgFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called AdminCreateOrgFn") + username, _ := req.GetArguments()["username"].(string) + name, _ := req.GetArguments()["name"].(string) + if username == "" || name == "" { + return to.ErrorResult(errors.New("username and name are required")) + } + opt := gitea_sdk.CreateOrgOption{ + Name: name, + } + if v, ok := req.GetArguments()["full_name"].(string); ok { + opt.FullName = v + } + if v, ok := req.GetArguments()["description"].(string); ok { + opt.Description = v + } + if v, ok := req.GetArguments()["visibility"].(string); ok { + opt.Visibility = gitea_sdk.VisibleType(v) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + org, _, err := client.AdminCreateOrg(username, opt) + if err != nil { + return to.ErrorResult(fmt.Errorf("admin create org err: %v", err)) + } + return to.TextResult(org) +} + +func AdminCreateRepoFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called AdminCreateRepoFn") + username, _ := req.GetArguments()["username"].(string) + name, _ := req.GetArguments()["name"].(string) + if username == "" || name == "" { + return to.ErrorResult(errors.New("username and name are required")) + } + opt := gitea_sdk.CreateRepoOption{ + Name: name, + } + if v, ok := req.GetArguments()["description"].(string); ok { + opt.Description = v + } + if v, ok := req.GetArguments()["private"].(bool); ok { + opt.Private = v + } + if v, ok := req.GetArguments()["auto_init"].(bool); ok { + opt.AutoInit = v + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + repo, _, err := client.AdminCreateRepo(username, opt) + if err != nil { + return to.ErrorResult(fmt.Errorf("admin create repo err: %v", err)) + } + return to.TextResult(repo) +} + +func AdminListHooksFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called AdminListHooksFn") + page := params.GetOptionalInt(req.GetArguments(), "page", 1) + pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100) + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + hooks, _, err := client.ListAdminHooks(gitea_sdk.ListAdminHooksOptions{ + ListOptions: gitea_sdk.ListOptions{Page: int(page), PageSize: int(pageSize)}, + }) + if err != nil { + return to.ErrorResult(fmt.Errorf("admin list hooks err: %v", err)) + } + return to.TextResult(hooks) +} + +func AdminGetHookFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called AdminGetHookFn") + id, err := params.GetIndex(req.GetArguments(), "id") + if err != nil { + return to.ErrorResult(err) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + hook, _, err := client.GetAdminHook(id) + if err != nil { + return to.ErrorResult(fmt.Errorf("admin get hook err: %v", err)) + } + return to.TextResult(hook) +} + +func AdminCreateHookFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called AdminCreateHookFn") + hookType, _ := req.GetArguments()["type"].(string) + url, _ := req.GetArguments()["url"].(string) + if hookType == "" || url == "" { + return to.ErrorResult(errors.New("type and url are required")) + } + contentType := "json" + if v, ok := req.GetArguments()["content_type"].(string); ok { + contentType = v + } + config := map[string]string{ + "url": url, + "content_type": contentType, + } + if v, ok := req.GetArguments()["secret"].(string); ok { + config["secret"] = v + } + opt := gitea_sdk.CreateHookOption{ + Type: gitea_sdk.HookType(hookType), + Config: config, + Active: true, + } + if v, ok := req.GetArguments()["active"].(bool); ok { + opt.Active = v + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + hook, _, err := client.CreateAdminHook(opt) + if err != nil { + return to.ErrorResult(fmt.Errorf("admin create hook err: %v", err)) + } + return to.TextResult(hook) +} + +func AdminEditHookFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called AdminEditHookFn") + id, err := params.GetIndex(req.GetArguments(), "id") + if err != nil { + return to.ErrorResult(err) + } + opt := gitea_sdk.EditHookOption{} + config := map[string]string{} + if v, ok := req.GetArguments()["url"].(string); ok { + config["url"] = v + } + if v, ok := req.GetArguments()["content_type"].(string); ok { + config["content_type"] = v + } + if v, ok := req.GetArguments()["secret"].(string); ok { + config["secret"] = v + } + if len(config) > 0 { + opt.Config = config + } + if v, ok := req.GetArguments()["active"].(bool); ok { + opt.Active = &v + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + _, _, err = client.EditAdminHook(id, opt) + if err != nil { + return to.ErrorResult(fmt.Errorf("admin edit hook err: %v", err)) + } + return to.TextResult(map[string]string{"status": "updated"}) +} + +func AdminDeleteHookFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called AdminDeleteHookFn") + id, err := params.GetIndex(req.GetArguments(), "id") + if err != nil { + return to.ErrorResult(err) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + _, err = client.DeleteAdminHook(id) + if err != nil { + return to.ErrorResult(fmt.Errorf("admin delete hook err: %v", err)) + } + return to.TextResult(map[string]string{"status": "deleted"}) +} + +func ListCronTasksFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called ListCronTasksFn") + page := params.GetOptionalInt(req.GetArguments(), "page", 1) + pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100) + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + tasks, _, err := client.ListCronTasks(gitea_sdk.ListCronTaskOptions{ + ListOptions: gitea_sdk.ListOptions{Page: int(page), PageSize: int(pageSize)}, + }) + if err != nil { + return to.ErrorResult(fmt.Errorf("list cron tasks err: %v", err)) + } + return to.TextResult(tasks) +} + +func RunCronTaskFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called RunCronTaskFn") + task, _ := req.GetArguments()["task"].(string) + if task == "" { + return to.ErrorResult(errors.New("task is required")) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + _, err = client.RunCronTasks(task) + if err != nil { + return to.ErrorResult(fmt.Errorf("run cron task err: %v", err)) + } + return to.TextResult(map[string]string{"status": "triggered", "task": task}) +} + +func ListUnadoptedReposFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called ListUnadoptedReposFn") + page := params.GetOptionalInt(req.GetArguments(), "page", 1) + pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100) + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + repos, _, err := client.ListUnadoptedRepos(gitea_sdk.ListUnadoptedReposOptions{ + ListOptions: gitea_sdk.ListOptions{Page: int(page), PageSize: int(pageSize)}, + }) + if err != nil { + return to.ErrorResult(fmt.Errorf("list unadopted repos err: %v", err)) + } + return to.TextResult(repos) +} + +func AdoptUnadoptedRepoFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called AdoptUnadoptedRepoFn") + owner, _ := req.GetArguments()["owner"].(string) + repo, _ := req.GetArguments()["repo"].(string) + if owner == "" || repo == "" { + return to.ErrorResult(errors.New("owner and repo are required")) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + _, err = client.AdoptUnadoptedRepo(owner, repo) + if err != nil { + return to.ErrorResult(fmt.Errorf("adopt repo err: %v", err)) + } + return to.TextResult(map[string]string{"status": "adopted"}) +} + +func DeleteUnadoptedRepoFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called DeleteUnadoptedRepoFn") + owner, _ := req.GetArguments()["owner"].(string) + repo, _ := req.GetArguments()["repo"].(string) + if owner == "" || repo == "" { + return to.ErrorResult(errors.New("owner and repo are required")) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + _, err = client.DeleteUnadoptedRepo(owner, repo) + if err != nil { + return to.ErrorResult(fmt.Errorf("delete unadopted repo err: %v", err)) + } + return to.TextResult(map[string]string{"status": "deleted"}) +} + +func AdminListEmailsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called AdminListEmailsFn") + page := params.GetOptionalInt(req.GetArguments(), "page", 1) + pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100) + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + emails, _, err := client.ListAdminEmails(gitea_sdk.ListAdminEmailsOptions{ + ListOptions: gitea_sdk.ListOptions{Page: int(page), PageSize: int(pageSize)}, + }) + if err != nil { + return to.ErrorResult(fmt.Errorf("admin list emails err: %v", err)) + } + return to.TextResult(emails) +} + +func AdminSearchEmailsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called AdminSearchEmailsFn") + query, _ := req.GetArguments()["query"].(string) + if query == "" { + return to.ErrorResult(errors.New("query is required")) + } + page := params.GetOptionalInt(req.GetArguments(), "page", 1) + pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100) + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + emails, _, err := client.SearchAdminEmails(gitea_sdk.SearchAdminEmailsOptions{ + ListOptions: gitea_sdk.ListOptions{Page: int(page), PageSize: int(pageSize)}, + Query: query, + }) + if err != nil { + return to.ErrorResult(fmt.Errorf("admin search emails err: %v", err)) + } + return to.TextResult(emails) +} + +func splitAndTrim(s string) []string { + var result []string + for _, part := range splitByComma(s) { + trimmed := trimSpace(part) + if trimmed != "" { + result = append(result, trimmed) + } + } + return result +} + +func splitByComma(s string) []string { + var result []string + start := 0 + for i := 0; i < len(s); i++ { + if s[i] == ',' { + result = append(result, s[start:i]) + start = i + 1 + } + } + result = append(result, s[start:]) + return result +} + +func trimSpace(s string) string { + start, end := 0, len(s) + for start < end && (s[start] == ' ' || s[start] == '\t') { + start++ + } + for end > start && (s[end-1] == ' ' || s[end-1] == '\t') { + end-- + } + return s[start:end] +} + +func ListUserBadgesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called ListUserBadgesFn") + username, _ := req.GetArguments()["username"].(string) + if username == "" { + return to.ErrorResult(errors.New("username is required")) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + badges, _, err := client.ListUserBadges(username) + if err != nil { + return to.ErrorResult(fmt.Errorf("list user badges err: %v", err)) + } + return to.TextResult(badges) +} + +func AddUserBadgesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called AddUserBadgesFn") + username, _ := req.GetArguments()["username"].(string) + slugs, _ := req.GetArguments()["slugs"].(string) + if username == "" || slugs == "" { + return to.ErrorResult(errors.New("username and slugs are required")) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + _, err = client.AddUserBadges(username, gitea_sdk.UserBadgeOption{ + BadgeSlugs: splitAndTrim(slugs), + }) + if err != nil { + return to.ErrorResult(fmt.Errorf("add user badges err: %v", err)) + } + return to.TextResult(map[string]string{"status": "added"}) +} + +func DeleteUserBadgeFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called DeleteUserBadgeFn") + username, _ := req.GetArguments()["username"].(string) + slugs, _ := req.GetArguments()["slugs"].(string) + if username == "" || slugs == "" { + return to.ErrorResult(errors.New("username and slugs are required")) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + _, err = client.DeleteUserBadge(username, gitea_sdk.UserBadgeOption{ + BadgeSlugs: splitAndTrim(slugs), + }) + if err != nil { + return to.ErrorResult(fmt.Errorf("delete user badge err: %v", err)) + } + return to.TextResult(map[string]string{"status": "removed"}) +} diff --git a/operation/issue/issue.go b/operation/issue/issue.go index a380a84..41812fd 100644 --- a/operation/issue/issue.go +++ b/operation/issue/issue.go @@ -5,11 +5,11 @@ import ( "errors" "fmt" - "gitea.com/gitea/gitea-mcp/pkg/gitea" - "gitea.com/gitea/gitea-mcp/pkg/log" - "gitea.com/gitea/gitea-mcp/pkg/params" - "gitea.com/gitea/gitea-mcp/pkg/to" - "gitea.com/gitea/gitea-mcp/pkg/tool" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/gitea" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/log" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/params" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/to" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/tool" gitea_sdk "code.gitea.io/sdk/gitea" "github.com/mark3labs/mcp-go/mcp" diff --git a/operation/issue/pin.go b/operation/issue/pin.go new file mode 100644 index 0000000..90332b0 --- /dev/null +++ b/operation/issue/pin.go @@ -0,0 +1,151 @@ +package issue + +import ( + "context" + "errors" + "fmt" + + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/gitea" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/log" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/params" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/to" + + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" +) + +const ( + ListPinnedIssuesToolName = "list_pinned_issues" + PinIssueToolName = "pin_issue" + UnpinIssueToolName = "unpin_issue" + MoveIssuePinToolName = "move_issue_pin" +) + +var ( + ListPinnedIssuesTool = mcp.NewTool( + ListPinnedIssuesToolName, + mcp.WithDescription("List pinned issues in a repository"), + mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), + mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), + ) + + PinIssueTool = mcp.NewTool( + PinIssueToolName, + mcp.WithDescription("Pin an issue in a repository"), + 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("issue index")), + ) + + UnpinIssueTool = mcp.NewTool( + UnpinIssueToolName, + mcp.WithDescription("Unpin an issue from a repository"), + 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("issue index")), + ) + + MoveIssuePinTool = mcp.NewTool( + MoveIssuePinToolName, + mcp.WithDescription("Move a pinned issue to a new position"), + 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("issue index")), + mcp.WithNumber("position", mcp.Required(), mcp.Description("new position (1-based)")), + ) +) + +func init() { + Tool.RegisterRead(server.ServerTool{Tool: ListPinnedIssuesTool, Handler: ListPinnedIssuesFn}) + Tool.RegisterWrite(server.ServerTool{Tool: PinIssueTool, Handler: PinIssueFn}) + Tool.RegisterWrite(server.ServerTool{Tool: UnpinIssueTool, Handler: UnpinIssueFn}) + Tool.RegisterWrite(server.ServerTool{Tool: MoveIssuePinTool, Handler: MoveIssuePinFn}) +} + +func ListPinnedIssuesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called ListPinnedIssuesFn") + owner, _ := req.GetArguments()["owner"].(string) + repo, _ := req.GetArguments()["repo"].(string) + if owner == "" || repo == "" { + return to.ErrorResult(errors.New("owner and repo are required")) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + issues, _, err := client.ListRepoPinnedIssues(owner, repo) + if err != nil { + return to.ErrorResult(fmt.Errorf("list pinned issues err: %v", err)) + } + return to.TextResult(issues) +} + +func PinIssueFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called PinIssueFn") + owner, _ := req.GetArguments()["owner"].(string) + repo, _ := req.GetArguments()["repo"].(string) + if owner == "" || repo == "" { + return to.ErrorResult(errors.New("owner and repo are required")) + } + index, err := params.GetIndex(req.GetArguments(), "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)) + } + _, err = client.PinIssue(owner, repo, index) + if err != nil { + return to.ErrorResult(fmt.Errorf("pin issue err: %v", err)) + } + return to.TextResult(map[string]string{"status": "pinned"}) +} + +func UnpinIssueFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called UnpinIssueFn") + owner, _ := req.GetArguments()["owner"].(string) + repo, _ := req.GetArguments()["repo"].(string) + if owner == "" || repo == "" { + return to.ErrorResult(errors.New("owner and repo are required")) + } + index, err := params.GetIndex(req.GetArguments(), "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)) + } + _, err = client.UnpinIssue(owner, repo, index) + if err != nil { + return to.ErrorResult(fmt.Errorf("unpin issue err: %v", err)) + } + return to.TextResult(map[string]string{"status": "unpinned"}) +} + +func MoveIssuePinFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called MoveIssuePinFn") + owner, _ := req.GetArguments()["owner"].(string) + repo, _ := req.GetArguments()["repo"].(string) + if owner == "" || repo == "" { + return to.ErrorResult(errors.New("owner and repo are required")) + } + index, err := params.GetIndex(req.GetArguments(), "index") + if err != nil { + return to.ErrorResult(err) + } + position, ok := req.GetArguments()["position"].(float64) + if !ok { + return to.ErrorResult(errors.New("position is required")) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + _, err = client.MoveIssuePin(owner, repo, index, int64(position)) + if err != nil { + return to.ErrorResult(fmt.Errorf("move issue pin err: %v", err)) + } + return to.TextResult(map[string]any{"status": "moved", "position": int64(position)}) +} diff --git a/operation/issue/reaction.go b/operation/issue/reaction.go new file mode 100644 index 0000000..99924a6 --- /dev/null +++ b/operation/issue/reaction.go @@ -0,0 +1,224 @@ +package issue + +import ( + "context" + "errors" + "fmt" + + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/gitea" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/log" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/params" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/to" + + gitea_sdk "code.gitea.io/sdk/gitea" + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" +) + +const ( + ListIssueReactionsToolName = "list_issue_reactions" + PostIssueReactionToolName = "post_issue_reaction" + DeleteIssueReactionToolName = "delete_issue_reaction" + GetIssueCommentReactionsToolName = "get_issue_comment_reactions" + PostIssueCommentReactionToolName = "post_issue_comment_reaction" + DeleteIssueCommentReactionToolName = "delete_issue_comment_reaction" +) + +var ( + ListIssueReactionsTool = mcp.NewTool( + ListIssueReactionsToolName, + mcp.WithDescription("List reactions on an issue"), + 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("issue index")), + ) + + PostIssueReactionTool = mcp.NewTool( + PostIssueReactionToolName, + mcp.WithDescription("Add a reaction to an issue"), + 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("issue index")), + mcp.WithString("reaction", mcp.Required(), mcp.Description("reaction emoji (e.g., +1, -1, laugh, confused, heart, hooray, rocket, eyes)")), + ) + + DeleteIssueReactionTool = mcp.NewTool( + DeleteIssueReactionToolName, + mcp.WithDescription("Remove a reaction from an issue"), + 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("issue index")), + mcp.WithString("reaction", mcp.Required(), mcp.Description("reaction emoji to remove")), + ) + + GetIssueCommentReactionsTool = mcp.NewTool( + GetIssueCommentReactionsToolName, + mcp.WithDescription("List reactions on an issue comment"), + mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), + mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), + mcp.WithNumber("comment_id", mcp.Required(), mcp.Description("comment ID")), + ) + + PostIssueCommentReactionTool = mcp.NewTool( + PostIssueCommentReactionToolName, + mcp.WithDescription("Add a reaction to an issue comment"), + mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), + mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), + mcp.WithNumber("comment_id", mcp.Required(), mcp.Description("comment ID")), + mcp.WithString("reaction", mcp.Required(), mcp.Description("reaction emoji")), + ) + + DeleteIssueCommentReactionTool = mcp.NewTool( + DeleteIssueCommentReactionToolName, + mcp.WithDescription("Remove a reaction from an issue comment"), + mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), + mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), + mcp.WithNumber("comment_id", mcp.Required(), mcp.Description("comment ID")), + mcp.WithString("reaction", mcp.Required(), mcp.Description("reaction emoji to remove")), + ) +) + +func init() { + Tool.RegisterRead(server.ServerTool{Tool: ListIssueReactionsTool, Handler: ListIssueReactionsFn}) + Tool.RegisterRead(server.ServerTool{Tool: GetIssueCommentReactionsTool, Handler: GetIssueCommentReactionsFn}) + Tool.RegisterWrite(server.ServerTool{Tool: PostIssueReactionTool, Handler: PostIssueReactionFn}) + Tool.RegisterWrite(server.ServerTool{Tool: DeleteIssueReactionTool, Handler: DeleteIssueReactionFn}) + Tool.RegisterWrite(server.ServerTool{Tool: PostIssueCommentReactionTool, Handler: PostIssueCommentReactionFn}) + Tool.RegisterWrite(server.ServerTool{Tool: DeleteIssueCommentReactionTool, Handler: DeleteIssueCommentReactionFn}) +} + +func ListIssueReactionsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called ListIssueReactionsFn") + owner, _ := req.GetArguments()["owner"].(string) + repo, _ := req.GetArguments()["repo"].(string) + if owner == "" || repo == "" { + return to.ErrorResult(errors.New("owner and repo are required")) + } + index, err := params.GetIndex(req.GetArguments(), "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)) + } + reactions, _, err := client.ListIssueReactions(owner, repo, index, gitea_sdk.ListIssueReactionsOptions{}) + if err != nil { + return to.ErrorResult(fmt.Errorf("list issue reactions err: %v", err)) + } + return to.TextResult(reactions) +} + +func PostIssueReactionFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called PostIssueReactionFn") + owner, _ := req.GetArguments()["owner"].(string) + repo, _ := req.GetArguments()["repo"].(string) + reaction, _ := req.GetArguments()["reaction"].(string) + if owner == "" || repo == "" || reaction == "" { + return to.ErrorResult(errors.New("owner, repo, and reaction are required")) + } + index, err := params.GetIndex(req.GetArguments(), "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)) + } + r, _, err := client.PostIssueReaction(owner, repo, index, reaction) + if err != nil { + return to.ErrorResult(fmt.Errorf("post issue reaction err: %v", err)) + } + return to.TextResult(r) +} + +func DeleteIssueReactionFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called DeleteIssueReactionFn") + owner, _ := req.GetArguments()["owner"].(string) + repo, _ := req.GetArguments()["repo"].(string) + reaction, _ := req.GetArguments()["reaction"].(string) + if owner == "" || repo == "" || reaction == "" { + return to.ErrorResult(errors.New("owner, repo, and reaction are required")) + } + index, err := params.GetIndex(req.GetArguments(), "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)) + } + _, err = client.DeleteIssueReaction(owner, repo, index, reaction) + if err != nil { + return to.ErrorResult(fmt.Errorf("delete issue reaction err: %v", err)) + } + return to.TextResult(map[string]string{"status": "removed", "reaction": reaction}) +} + +func GetIssueCommentReactionsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called GetIssueCommentReactionsFn") + owner, _ := req.GetArguments()["owner"].(string) + repo, _ := req.GetArguments()["repo"].(string) + if owner == "" || repo == "" { + return to.ErrorResult(errors.New("owner and repo are required")) + } + commentID, err := params.GetIndex(req.GetArguments(), "comment_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)) + } + reactions, _, err := client.GetIssueCommentReactions(owner, repo, commentID) + if err != nil { + return to.ErrorResult(fmt.Errorf("get issue comment reactions err: %v", err)) + } + return to.TextResult(reactions) +} + +func PostIssueCommentReactionFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called PostIssueCommentReactionFn") + owner, _ := req.GetArguments()["owner"].(string) + repo, _ := req.GetArguments()["repo"].(string) + reaction, _ := req.GetArguments()["reaction"].(string) + if owner == "" || repo == "" || reaction == "" { + return to.ErrorResult(errors.New("owner, repo, and reaction are required")) + } + commentID, err := params.GetIndex(req.GetArguments(), "comment_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)) + } + r, _, err := client.PostIssueCommentReaction(owner, repo, commentID, reaction) + if err != nil { + return to.ErrorResult(fmt.Errorf("post issue comment reaction err: %v", err)) + } + return to.TextResult(r) +} + +func DeleteIssueCommentReactionFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called DeleteIssueCommentReactionFn") + owner, _ := req.GetArguments()["owner"].(string) + repo, _ := req.GetArguments()["repo"].(string) + reaction, _ := req.GetArguments()["reaction"].(string) + if owner == "" || repo == "" || reaction == "" { + return to.ErrorResult(errors.New("owner, repo, and reaction are required")) + } + commentID, err := params.GetIndex(req.GetArguments(), "comment_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.DeleteIssueCommentReaction(owner, repo, commentID, reaction) + if err != nil { + return to.ErrorResult(fmt.Errorf("delete issue comment reaction err: %v", err)) + } + return to.TextResult(map[string]string{"status": "removed", "reaction": reaction}) +} diff --git a/operation/issue/subscription.go b/operation/issue/subscription.go new file mode 100644 index 0000000..2c2d204 --- /dev/null +++ b/operation/issue/subscription.go @@ -0,0 +1,152 @@ +package issue + +import ( + "context" + "errors" + "fmt" + + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/gitea" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/log" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/params" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/to" + + gitea_sdk "code.gitea.io/sdk/gitea" + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" +) + +const ( + ListIssueSubscribersToolName = "list_issue_subscribers" + CheckIssueSubscriptionToolName = "check_issue_subscription" + IssueSubscribeToolName = "issue_subscribe" + IssueUnSubscribeToolName = "issue_unsubscribe" +) + +var ( + ListIssueSubscribersTool = mcp.NewTool( + ListIssueSubscribersToolName, + mcp.WithDescription("List users subscribed to an issue"), + 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("issue index")), + ) + + CheckIssueSubscriptionTool = mcp.NewTool( + CheckIssueSubscriptionToolName, + mcp.WithDescription("Check if the authenticated user is subscribed to an issue"), + 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("issue index")), + ) + + IssueSubscribeTool = mcp.NewTool( + IssueSubscribeToolName, + mcp.WithDescription("Subscribe to an issue"), + 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("issue index")), + ) + + IssueUnSubscribeTool = mcp.NewTool( + IssueUnSubscribeToolName, + mcp.WithDescription("Unsubscribe from an issue"), + 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("issue index")), + ) +) + +func init() { + Tool.RegisterRead(server.ServerTool{Tool: ListIssueSubscribersTool, Handler: ListIssueSubscribersFn}) + Tool.RegisterRead(server.ServerTool{Tool: CheckIssueSubscriptionTool, Handler: CheckIssueSubscriptionFn}) + Tool.RegisterWrite(server.ServerTool{Tool: IssueSubscribeTool, Handler: IssueSubscribeFn}) + Tool.RegisterWrite(server.ServerTool{Tool: IssueUnSubscribeTool, Handler: IssueUnSubscribeFn}) +} + +func ListIssueSubscribersFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called ListIssueSubscribersFn") + owner, _ := req.GetArguments()["owner"].(string) + repo, _ := req.GetArguments()["repo"].(string) + if owner == "" || repo == "" { + return to.ErrorResult(errors.New("owner and repo are required")) + } + index, err := params.GetIndex(req.GetArguments(), "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)) + } + users, _, err := client.ListIssueSubscribers(owner, repo, index, gitea_sdk.ListIssueSubscribersOptions{}) + if err != nil { + return to.ErrorResult(fmt.Errorf("list issue subscribers err: %v", err)) + } + return to.TextResult(users) +} + +func CheckIssueSubscriptionFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called CheckIssueSubscriptionFn") + owner, _ := req.GetArguments()["owner"].(string) + repo, _ := req.GetArguments()["repo"].(string) + if owner == "" || repo == "" { + return to.ErrorResult(errors.New("owner and repo are required")) + } + index, err := params.GetIndex(req.GetArguments(), "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)) + } + info, _, err := client.CheckIssueSubscription(owner, repo, index) + if err != nil { + return to.ErrorResult(fmt.Errorf("check issue subscription err: %v", err)) + } + return to.TextResult(info) +} + +func IssueSubscribeFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called IssueSubscribeFn") + owner, _ := req.GetArguments()["owner"].(string) + repo, _ := req.GetArguments()["repo"].(string) + if owner == "" || repo == "" { + return to.ErrorResult(errors.New("owner and repo are required")) + } + index, err := params.GetIndex(req.GetArguments(), "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)) + } + _, err = client.IssueSubscribe(owner, repo, index) + if err != nil { + return to.ErrorResult(fmt.Errorf("issue subscribe err: %v", err)) + } + return to.TextResult(map[string]string{"status": "subscribed"}) +} + +func IssueUnSubscribeFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called IssueUnSubscribeFn") + owner, _ := req.GetArguments()["owner"].(string) + repo, _ := req.GetArguments()["repo"].(string) + if owner == "" || repo == "" { + return to.ErrorResult(errors.New("owner and repo are required")) + } + index, err := params.GetIndex(req.GetArguments(), "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)) + } + _, err = client.IssueUnSubscribe(owner, repo, index) + if err != nil { + return to.ErrorResult(fmt.Errorf("issue unsubscribe err: %v", err)) + } + return to.TextResult(map[string]string{"status": "unsubscribed"}) +} diff --git a/operation/issue/timeline.go b/operation/issue/timeline.go new file mode 100644 index 0000000..6180e55 --- /dev/null +++ b/operation/issue/timeline.go @@ -0,0 +1,89 @@ +package issue + +import ( + "context" + "errors" + "fmt" + + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/gitea" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/log" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/params" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/to" + + gitea_sdk "code.gitea.io/sdk/gitea" + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" +) + +const ( + ListIssueTimelineToolName = "list_issue_timeline" + GetIssueTemplatesToolName = "get_issue_templates" +) + +var ( + ListIssueTimelineTool = mcp.NewTool( + ListIssueTimelineToolName, + mcp.WithDescription("List timeline events for an issue (comments, labels, milestones, references, etc.)"), + 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("issue index")), + mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)), + mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)), + ) + + GetIssueTemplatesTool = mcp.NewTool( + GetIssueTemplatesToolName, + mcp.WithDescription("Get issue templates for a repository"), + mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), + mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), + ) +) + +func init() { + Tool.RegisterRead(server.ServerTool{Tool: ListIssueTimelineTool, Handler: ListIssueTimelineFn}) + Tool.RegisterRead(server.ServerTool{Tool: GetIssueTemplatesTool, Handler: GetIssueTemplatesFn}) +} + +func ListIssueTimelineFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called ListIssueTimelineFn") + owner, _ := req.GetArguments()["owner"].(string) + repo, _ := req.GetArguments()["repo"].(string) + if owner == "" || repo == "" { + return to.ErrorResult(errors.New("owner and repo are required")) + } + index, err := params.GetIndex(req.GetArguments(), "index") + if err != nil { + return to.ErrorResult(err) + } + page := params.GetOptionalInt(req.GetArguments(), "page", 1) + pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100) + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + events, _, err := client.ListIssueTimeline(owner, repo, index, gitea_sdk.ListIssueCommentOptions{ + ListOptions: gitea_sdk.ListOptions{Page: int(page), PageSize: int(pageSize)}, + }) + if err != nil { + return to.ErrorResult(fmt.Errorf("list issue timeline err: %v", err)) + } + return to.TextResult(events) +} + +func GetIssueTemplatesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called GetIssueTemplatesFn") + owner, _ := req.GetArguments()["owner"].(string) + repo, _ := req.GetArguments()["repo"].(string) + if owner == "" || repo == "" { + return to.ErrorResult(errors.New("owner and repo are required")) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + templates, _, err := client.GetIssueTemplates(owner, repo) + if err != nil { + return to.ErrorResult(fmt.Errorf("get issue templates err: %v", err)) + } + return to.TextResult(templates) +} diff --git a/operation/label/label.go b/operation/label/label.go index acb6128..d2e7488 100644 --- a/operation/label/label.go +++ b/operation/label/label.go @@ -5,11 +5,11 @@ import ( "errors" "fmt" - "gitea.com/gitea/gitea-mcp/pkg/gitea" - "gitea.com/gitea/gitea-mcp/pkg/log" - "gitea.com/gitea/gitea-mcp/pkg/params" - "gitea.com/gitea/gitea-mcp/pkg/to" - "gitea.com/gitea/gitea-mcp/pkg/tool" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/gitea" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/log" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/params" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/to" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/tool" gitea_sdk "code.gitea.io/sdk/gitea" "github.com/mark3labs/mcp-go/mcp" diff --git a/operation/milestone/milestone.go b/operation/milestone/milestone.go index 8f853f3..9f847ac 100644 --- a/operation/milestone/milestone.go +++ b/operation/milestone/milestone.go @@ -5,11 +5,11 @@ import ( "errors" "fmt" - "gitea.com/gitea/gitea-mcp/pkg/gitea" - "gitea.com/gitea/gitea-mcp/pkg/log" - "gitea.com/gitea/gitea-mcp/pkg/params" - "gitea.com/gitea/gitea-mcp/pkg/to" - "gitea.com/gitea/gitea-mcp/pkg/tool" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/gitea" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/log" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/params" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/to" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/tool" gitea_sdk "code.gitea.io/sdk/gitea" "github.com/mark3labs/mcp-go/mcp" diff --git a/operation/miscellaneous/miscellaneous.go b/operation/miscellaneous/miscellaneous.go new file mode 100644 index 0000000..f8e5549 --- /dev/null +++ b/operation/miscellaneous/miscellaneous.go @@ -0,0 +1,325 @@ +package miscellaneous + +import ( + "context" + "errors" + "fmt" + + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/gitea" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/log" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/to" + "git.lethalbits.com/lethalbits/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 ( + GetServerVersionToolName = "get_server_version" + ListGitignoreTemplatesToolName = "list_gitignore_templates" + GetGitignoreTemplateToolName = "get_gitignore_template" + ListLabelTemplatesToolName = "list_label_templates" + GetLabelTemplateToolName = "get_label_template" + ListLicenseTemplatesToolName = "list_license_templates" + GetLicenseTemplateToolName = "get_license_template" + RenderMarkdownToolName = "render_markdown" + RenderMarkupToolName = "render_markup" + GetNodeInfoToolName = "get_node_info" + GetSigningKeyGPGToolName = "get_signing_key_gpg" + GetSigningKeySSHToolName = "get_signing_key_ssh" +) + +var ( + GetServerVersionTool = mcp.NewTool( + GetServerVersionToolName, + mcp.WithDescription("Get the Gitea server version"), + ) + + ListGitignoreTemplatesTool = mcp.NewTool( + ListGitignoreTemplatesToolName, + mcp.WithDescription("List available .gitignore templates"), + ) + + GetGitignoreTemplateTool = mcp.NewTool( + GetGitignoreTemplateToolName, + mcp.WithDescription("Get the content of a .gitignore template by name"), + mcp.WithString("name", mcp.Required(), mcp.Description("template name (e.g., Go, Python, Node)")), + ) + + ListLabelTemplatesTool = mcp.NewTool( + ListLabelTemplatesToolName, + mcp.WithDescription("List available label templates"), + ) + + GetLabelTemplateTool = mcp.NewTool( + GetLabelTemplateToolName, + mcp.WithDescription("Get the labels from a label template"), + mcp.WithString("name", mcp.Required(), mcp.Description("template name")), + ) + + ListLicenseTemplatesTool = mcp.NewTool( + ListLicenseTemplatesToolName, + mcp.WithDescription("List available license templates"), + ) + + GetLicenseTemplateTool = mcp.NewTool( + GetLicenseTemplateToolName, + mcp.WithDescription("Get the content of a license template"), + mcp.WithString("name", mcp.Required(), mcp.Description("license name (e.g., MIT, GPL-3.0, Apache-2.0)")), + ) + + RenderMarkdownTool = mcp.NewTool( + RenderMarkdownToolName, + mcp.WithDescription("Render markdown text to HTML"), + mcp.WithString("text", mcp.Required(), mcp.Description("markdown text to render")), + mcp.WithString("mode", mcp.Description("render mode: markdown, gfm, comment (default: markdown)")), + mcp.WithString("context", mcp.Description("context for relative links (owner/repo format)")), + mcp.WithBoolean("wiki", mcp.Description("treat text as wiki content")), + ) + + RenderMarkupTool = mcp.NewTool( + RenderMarkupToolName, + mcp.WithDescription("Render markup content to HTML (supports multiple markup languages)"), + mcp.WithString("text", mcp.Required(), mcp.Description("markup text to render")), + mcp.WithString("mode", mcp.Description("render mode")), + mcp.WithString("context", mcp.Description("context for relative links (owner/repo format)")), + mcp.WithString("filepath", mcp.Description("file path to determine markup language")), + mcp.WithBoolean("wiki", mcp.Description("treat text as wiki content")), + ) + + GetNodeInfoTool = mcp.NewTool( + GetNodeInfoToolName, + mcp.WithDescription("Get the NodeInfo of the Gitea instance (federation metadata)"), + ) + + GetSigningKeyGPGTool = mcp.NewTool( + GetSigningKeyGPGToolName, + mcp.WithDescription("Get the GPG signing key of the Gitea instance"), + ) + + GetSigningKeySSHTool = mcp.NewTool( + GetSigningKeySSHToolName, + mcp.WithDescription("Get the SSH signing key of the Gitea instance"), + ) +) + +func init() { + Tool.RegisterRead(server.ServerTool{Tool: GetServerVersionTool, Handler: GetServerVersionFn}) + Tool.RegisterRead(server.ServerTool{Tool: ListGitignoreTemplatesTool, Handler: ListGitignoreTemplatesFn}) + Tool.RegisterRead(server.ServerTool{Tool: GetGitignoreTemplateTool, Handler: GetGitignoreTemplateFn}) + Tool.RegisterRead(server.ServerTool{Tool: ListLabelTemplatesTool, Handler: ListLabelTemplatesFn}) + Tool.RegisterRead(server.ServerTool{Tool: GetLabelTemplateTool, Handler: GetLabelTemplateFn}) + Tool.RegisterRead(server.ServerTool{Tool: ListLicenseTemplatesTool, Handler: ListLicenseTemplatesFn}) + Tool.RegisterRead(server.ServerTool{Tool: GetLicenseTemplateTool, Handler: GetLicenseTemplateFn}) + Tool.RegisterRead(server.ServerTool{Tool: GetNodeInfoTool, Handler: GetNodeInfoFn}) + Tool.RegisterRead(server.ServerTool{Tool: GetSigningKeyGPGTool, Handler: GetSigningKeyGPGFn}) + Tool.RegisterRead(server.ServerTool{Tool: GetSigningKeySSHTool, Handler: GetSigningKeySSHFn}) + Tool.RegisterWrite(server.ServerTool{Tool: RenderMarkdownTool, Handler: RenderMarkdownFn}) + Tool.RegisterWrite(server.ServerTool{Tool: RenderMarkupTool, Handler: RenderMarkupFn}) +} + +func GetServerVersionFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called GetServerVersionFn") + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + version, _, err := client.ServerVersion() + if err != nil { + return to.ErrorResult(fmt.Errorf("get server version err: %v", err)) + } + return to.TextResult(map[string]string{"version": version}) +} + +func ListGitignoreTemplatesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called ListGitignoreTemplatesFn") + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + templates, _, err := client.ListGitignoresTemplates() + if err != nil { + return to.ErrorResult(fmt.Errorf("list gitignore templates err: %v", err)) + } + return to.TextResult(templates) +} + +func GetGitignoreTemplateFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called GetGitignoreTemplateFn") + name, _ := req.GetArguments()["name"].(string) + if name == "" { + return to.ErrorResult(errors.New("name is required")) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + info, _, err := client.GetGitignoreTemplateInfo(name) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitignore template err: %v", err)) + } + return to.TextResult(info) +} + +func ListLabelTemplatesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called ListLabelTemplatesFn") + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + templates, _, err := client.ListLabelTemplates() + if err != nil { + return to.ErrorResult(fmt.Errorf("list label templates err: %v", err)) + } + return to.TextResult(templates) +} + +func GetLabelTemplateFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called GetLabelTemplateFn") + name, _ := req.GetArguments()["name"].(string) + if name == "" { + return to.ErrorResult(errors.New("name is required")) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + labels, _, err := client.GetLabelTemplate(name) + if err != nil { + return to.ErrorResult(fmt.Errorf("get label template err: %v", err)) + } + return to.TextResult(labels) +} + +func ListLicenseTemplatesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called ListLicenseTemplatesFn") + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + templates, _, err := client.ListLicenseTemplates() + if err != nil { + return to.ErrorResult(fmt.Errorf("list license templates err: %v", err)) + } + return to.TextResult(templates) +} + +func GetLicenseTemplateFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called GetLicenseTemplateFn") + name, _ := req.GetArguments()["name"].(string) + if name == "" { + return to.ErrorResult(errors.New("name is required")) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + info, _, err := client.GetLicenseTemplateInfo(name) + if err != nil { + return to.ErrorResult(fmt.Errorf("get license template err: %v", err)) + } + return to.TextResult(info) +} + +func RenderMarkdownFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called RenderMarkdownFn") + text, _ := req.GetArguments()["text"].(string) + if text == "" { + return to.ErrorResult(errors.New("text is required")) + } + opt := gitea_sdk.MarkdownOption{ + Text: text, + } + if v, ok := req.GetArguments()["mode"].(string); ok { + opt.Mode = v + } + if v, ok := req.GetArguments()["context"].(string); ok { + opt.Context = v + } + if v, ok := req.GetArguments()["wiki"].(bool); ok { + opt.Wiki = v + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + html, _, err := client.RenderMarkdown(opt) + if err != nil { + return to.ErrorResult(fmt.Errorf("render markdown err: %v", err)) + } + return to.TextResult(map[string]string{"html": html}) +} + +func RenderMarkupFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called RenderMarkupFn") + text, _ := req.GetArguments()["text"].(string) + if text == "" { + return to.ErrorResult(errors.New("text is required")) + } + opt := gitea_sdk.MarkupOption{ + Text: text, + } + if v, ok := req.GetArguments()["mode"].(string); ok { + opt.Mode = v + } + if v, ok := req.GetArguments()["context"].(string); ok { + opt.Context = v + } + if v, ok := req.GetArguments()["filepath"].(string); ok { + opt.FilePath = v + } + if v, ok := req.GetArguments()["wiki"].(bool); ok { + opt.Wiki = v + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + html, _, err := client.RenderMarkup(opt) + if err != nil { + return to.ErrorResult(fmt.Errorf("render markup err: %v", err)) + } + return to.TextResult(map[string]string{"html": html}) +} + +func GetNodeInfoFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called GetNodeInfoFn") + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + info, _, err := client.GetNodeInfo() + if err != nil { + return to.ErrorResult(fmt.Errorf("get node info err: %v", err)) + } + return to.TextResult(info) +} + +func GetSigningKeyGPGFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called GetSigningKeyGPGFn") + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + key, _, err := client.GetSigningKeyGPG() + if err != nil { + return to.ErrorResult(fmt.Errorf("get signing key GPG err: %v", err)) + } + return to.TextResult(map[string]string{"gpg_key": key}) +} + +func GetSigningKeySSHFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called GetSigningKeySSHFn") + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + key, _, err := client.GetSigningKeySSH() + if err != nil { + return to.ErrorResult(fmt.Errorf("get signing key SSH err: %v", err)) + } + return to.TextResult(map[string]string{"ssh_key": key}) +} diff --git a/operation/notification/notification.go b/operation/notification/notification.go new file mode 100644 index 0000000..922bc41 --- /dev/null +++ b/operation/notification/notification.go @@ -0,0 +1,219 @@ +package notification + +import ( + "context" + "errors" + "fmt" + + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/gitea" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/log" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/params" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/to" + "git.lethalbits.com/lethalbits/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 ( + ListNotificationsToolName = "list_notifications" + CheckNewNotificationsToolName = "check_new_notifications" + GetNotificationToolName = "get_notification" + ReadNotificationToolName = "read_notification" + ReadAllNotificationsToolName = "read_all_notifications" + ListRepoNotificationsToolName = "list_repo_notifications" + ReadRepoNotificationsToolName = "read_repo_notifications" +) + +var ( + ListNotificationsTool = mcp.NewTool( + ListNotificationsToolName, + mcp.WithDescription("List the authenticated user's notifications"), + mcp.WithString("status", mcp.Description("filter by status: read, unread, pinned")), + mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)), + mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)), + ) + + CheckNewNotificationsTool = mcp.NewTool( + CheckNewNotificationsToolName, + mcp.WithDescription("Check if there are new notifications"), + ) + + GetNotificationTool = mcp.NewTool( + GetNotificationToolName, + mcp.WithDescription("Get a notification thread by ID"), + mcp.WithNumber("id", mcp.Required(), mcp.Description("notification thread ID")), + ) + + ReadNotificationTool = mcp.NewTool( + ReadNotificationToolName, + mcp.WithDescription("Mark a notification as read"), + mcp.WithNumber("id", mcp.Required(), mcp.Description("notification thread ID")), + ) + + ReadAllNotificationsTool = mcp.NewTool( + ReadAllNotificationsToolName, + mcp.WithDescription("Mark all notifications as read"), + ) + + ListRepoNotificationsTool = mcp.NewTool( + ListRepoNotificationsToolName, + mcp.WithDescription("List notifications for a repository"), + mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), + mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), + mcp.WithString("status", mcp.Description("filter by status: read, unread, pinned")), + mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)), + mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)), + ) + + ReadRepoNotificationsTool = mcp.NewTool( + ReadRepoNotificationsToolName, + mcp.WithDescription("Mark all notifications in a repository as read"), + mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), + mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), + ) +) + +func init() { + Tool.RegisterRead(server.ServerTool{Tool: ListNotificationsTool, Handler: ListNotificationsFn}) + Tool.RegisterRead(server.ServerTool{Tool: CheckNewNotificationsTool, Handler: CheckNewNotificationsFn}) + Tool.RegisterRead(server.ServerTool{Tool: GetNotificationTool, Handler: GetNotificationFn}) + Tool.RegisterRead(server.ServerTool{Tool: ListRepoNotificationsTool, Handler: ListRepoNotificationsFn}) + Tool.RegisterWrite(server.ServerTool{Tool: ReadNotificationTool, Handler: ReadNotificationFn}) + Tool.RegisterWrite(server.ServerTool{Tool: ReadAllNotificationsTool, Handler: ReadAllNotificationsFn}) + Tool.RegisterWrite(server.ServerTool{Tool: ReadRepoNotificationsTool, Handler: ReadRepoNotificationsFn}) +} + +func ListNotificationsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called ListNotificationsFn") + page := params.GetOptionalInt(req.GetArguments(), "page", 1) + pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100) + opt := gitea_sdk.ListNotificationOptions{ + ListOptions: gitea_sdk.ListOptions{Page: int(page), PageSize: int(pageSize)}, + } + if v, ok := req.GetArguments()["status"].(string); ok { + opt.Status = []gitea_sdk.NotifyStatus{gitea_sdk.NotifyStatus(v)} + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + notifications, _, err := client.ListNotifications(opt) + if err != nil { + return to.ErrorResult(fmt.Errorf("list notifications err: %v", err)) + } + return to.TextResult(notifications) +} + +func CheckNewNotificationsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called CheckNewNotificationsFn") + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + count, _, err := client.CheckNotifications() + if err != nil { + return to.ErrorResult(fmt.Errorf("check notifications err: %v", err)) + } + return to.TextResult(map[string]int64{"new_notifications": count}) +} + +func GetNotificationFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called GetNotificationFn") + 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)) + } + notification, _, err := client.GetNotification(id) + if err != nil { + return to.ErrorResult(fmt.Errorf("get notification %d err: %v", id, err)) + } + return to.TextResult(notification) +} + +func ReadNotificationFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called ReadNotificationFn") + 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)) + } + notification, _, err := client.ReadNotification(id) + if err != nil { + return to.ErrorResult(fmt.Errorf("read notification %d err: %v", id, err)) + } + return to.TextResult(notification) +} + +func ReadAllNotificationsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called ReadAllNotificationsFn") + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + notifications, _, err := client.ReadNotifications(gitea_sdk.MarkNotificationOptions{}) + if err != nil { + return to.ErrorResult(fmt.Errorf("read all notifications err: %v", err)) + } + return to.TextResult(notifications) +} + +func ListRepoNotificationsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called ListRepoNotificationsFn") + owner, ok := req.GetArguments()["owner"].(string) + if !ok { + return to.ErrorResult(errors.New("owner is required")) + } + repo, ok := req.GetArguments()["repo"].(string) + if !ok { + return to.ErrorResult(errors.New("repo is required")) + } + page := params.GetOptionalInt(req.GetArguments(), "page", 1) + pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100) + opt := gitea_sdk.ListNotificationOptions{ + ListOptions: gitea_sdk.ListOptions{Page: int(page), PageSize: int(pageSize)}, + } + if v, ok := req.GetArguments()["status"].(string); ok { + opt.Status = []gitea_sdk.NotifyStatus{gitea_sdk.NotifyStatus(v)} + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + notifications, _, err := client.ListRepoNotifications(owner, repo, opt) + if err != nil { + return to.ErrorResult(fmt.Errorf("list %s/%s notifications err: %v", owner, repo, err)) + } + return to.TextResult(notifications) +} + +func ReadRepoNotificationsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called ReadRepoNotificationsFn") + owner, ok := req.GetArguments()["owner"].(string) + if !ok { + return to.ErrorResult(errors.New("owner is required")) + } + repo, ok := req.GetArguments()["repo"].(string) + if !ok { + return to.ErrorResult(errors.New("repo is required")) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + notifications, _, err := client.ReadRepoNotifications(owner, repo, gitea_sdk.MarkNotificationOptions{}) + if err != nil { + return to.ErrorResult(fmt.Errorf("read %s/%s notifications err: %v", owner, repo, err)) + } + return to.TextResult(notifications) +} diff --git a/operation/operation.go b/operation/operation.go index 8864101..739a160 100644 --- a/operation/operation.go +++ b/operation/operation.go @@ -10,20 +10,26 @@ import ( "syscall" "time" - "gitea.com/gitea/gitea-mcp/operation/actions" - "gitea.com/gitea/gitea-mcp/operation/issue" - "gitea.com/gitea/gitea-mcp/operation/label" - "gitea.com/gitea/gitea-mcp/operation/milestone" - "gitea.com/gitea/gitea-mcp/operation/pull" - "gitea.com/gitea/gitea-mcp/operation/repo" - "gitea.com/gitea/gitea-mcp/operation/search" - "gitea.com/gitea/gitea-mcp/operation/timetracking" - "gitea.com/gitea/gitea-mcp/operation/user" - "gitea.com/gitea/gitea-mcp/operation/version" - "gitea.com/gitea/gitea-mcp/operation/wiki" - mcpContext "gitea.com/gitea/gitea-mcp/pkg/context" - "gitea.com/gitea/gitea-mcp/pkg/flag" - "gitea.com/gitea/gitea-mcp/pkg/log" + "git.lethalbits.com/lethalbits/gitea-mcp/operation/actions" + "git.lethalbits.com/lethalbits/gitea-mcp/operation/admin" + "git.lethalbits.com/lethalbits/gitea-mcp/operation/issue" + "git.lethalbits.com/lethalbits/gitea-mcp/operation/label" + "git.lethalbits.com/lethalbits/gitea-mcp/operation/milestone" + "git.lethalbits.com/lethalbits/gitea-mcp/operation/miscellaneous" + "git.lethalbits.com/lethalbits/gitea-mcp/operation/notification" + "git.lethalbits.com/lethalbits/gitea-mcp/operation/organization" + "git.lethalbits.com/lethalbits/gitea-mcp/operation/packages" + "git.lethalbits.com/lethalbits/gitea-mcp/operation/pull" + "git.lethalbits.com/lethalbits/gitea-mcp/operation/repo" + "git.lethalbits.com/lethalbits/gitea-mcp/operation/search" + "git.lethalbits.com/lethalbits/gitea-mcp/operation/settings" + "git.lethalbits.com/lethalbits/gitea-mcp/operation/timetracking" + "git.lethalbits.com/lethalbits/gitea-mcp/operation/user" + "git.lethalbits.com/lethalbits/gitea-mcp/operation/version" + "git.lethalbits.com/lethalbits/gitea-mcp/operation/wiki" + mcpContext "git.lethalbits.com/lethalbits/gitea-mcp/pkg/context" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/flag" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/log" "github.com/mark3labs/mcp-go/server" ) @@ -64,6 +70,24 @@ func RegisterTool(s *server.MCPServer) { // Time Tracking Tool s.AddTools(timetracking.Tool.Tools()...) + // Organization Tool + s.AddTools(organization.Tool.Tools()...) + + // Notification Tool + s.AddTools(notification.Tool.Tools()...) + + // Settings Tool + s.AddTools(settings.Tool.Tools()...) + + // Package Tool + s.AddTools(packages.Tool.Tools()...) + + // Miscellaneous Tool + s.AddTools(miscellaneous.Tool.Tools()...) + + // Admin Tool + s.AddTools(admin.Tool.Tools()...) + s.DeleteTools("") } diff --git a/operation/organization/activity.go b/operation/organization/activity.go new file mode 100644 index 0000000..1e217e1 --- /dev/null +++ b/operation/organization/activity.go @@ -0,0 +1,86 @@ +package organization + +import ( + "context" + "errors" + "fmt" + + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/gitea" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/log" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/params" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/to" + + gitea_sdk "code.gitea.io/sdk/gitea" + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" +) + +const ( + ListOrgActivityFeedsToolName = "list_org_activity_feeds" + ListTeamActivityFeedsToolName = "list_team_activity_feeds" +) + +var ( + ListOrgActivityFeedsTool = mcp.NewTool( + ListOrgActivityFeedsToolName, + mcp.WithDescription("List an organization's activity feeds"), + mcp.WithString("org", mcp.Required(), mcp.Description("organization name")), + mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)), + mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)), + ) + + ListTeamActivityFeedsTool = mcp.NewTool( + ListTeamActivityFeedsToolName, + mcp.WithDescription("List a team's activity feeds"), + mcp.WithNumber("id", mcp.Required(), mcp.Description("team ID")), + mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)), + mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)), + ) +) + +func init() { + Tool.RegisterRead(server.ServerTool{Tool: ListOrgActivityFeedsTool, Handler: ListOrgActivityFeedsFn}) + Tool.RegisterRead(server.ServerTool{Tool: ListTeamActivityFeedsTool, Handler: ListTeamActivityFeedsFn}) +} + +func ListOrgActivityFeedsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called ListOrgActivityFeedsFn") + org, ok := req.GetArguments()["org"].(string) + if !ok { + return to.ErrorResult(errors.New("org is required")) + } + page := params.GetOptionalInt(req.GetArguments(), "page", 1) + pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100) + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + feeds, _, err := client.ListOrgActivityFeeds(org, gitea_sdk.ListOrgActivityFeedsOptions{ + ListOptions: gitea_sdk.ListOptions{Page: int(page), PageSize: int(pageSize)}, + }) + if err != nil { + return to.ErrorResult(fmt.Errorf("list org %s activity feeds err: %v", org, err)) + } + return to.TextResult(feeds) +} + +func ListTeamActivityFeedsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called ListTeamActivityFeedsFn") + id, err := params.GetIndex(req.GetArguments(), "id") + if err != nil { + return to.ErrorResult(err) + } + page := params.GetOptionalInt(req.GetArguments(), "page", 1) + pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100) + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + feeds, _, err := client.ListTeamActivityFeeds(id, gitea_sdk.ListTeamActivityFeedsOptions{ + ListOptions: gitea_sdk.ListOptions{Page: int(page), PageSize: int(pageSize)}, + }) + if err != nil { + return to.ErrorResult(fmt.Errorf("list team %d activity feeds err: %v", id, err)) + } + return to.TextResult(feeds) +} diff --git a/operation/organization/blocks.go b/operation/organization/blocks.go new file mode 100644 index 0000000..0a7656e --- /dev/null +++ b/operation/organization/blocks.go @@ -0,0 +1,145 @@ +package organization + +import ( + "context" + "errors" + "fmt" + + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/gitea" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/log" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/params" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/to" + + gitea_sdk "code.gitea.io/sdk/gitea" + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" +) + +const ( + ListOrgBlocksToolName = "list_org_blocks" + CheckOrgBlockToolName = "check_org_block" + BlockOrgUserToolName = "block_org_user" + UnblockOrgUserToolName = "unblock_org_user" +) + +var ( + ListOrgBlocksTool = mcp.NewTool( + ListOrgBlocksToolName, + mcp.WithDescription("List users blocked by an organization"), + mcp.WithString("org", mcp.Required(), mcp.Description("organization name")), + mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)), + mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)), + ) + + CheckOrgBlockTool = mcp.NewTool( + CheckOrgBlockToolName, + mcp.WithDescription("Check if a user is blocked by an organization"), + mcp.WithString("org", mcp.Required(), mcp.Description("organization name")), + mcp.WithString("username", mcp.Required(), mcp.Description("username to check")), + ) + + BlockOrgUserTool = mcp.NewTool( + BlockOrgUserToolName, + mcp.WithDescription("Block a user from an organization"), + mcp.WithString("org", mcp.Required(), mcp.Description("organization name")), + mcp.WithString("username", mcp.Required(), mcp.Description("username to block")), + ) + + UnblockOrgUserTool = mcp.NewTool( + UnblockOrgUserToolName, + mcp.WithDescription("Unblock a user from an organization"), + mcp.WithString("org", mcp.Required(), mcp.Description("organization name")), + mcp.WithString("username", mcp.Required(), mcp.Description("username to unblock")), + ) +) + +func init() { + Tool.RegisterRead(server.ServerTool{Tool: ListOrgBlocksTool, Handler: ListOrgBlocksFn}) + Tool.RegisterRead(server.ServerTool{Tool: CheckOrgBlockTool, Handler: CheckOrgBlockFn}) + Tool.RegisterWrite(server.ServerTool{Tool: BlockOrgUserTool, Handler: BlockOrgUserFn}) + Tool.RegisterWrite(server.ServerTool{Tool: UnblockOrgUserTool, Handler: UnblockOrgUserFn}) +} + +func ListOrgBlocksFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called ListOrgBlocksFn") + org, ok := req.GetArguments()["org"].(string) + if !ok { + return to.ErrorResult(errors.New("org is required")) + } + page := params.GetOptionalInt(req.GetArguments(), "page", 1) + pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100) + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + users, _, err := client.ListOrgBlocks(org, gitea_sdk.ListOrgBlocksOptions{ + ListOptions: gitea_sdk.ListOptions{Page: int(page), PageSize: int(pageSize)}, + }) + if err != nil { + return to.ErrorResult(fmt.Errorf("list org %s blocks err: %v", org, err)) + } + return to.TextResult(users) +} + +func CheckOrgBlockFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called CheckOrgBlockFn") + org, ok := req.GetArguments()["org"].(string) + if !ok { + return to.ErrorResult(errors.New("org is required")) + } + username, ok := req.GetArguments()["username"].(string) + if !ok { + return to.ErrorResult(errors.New("username is required")) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + isBlocked, _, err := client.CheckOrgBlock(org, username) + if err != nil { + return to.ErrorResult(fmt.Errorf("check org %s block for %s err: %v", org, username, err)) + } + return to.TextResult(map[string]any{"org": org, "username": username, "is_blocked": isBlocked}) +} + +func BlockOrgUserFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called BlockOrgUserFn") + org, ok := req.GetArguments()["org"].(string) + if !ok { + return to.ErrorResult(errors.New("org is required")) + } + username, ok := req.GetArguments()["username"].(string) + if !ok { + return to.ErrorResult(errors.New("username is required")) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + _, err = client.BlockOrgUser(org, username) + if err != nil { + return to.ErrorResult(fmt.Errorf("block user %s from org %s err: %v", username, org, err)) + } + return to.TextResult(map[string]string{"status": "blocked", "org": org, "username": username}) +} + +func UnblockOrgUserFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called UnblockOrgUserFn") + org, ok := req.GetArguments()["org"].(string) + if !ok { + return to.ErrorResult(errors.New("org is required")) + } + username, ok := req.GetArguments()["username"].(string) + if !ok { + return to.ErrorResult(errors.New("username is required")) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + _, err = client.UnblockOrgUser(org, username) + if err != nil { + return to.ErrorResult(fmt.Errorf("unblock user %s from org %s err: %v", username, org, err)) + } + return to.TextResult(map[string]string{"status": "unblocked", "org": org, "username": username}) +} diff --git a/operation/organization/hooks.go b/operation/organization/hooks.go new file mode 100644 index 0000000..b88206b --- /dev/null +++ b/operation/organization/hooks.go @@ -0,0 +1,251 @@ +package organization + +import ( + "context" + "errors" + "fmt" + + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/gitea" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/log" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/params" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/to" + + gitea_sdk "code.gitea.io/sdk/gitea" + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" +) + +const ( + ListOrgHooksToolName = "list_org_hooks" + GetOrgHookToolName = "get_org_hook" + CreateOrgHookToolName = "create_org_hook" + EditOrgHookToolName = "edit_org_hook" + DeleteOrgHookToolName = "delete_org_hook" +) + +var ( + ListOrgHooksTool = mcp.NewTool( + ListOrgHooksToolName, + mcp.WithDescription("List an organization's webhooks"), + mcp.WithString("org", mcp.Required(), mcp.Description("organization name")), + mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)), + mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)), + ) + + GetOrgHookTool = mcp.NewTool( + GetOrgHookToolName, + mcp.WithDescription("Get an organization webhook by ID"), + mcp.WithString("org", mcp.Required(), mcp.Description("organization name")), + mcp.WithNumber("id", mcp.Required(), mcp.Description("webhook ID")), + ) + + CreateOrgHookTool = mcp.NewTool( + CreateOrgHookToolName, + mcp.WithDescription("Create an organization webhook"), + mcp.WithString("org", mcp.Required(), mcp.Description("organization name")), + mcp.WithString("type", mcp.Required(), mcp.Description("hook type: gitea, slack, discord, dingtalk, telegram, msteams, feishu, matrix, wechatwork, packagist")), + mcp.WithString("url", mcp.Required(), mcp.Description("target URL for the webhook")), + mcp.WithString("content_type", mcp.Description("content type: json or form"), mcp.DefaultString("json")), + mcp.WithString("secret", mcp.Description("webhook secret")), + mcp.WithBoolean("active", mcp.Description("whether the webhook is active (default: true)")), + mcp.WithArray("events", mcp.Description("list of events to trigger on"), mcp.Items(map[string]any{"type": "string"})), + ) + + EditOrgHookTool = mcp.NewTool( + EditOrgHookToolName, + mcp.WithDescription("Edit an organization webhook"), + mcp.WithString("org", mcp.Required(), mcp.Description("organization name")), + mcp.WithNumber("id", mcp.Required(), mcp.Description("webhook ID")), + mcp.WithString("url", mcp.Description("target URL")), + mcp.WithString("content_type", mcp.Description("content type: json or form")), + mcp.WithString("secret", mcp.Description("webhook secret")), + mcp.WithBoolean("active", mcp.Description("whether the webhook is active")), + mcp.WithArray("events", mcp.Description("list of events to trigger on"), mcp.Items(map[string]any{"type": "string"})), + ) + + DeleteOrgHookTool = mcp.NewTool( + DeleteOrgHookToolName, + mcp.WithDescription("Delete an organization webhook"), + mcp.WithString("org", mcp.Required(), mcp.Description("organization name")), + mcp.WithNumber("id", mcp.Required(), mcp.Description("webhook ID")), + ) +) + +func init() { + Tool.RegisterRead(server.ServerTool{Tool: ListOrgHooksTool, Handler: ListOrgHooksFn}) + Tool.RegisterRead(server.ServerTool{Tool: GetOrgHookTool, Handler: GetOrgHookFn}) + Tool.RegisterWrite(server.ServerTool{Tool: CreateOrgHookTool, Handler: CreateOrgHookFn}) + Tool.RegisterWrite(server.ServerTool{Tool: EditOrgHookTool, Handler: EditOrgHookFn}) + Tool.RegisterWrite(server.ServerTool{Tool: DeleteOrgHookTool, Handler: DeleteOrgHookFn}) +} + +func ListOrgHooksFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called ListOrgHooksFn") + org, ok := req.GetArguments()["org"].(string) + if !ok { + return to.ErrorResult(errors.New("org is required")) + } + page := params.GetOptionalInt(req.GetArguments(), "page", 1) + pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100) + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + hooks, _, err := client.ListOrgHooks(org, gitea_sdk.ListHooksOptions{ + ListOptions: gitea_sdk.ListOptions{Page: int(page), PageSize: int(pageSize)}, + }) + if err != nil { + return to.ErrorResult(fmt.Errorf("list org %s hooks err: %v", org, err)) + } + return to.TextResult(hooks) +} + +func GetOrgHookFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called GetOrgHookFn") + org, ok := req.GetArguments()["org"].(string) + if !ok { + return to.ErrorResult(errors.New("org is required")) + } + id, err := params.GetIndex(req.GetArguments(), "id") + if err != nil { + return to.ErrorResult(err) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + hook, _, err := client.GetOrgHook(org, id) + if err != nil { + return to.ErrorResult(fmt.Errorf("get org %s hook %d err: %v", org, id, err)) + } + return to.TextResult(hook) +} + +func CreateOrgHookFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called CreateOrgHookFn") + org, ok := req.GetArguments()["org"].(string) + if !ok { + return to.ErrorResult(errors.New("org is required")) + } + hookType, ok := req.GetArguments()["type"].(string) + if !ok { + return to.ErrorResult(errors.New("type is required")) + } + hookURL, ok := req.GetArguments()["url"].(string) + if !ok { + return to.ErrorResult(errors.New("url is required")) + } + contentType := "json" + if v, ok := req.GetArguments()["content_type"].(string); ok { + contentType = v + } + config := map[string]string{ + "url": hookURL, + "content_type": contentType, + } + if v, ok := req.GetArguments()["secret"].(string); ok { + config["secret"] = v + } + opt := gitea_sdk.CreateHookOption{ + Type: gitea_sdk.HookType(hookType), + Config: config, + Active: true, + } + if v, ok := req.GetArguments()["active"].(bool); ok { + opt.Active = v + } + if eventsArg, exists := req.GetArguments()["events"]; exists { + if eventsSlice, ok := eventsArg.([]any); ok { + events := make([]string, 0, len(eventsSlice)) + for _, e := range eventsSlice { + if s, ok := e.(string); ok { + events = append(events, s) + } + } + opt.Events = events + } + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + hook, _, err := client.CreateOrgHook(org, opt) + if err != nil { + return to.ErrorResult(fmt.Errorf("create org %s hook err: %v", org, err)) + } + return to.TextResult(hook) +} + +func EditOrgHookFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called EditOrgHookFn") + org, ok := req.GetArguments()["org"].(string) + if !ok { + return to.ErrorResult(errors.New("org is required")) + } + id, err := params.GetIndex(req.GetArguments(), "id") + if err != nil { + return to.ErrorResult(err) + } + opt := gitea_sdk.EditHookOption{} + config := map[string]string{} + if v, ok := req.GetArguments()["url"].(string); ok { + config["url"] = v + } + if v, ok := req.GetArguments()["content_type"].(string); ok { + config["content_type"] = v + } + if v, ok := req.GetArguments()["secret"].(string); ok { + config["secret"] = v + } + if len(config) > 0 { + opt.Config = config + } + if v, ok := req.GetArguments()["active"].(bool); ok { + opt.Active = &v + } + if eventsArg, exists := req.GetArguments()["events"]; exists { + if eventsSlice, ok := eventsArg.([]any); ok { + events := make([]string, 0, len(eventsSlice)) + for _, e := range eventsSlice { + if s, ok := e.(string); ok { + events = append(events, s) + } + } + opt.Events = events + } + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + _, err = client.EditOrgHook(org, id, opt) + if err != nil { + return to.ErrorResult(fmt.Errorf("edit org %s hook %d err: %v", org, id, err)) + } + hook, _, err := client.GetOrgHook(org, id) + if err != nil { + return to.ErrorResult(fmt.Errorf("get org %s hook %d after edit err: %v", org, id, err)) + } + return to.TextResult(hook) +} + +func DeleteOrgHookFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called DeleteOrgHookFn") + org, ok := req.GetArguments()["org"].(string) + if !ok { + return to.ErrorResult(errors.New("org is required")) + } + id, err := params.GetIndex(req.GetArguments(), "id") + if err != nil { + return to.ErrorResult(err) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + _, err = client.DeleteOrgHook(org, id) + if err != nil { + return to.ErrorResult(fmt.Errorf("delete org %s hook %d err: %v", org, id, err)) + } + return to.TextResult(map[string]any{"status": "deleted", "org": org, "hook_id": id}) +} diff --git a/operation/organization/members.go b/operation/organization/members.go new file mode 100644 index 0000000..c38d44f --- /dev/null +++ b/operation/organization/members.go @@ -0,0 +1,241 @@ +package organization + +import ( + "context" + "errors" + "fmt" + + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/gitea" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/log" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/params" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/to" + + gitea_sdk "code.gitea.io/sdk/gitea" + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" +) + +const ( + ListOrgMembersToolName = "list_org_members" + CheckOrgMembershipToolName = "check_org_membership" + RemoveOrgMemberToolName = "remove_org_member" + ListOrgPublicMembersToolName = "list_org_public_members" + CheckOrgPublicMemberToolName = "check_org_public_member" + SetOrgPublicMemberToolName = "set_org_public_member" + GetOrgPermissionsToolName = "get_org_permissions" +) + +var ( + ListOrgMembersTool = mcp.NewTool( + ListOrgMembersToolName, + mcp.WithDescription("List an organization's members"), + mcp.WithString("org", mcp.Required(), mcp.Description("organization name")), + mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)), + mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)), + ) + + CheckOrgMembershipTool = mcp.NewTool( + CheckOrgMembershipToolName, + mcp.WithDescription("Check if a user is a member of an organization"), + mcp.WithString("org", mcp.Required(), mcp.Description("organization name")), + mcp.WithString("username", mcp.Required(), mcp.Description("username to check")), + ) + + RemoveOrgMemberTool = mcp.NewTool( + RemoveOrgMemberToolName, + mcp.WithDescription("Remove a member from an organization"), + mcp.WithString("org", mcp.Required(), mcp.Description("organization name")), + mcp.WithString("username", mcp.Required(), mcp.Description("username to remove")), + ) + + ListOrgPublicMembersTool = mcp.NewTool( + ListOrgPublicMembersToolName, + mcp.WithDescription("List an organization's public members"), + mcp.WithString("org", mcp.Required(), mcp.Description("organization name")), + mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)), + mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)), + ) + + CheckOrgPublicMemberTool = mcp.NewTool( + CheckOrgPublicMemberToolName, + mcp.WithDescription("Check if a user is a public member of an organization"), + mcp.WithString("org", mcp.Required(), mcp.Description("organization name")), + mcp.WithString("username", mcp.Required(), mcp.Description("username to check")), + ) + + SetOrgPublicMemberTool = mcp.NewTool( + SetOrgPublicMemberToolName, + mcp.WithDescription("Set or remove a user's public membership in an organization"), + mcp.WithString("org", mcp.Required(), mcp.Description("organization name")), + mcp.WithString("username", mcp.Required(), mcp.Description("username")), + mcp.WithBoolean("visible", mcp.Required(), mcp.Description("true to make public, false to make private")), + ) + + GetOrgPermissionsTool = mcp.NewTool( + GetOrgPermissionsToolName, + mcp.WithDescription("Get a user's permissions in an organization"), + mcp.WithString("org", mcp.Required(), mcp.Description("organization name")), + mcp.WithString("username", mcp.Required(), mcp.Description("username")), + ) +) + +func init() { + Tool.RegisterRead(server.ServerTool{Tool: ListOrgMembersTool, Handler: ListOrgMembersFn}) + Tool.RegisterRead(server.ServerTool{Tool: CheckOrgMembershipTool, Handler: CheckOrgMembershipFn}) + Tool.RegisterRead(server.ServerTool{Tool: ListOrgPublicMembersTool, Handler: ListOrgPublicMembersFn}) + Tool.RegisterRead(server.ServerTool{Tool: CheckOrgPublicMemberTool, Handler: CheckOrgPublicMemberFn}) + Tool.RegisterRead(server.ServerTool{Tool: GetOrgPermissionsTool, Handler: GetOrgPermissionsFn}) + Tool.RegisterWrite(server.ServerTool{Tool: RemoveOrgMemberTool, Handler: RemoveOrgMemberFn}) + Tool.RegisterWrite(server.ServerTool{Tool: SetOrgPublicMemberTool, Handler: SetOrgPublicMemberFn}) +} + +func ListOrgMembersFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called ListOrgMembersFn") + org, ok := req.GetArguments()["org"].(string) + if !ok { + return to.ErrorResult(errors.New("org is required")) + } + page := params.GetOptionalInt(req.GetArguments(), "page", 1) + pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100) + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + members, _, err := client.ListOrgMembership(org, gitea_sdk.ListOrgMembershipOption{ + ListOptions: gitea_sdk.ListOptions{Page: int(page), PageSize: int(pageSize)}, + }) + if err != nil { + return to.ErrorResult(fmt.Errorf("list org %s members err: %v", org, err)) + } + return to.TextResult(members) +} + +func CheckOrgMembershipFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called CheckOrgMembershipFn") + org, ok := req.GetArguments()["org"].(string) + if !ok { + return to.ErrorResult(errors.New("org is required")) + } + username, ok := req.GetArguments()["username"].(string) + if !ok { + return to.ErrorResult(errors.New("username is required")) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + isMember, _, err := client.CheckOrgMembership(org, username) + if err != nil { + return to.ErrorResult(fmt.Errorf("check org %s membership for %s err: %v", org, username, err)) + } + return to.TextResult(map[string]any{"org": org, "username": username, "is_member": isMember}) +} + +func RemoveOrgMemberFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called RemoveOrgMemberFn") + org, ok := req.GetArguments()["org"].(string) + if !ok { + return to.ErrorResult(errors.New("org is required")) + } + username, ok := req.GetArguments()["username"].(string) + if !ok { + return to.ErrorResult(errors.New("username is required")) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + _, err = client.DeleteOrgMembership(org, username) + if err != nil { + return to.ErrorResult(fmt.Errorf("remove member %s from org %s err: %v", username, org, err)) + } + return to.TextResult(map[string]string{"status": "removed", "org": org, "username": username}) +} + +func ListOrgPublicMembersFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called ListOrgPublicMembersFn") + org, ok := req.GetArguments()["org"].(string) + if !ok { + return to.ErrorResult(errors.New("org is required")) + } + page := params.GetOptionalInt(req.GetArguments(), "page", 1) + pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100) + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + members, _, err := client.ListPublicOrgMembership(org, gitea_sdk.ListOrgMembershipOption{ + ListOptions: gitea_sdk.ListOptions{Page: int(page), PageSize: int(pageSize)}, + }) + if err != nil { + return to.ErrorResult(fmt.Errorf("list org %s public members err: %v", org, err)) + } + return to.TextResult(members) +} + +func CheckOrgPublicMemberFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called CheckOrgPublicMemberFn") + org, ok := req.GetArguments()["org"].(string) + if !ok { + return to.ErrorResult(errors.New("org is required")) + } + username, ok := req.GetArguments()["username"].(string) + if !ok { + return to.ErrorResult(errors.New("username is required")) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + isPublic, _, err := client.CheckPublicOrgMembership(org, username) + if err != nil { + return to.ErrorResult(fmt.Errorf("check org %s public membership for %s err: %v", org, username, err)) + } + return to.TextResult(map[string]any{"org": org, "username": username, "is_public_member": isPublic}) +} + +func SetOrgPublicMemberFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called SetOrgPublicMemberFn") + org, ok := req.GetArguments()["org"].(string) + if !ok { + return to.ErrorResult(errors.New("org is required")) + } + username, ok := req.GetArguments()["username"].(string) + if !ok { + return to.ErrorResult(errors.New("username is required")) + } + visible, ok := req.GetArguments()["visible"].(bool) + if !ok { + return to.ErrorResult(errors.New("visible is required")) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + _, err = client.SetPublicOrgMembership(org, username, visible) + if err != nil { + return to.ErrorResult(fmt.Errorf("set public membership for %s in org %s err: %v", username, org, err)) + } + return to.TextResult(map[string]any{"org": org, "username": username, "visible": visible}) +} + +func GetOrgPermissionsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called GetOrgPermissionsFn") + org, ok := req.GetArguments()["org"].(string) + if !ok { + return to.ErrorResult(errors.New("org is required")) + } + username, ok := req.GetArguments()["username"].(string) + if !ok { + return to.ErrorResult(errors.New("username is required")) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + perms, _, err := client.GetOrgPermissions(org, username) + if err != nil { + return to.ErrorResult(fmt.Errorf("get permissions for %s in org %s err: %v", username, org, err)) + } + return to.TextResult(perms) +} diff --git a/operation/organization/org.go b/operation/organization/org.go new file mode 100644 index 0000000..9d19442 --- /dev/null +++ b/operation/organization/org.go @@ -0,0 +1,319 @@ +package organization + +import ( + "context" + "errors" + "fmt" + + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/gitea" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/log" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/params" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/to" + "git.lethalbits.com/lethalbits/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 ( + ListOrgsToolName = "list_orgs" + GetOrgToolName = "get_org" + CreateOrgToolName = "create_org" + EditOrgToolName = "edit_org" + DeleteOrgToolName = "delete_org" + RenameOrgToolName = "rename_org" + ListOrgReposToolName = "list_org_repos" + CreateOrgRepoToolName = "create_org_repo" +) + +var ( + ListOrgsTool = mcp.NewTool( + ListOrgsToolName, + mcp.WithDescription("List all visible organizations"), + mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)), + mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)), + ) + + GetOrgTool = mcp.NewTool( + GetOrgToolName, + mcp.WithDescription("Get an organization by name"), + mcp.WithString("org", mcp.Required(), mcp.Description("organization name")), + ) + + CreateOrgTool = mcp.NewTool( + CreateOrgToolName, + mcp.WithDescription("Create an organization"), + mcp.WithString("username", mcp.Required(), mcp.Description("organization username")), + mcp.WithString("full_name", mcp.Description("organization full name")), + mcp.WithString("description", mcp.Description("organization description")), + mcp.WithString("website", mcp.Description("organization website")), + mcp.WithString("location", mcp.Description("organization location")), + mcp.WithString("visibility", mcp.Description("visibility: public, limited, or private"), mcp.DefaultString("public")), + ) + + EditOrgTool = mcp.NewTool( + EditOrgToolName, + mcp.WithDescription("Edit an organization"), + mcp.WithString("org", mcp.Required(), mcp.Description("organization name")), + mcp.WithString("full_name", mcp.Description("organization full name")), + mcp.WithString("description", mcp.Description("organization description")), + mcp.WithString("website", mcp.Description("organization website")), + mcp.WithString("location", mcp.Description("organization location")), + mcp.WithString("visibility", mcp.Description("visibility: public, limited, or private")), + ) + + DeleteOrgTool = mcp.NewTool( + DeleteOrgToolName, + mcp.WithDescription("Delete an organization"), + mcp.WithString("org", mcp.Required(), mcp.Description("organization name")), + ) + + RenameOrgTool = mcp.NewTool( + RenameOrgToolName, + mcp.WithDescription("Rename an organization"), + mcp.WithString("org", mcp.Required(), mcp.Description("current organization name")), + mcp.WithString("new_name", mcp.Required(), mcp.Description("new organization name")), + ) + + ListOrgReposTool = mcp.NewTool( + ListOrgReposToolName, + mcp.WithDescription("List an organization's repositories"), + mcp.WithString("org", mcp.Required(), mcp.Description("organization name")), + mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)), + mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)), + ) + + CreateOrgRepoTool = mcp.NewTool( + CreateOrgRepoToolName, + mcp.WithDescription("Create a repository in an organization"), + mcp.WithString("org", mcp.Required(), mcp.Description("organization name")), + mcp.WithString("name", mcp.Required(), mcp.Description("repository name")), + mcp.WithString("description", mcp.Description("repository description")), + mcp.WithBoolean("private", mcp.Description("whether the repository is private")), + mcp.WithBoolean("auto_init", mcp.Description("whether to auto-initialize with README")), + mcp.WithString("default_branch", mcp.Description("default branch name")), + mcp.WithString("gitignores", mcp.Description("gitignore template")), + mcp.WithString("license", mcp.Description("license template")), + ) +) + +func init() { + Tool.RegisterRead(server.ServerTool{Tool: ListOrgsTool, Handler: ListOrgsFn}) + Tool.RegisterRead(server.ServerTool{Tool: GetOrgTool, Handler: GetOrgFn}) + Tool.RegisterRead(server.ServerTool{Tool: ListOrgReposTool, Handler: ListOrgReposFn}) + Tool.RegisterWrite(server.ServerTool{Tool: CreateOrgTool, Handler: CreateOrgFn}) + Tool.RegisterWrite(server.ServerTool{Tool: EditOrgTool, Handler: EditOrgFn}) + Tool.RegisterWrite(server.ServerTool{Tool: DeleteOrgTool, Handler: DeleteOrgFn}) + Tool.RegisterWrite(server.ServerTool{Tool: RenameOrgTool, Handler: RenameOrgFn}) + Tool.RegisterWrite(server.ServerTool{Tool: CreateOrgRepoTool, Handler: CreateOrgRepoFn}) +} + +func ListOrgsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called ListOrgsFn") + page := params.GetOptionalInt(req.GetArguments(), "page", 1) + pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100) + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + orgs, _, err := client.ListOrgs(gitea_sdk.ListOrgsOptions{ + ListOptions: gitea_sdk.ListOptions{Page: int(page), PageSize: int(pageSize)}, + }) + if err != nil { + return to.ErrorResult(fmt.Errorf("list orgs err: %v", err)) + } + return to.TextResult(orgs) +} + +func GetOrgFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called GetOrgFn") + org, ok := req.GetArguments()["org"].(string) + if !ok { + return to.ErrorResult(errors.New("org is required")) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + organization, _, err := client.GetOrg(org) + if err != nil { + return to.ErrorResult(fmt.Errorf("get org %s err: %v", org, err)) + } + return to.TextResult(organization) +} + +func CreateOrgFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called CreateOrgFn") + username, ok := req.GetArguments()["username"].(string) + if !ok { + return to.ErrorResult(errors.New("username is required")) + } + opt := gitea_sdk.CreateOrgOption{ + Name: username, + } + if v, ok := req.GetArguments()["full_name"].(string); ok { + opt.FullName = v + } + if v, ok := req.GetArguments()["description"].(string); ok { + opt.Description = v + } + if v, ok := req.GetArguments()["website"].(string); ok { + opt.Website = v + } + if v, ok := req.GetArguments()["location"].(string); ok { + opt.Location = v + } + if v, ok := req.GetArguments()["visibility"].(string); ok { + opt.Visibility = gitea_sdk.VisibleType(v) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + organization, _, err := client.CreateOrg(opt) + if err != nil { + return to.ErrorResult(fmt.Errorf("create org %s err: %v", username, err)) + } + return to.TextResult(organization) +} + +func EditOrgFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called EditOrgFn") + org, ok := req.GetArguments()["org"].(string) + if !ok { + return to.ErrorResult(errors.New("org is required")) + } + opt := gitea_sdk.EditOrgOption{} + if v, ok := req.GetArguments()["full_name"].(string); ok { + opt.FullName = v + } + if v, ok := req.GetArguments()["description"].(string); ok { + opt.Description = v + } + if v, ok := req.GetArguments()["website"].(string); ok { + opt.Website = v + } + if v, ok := req.GetArguments()["location"].(string); ok { + opt.Location = v + } + if v, ok := req.GetArguments()["visibility"].(string); ok { + opt.Visibility = gitea_sdk.VisibleType(v) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + _, err = client.EditOrg(org, opt) + if err != nil { + return to.ErrorResult(fmt.Errorf("edit org %s err: %v", org, err)) + } + updated, _, err := client.GetOrg(org) + if err != nil { + return to.ErrorResult(fmt.Errorf("get org %s after edit err: %v", org, err)) + } + return to.TextResult(updated) +} + +func DeleteOrgFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called DeleteOrgFn") + org, ok := req.GetArguments()["org"].(string) + if !ok { + return to.ErrorResult(errors.New("org is required")) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + _, err = client.DeleteOrg(org) + if err != nil { + return to.ErrorResult(fmt.Errorf("delete org %s err: %v", org, err)) + } + return to.TextResult(map[string]string{"status": "deleted", "org": org}) +} + +func RenameOrgFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called RenameOrgFn") + org, ok := req.GetArguments()["org"].(string) + if !ok { + return to.ErrorResult(errors.New("org is required")) + } + newName, ok := req.GetArguments()["new_name"].(string) + if !ok { + return to.ErrorResult(errors.New("new_name is required")) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + _, err = client.RenameOrg(org, gitea_sdk.RenameOrgOption{NewName: newName}) + if err != nil { + return to.ErrorResult(fmt.Errorf("rename org %s to %s err: %v", org, newName, err)) + } + return to.TextResult(map[string]string{"status": "renamed", "old_name": org, "new_name": newName}) +} + +func ListOrgReposFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called ListOrgReposFn") + org, ok := req.GetArguments()["org"].(string) + if !ok { + return to.ErrorResult(errors.New("org is required")) + } + page := params.GetOptionalInt(req.GetArguments(), "page", 1) + pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100) + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + repos, _, err := client.ListOrgRepos(org, gitea_sdk.ListOrgReposOptions{ + ListOptions: gitea_sdk.ListOptions{Page: int(page), PageSize: int(pageSize)}, + }) + if err != nil { + return to.ErrorResult(fmt.Errorf("list org %s repos err: %v", org, err)) + } + return to.TextResult(repos) +} + +func CreateOrgRepoFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called CreateOrgRepoFn") + org, ok := req.GetArguments()["org"].(string) + if !ok { + return to.ErrorResult(errors.New("org is required")) + } + name, ok := req.GetArguments()["name"].(string) + if !ok { + return to.ErrorResult(errors.New("name is required")) + } + opt := gitea_sdk.CreateRepoOption{ + Name: name, + } + if v, ok := req.GetArguments()["description"].(string); ok { + opt.Description = v + } + if v, ok := req.GetArguments()["private"].(bool); ok { + opt.Private = v + } + if v, ok := req.GetArguments()["auto_init"].(bool); ok { + opt.AutoInit = v + } + if v, ok := req.GetArguments()["default_branch"].(string); ok { + opt.DefaultBranch = v + } + if v, ok := req.GetArguments()["gitignores"].(string); ok { + opt.Gitignores = v + } + if v, ok := req.GetArguments()["license"].(string); ok { + opt.License = v + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + repo, _, err := client.CreateOrgRepo(org, opt) + if err != nil { + return to.ErrorResult(fmt.Errorf("create repo %s/%s err: %v", org, name, err)) + } + return to.TextResult(repo) +} diff --git a/operation/organization/teams.go b/operation/organization/teams.go new file mode 100644 index 0000000..a352b4d --- /dev/null +++ b/operation/organization/teams.go @@ -0,0 +1,427 @@ +package organization + +import ( + "context" + "errors" + "fmt" + + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/gitea" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/log" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/params" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/to" + + gitea_sdk "code.gitea.io/sdk/gitea" + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" +) + +const ( + ListOrgTeamsToolName = "list_org_teams" + GetTeamToolName = "get_team" + CreateTeamToolName = "create_team" + EditTeamToolName = "edit_team" + DeleteTeamToolName = "delete_team" + ListTeamMembersToolName = "list_team_members" + GetTeamMemberToolName = "get_team_member" + AddTeamMemberToolName = "add_team_member" + RemoveTeamMemberToolName = "remove_team_member" + ListTeamReposToolName = "list_team_repos" + AddTeamRepoToolName = "add_team_repo" + RemoveTeamRepoToolName = "remove_team_repo" +) + +var ( + ListOrgTeamsTool = mcp.NewTool( + ListOrgTeamsToolName, + mcp.WithDescription("List an organization's teams"), + mcp.WithString("org", mcp.Required(), mcp.Description("organization name")), + mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)), + mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)), + ) + + GetTeamTool = mcp.NewTool( + GetTeamToolName, + mcp.WithDescription("Get a team by ID"), + mcp.WithNumber("id", mcp.Required(), mcp.Description("team ID")), + ) + + CreateTeamTool = mcp.NewTool( + CreateTeamToolName, + mcp.WithDescription("Create a team in an organization"), + mcp.WithString("org", mcp.Required(), mcp.Description("organization name")), + mcp.WithString("name", mcp.Required(), mcp.Description("team name")), + mcp.WithString("description", mcp.Description("team description")), + mcp.WithString("permission", mcp.Description("team permission: read, write, admin"), mcp.DefaultString("read")), + mcp.WithBoolean("can_create_org_repo", mcp.Description("whether team members can create repos in the org")), + mcp.WithBoolean("includes_all_repositories", mcp.Description("whether team has access to all repos")), + ) + + EditTeamTool = mcp.NewTool( + EditTeamToolName, + mcp.WithDescription("Edit a team"), + mcp.WithNumber("id", mcp.Required(), mcp.Description("team ID")), + mcp.WithString("name", mcp.Description("team name")), + mcp.WithString("description", mcp.Description("team description")), + mcp.WithString("permission", mcp.Description("team permission: read, write, admin")), + mcp.WithBoolean("can_create_org_repo", mcp.Description("whether team members can create repos in the org")), + mcp.WithBoolean("includes_all_repositories", mcp.Description("whether team has access to all repos")), + ) + + DeleteTeamTool = mcp.NewTool( + DeleteTeamToolName, + mcp.WithDescription("Delete a team"), + mcp.WithNumber("id", mcp.Required(), mcp.Description("team ID")), + ) + + ListTeamMembersTool = mcp.NewTool( + ListTeamMembersToolName, + mcp.WithDescription("List a team's members"), + mcp.WithNumber("id", mcp.Required(), mcp.Description("team ID")), + mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)), + mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)), + ) + + GetTeamMemberTool = mcp.NewTool( + GetTeamMemberToolName, + mcp.WithDescription("Get a specific member of a team"), + mcp.WithNumber("id", mcp.Required(), mcp.Description("team ID")), + mcp.WithString("username", mcp.Required(), mcp.Description("username")), + ) + + AddTeamMemberTool = mcp.NewTool( + AddTeamMemberToolName, + mcp.WithDescription("Add a member to a team"), + mcp.WithNumber("id", mcp.Required(), mcp.Description("team ID")), + mcp.WithString("username", mcp.Required(), mcp.Description("username to add")), + ) + + RemoveTeamMemberTool = mcp.NewTool( + RemoveTeamMemberToolName, + mcp.WithDescription("Remove a member from a team"), + mcp.WithNumber("id", mcp.Required(), mcp.Description("team ID")), + mcp.WithString("username", mcp.Required(), mcp.Description("username to remove")), + ) + + ListTeamReposTool = mcp.NewTool( + ListTeamReposToolName, + mcp.WithDescription("List a team's repositories"), + mcp.WithNumber("id", mcp.Required(), mcp.Description("team ID")), + mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)), + mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)), + ) + + AddTeamRepoTool = mcp.NewTool( + AddTeamRepoToolName, + mcp.WithDescription("Add a repository to a team"), + mcp.WithNumber("id", mcp.Required(), mcp.Description("team ID")), + mcp.WithString("org", mcp.Required(), mcp.Description("organization name")), + mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), + ) + + RemoveTeamRepoTool = mcp.NewTool( + RemoveTeamRepoToolName, + mcp.WithDescription("Remove a repository from a team"), + mcp.WithNumber("id", mcp.Required(), mcp.Description("team ID")), + mcp.WithString("org", mcp.Required(), mcp.Description("organization name")), + mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), + ) +) + +func init() { + Tool.RegisterRead(server.ServerTool{Tool: ListOrgTeamsTool, Handler: ListOrgTeamsFn}) + Tool.RegisterRead(server.ServerTool{Tool: GetTeamTool, Handler: GetTeamFn}) + Tool.RegisterRead(server.ServerTool{Tool: ListTeamMembersTool, Handler: ListTeamMembersFn}) + Tool.RegisterRead(server.ServerTool{Tool: GetTeamMemberTool, Handler: GetTeamMemberFn}) + Tool.RegisterRead(server.ServerTool{Tool: ListTeamReposTool, Handler: ListTeamReposFn}) + Tool.RegisterWrite(server.ServerTool{Tool: CreateTeamTool, Handler: CreateTeamFn}) + Tool.RegisterWrite(server.ServerTool{Tool: EditTeamTool, Handler: EditTeamFn}) + Tool.RegisterWrite(server.ServerTool{Tool: DeleteTeamTool, Handler: DeleteTeamFn}) + Tool.RegisterWrite(server.ServerTool{Tool: AddTeamMemberTool, Handler: AddTeamMemberFn}) + Tool.RegisterWrite(server.ServerTool{Tool: RemoveTeamMemberTool, Handler: RemoveTeamMemberFn}) + Tool.RegisterWrite(server.ServerTool{Tool: AddTeamRepoTool, Handler: AddTeamRepoFn}) + Tool.RegisterWrite(server.ServerTool{Tool: RemoveTeamRepoTool, Handler: RemoveTeamRepoFn}) +} + +func ListOrgTeamsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called ListOrgTeamsFn") + org, ok := req.GetArguments()["org"].(string) + if !ok { + return to.ErrorResult(errors.New("org is required")) + } + page := params.GetOptionalInt(req.GetArguments(), "page", 1) + pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100) + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + teams, _, err := client.ListOrgTeams(org, gitea_sdk.ListTeamsOptions{ + ListOptions: gitea_sdk.ListOptions{Page: int(page), PageSize: int(pageSize)}, + }) + if err != nil { + return to.ErrorResult(fmt.Errorf("list org %s teams err: %v", org, err)) + } + return to.TextResult(teams) +} + +func GetTeamFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called GetTeamFn") + 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)) + } + team, _, err := client.GetTeam(id) + if err != nil { + return to.ErrorResult(fmt.Errorf("get team %d err: %v", id, err)) + } + return to.TextResult(team) +} + +func CreateTeamFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called CreateTeamFn") + org, ok := req.GetArguments()["org"].(string) + if !ok { + return to.ErrorResult(errors.New("org is required")) + } + name, ok := req.GetArguments()["name"].(string) + if !ok { + return to.ErrorResult(errors.New("name is required")) + } + opt := gitea_sdk.CreateTeamOption{ + Name: name, + } + if v, ok := req.GetArguments()["description"].(string); ok { + opt.Description = v + } + if v, ok := req.GetArguments()["permission"].(string); ok { + opt.Permission = gitea_sdk.AccessMode(v) + } + if v, ok := req.GetArguments()["can_create_org_repo"].(bool); ok { + opt.CanCreateOrgRepo = v + } + if v, ok := req.GetArguments()["includes_all_repositories"].(bool); ok { + opt.IncludesAllRepositories = v + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + team, _, err := client.CreateTeam(org, opt) + if err != nil { + return to.ErrorResult(fmt.Errorf("create team %s in org %s err: %v", name, org, err)) + } + return to.TextResult(team) +} + +func EditTeamFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called EditTeamFn") + id, err := params.GetIndex(req.GetArguments(), "id") + if err != nil { + return to.ErrorResult(err) + } + opt := gitea_sdk.EditTeamOption{} + if v, ok := req.GetArguments()["name"].(string); ok { + opt.Name = v + } + if v, ok := req.GetArguments()["description"].(string); ok { + opt.Description = &v + } + if v, ok := req.GetArguments()["permission"].(string); ok { + perm := gitea_sdk.AccessMode(v) + opt.Permission = perm + } + if v, ok := req.GetArguments()["can_create_org_repo"].(bool); ok { + opt.CanCreateOrgRepo = &v + } + if v, ok := req.GetArguments()["includes_all_repositories"].(bool); ok { + opt.IncludesAllRepositories = &v + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + _, err = client.EditTeam(id, opt) + if err != nil { + return to.ErrorResult(fmt.Errorf("edit team %d err: %v", id, err)) + } + team, _, err := client.GetTeam(id) + if err != nil { + return to.ErrorResult(fmt.Errorf("get team %d after edit err: %v", id, err)) + } + return to.TextResult(team) +} + +func DeleteTeamFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called DeleteTeamFn") + id, err := params.GetIndex(req.GetArguments(), "id") + if err != nil { + return to.ErrorResult(err) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + _, err = client.DeleteTeam(id) + if err != nil { + return to.ErrorResult(fmt.Errorf("delete team %d err: %v", id, err)) + } + return to.TextResult(map[string]any{"status": "deleted", "team_id": id}) +} + +func ListTeamMembersFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called ListTeamMembersFn") + id, err := params.GetIndex(req.GetArguments(), "id") + if err != nil { + return to.ErrorResult(err) + } + page := params.GetOptionalInt(req.GetArguments(), "page", 1) + pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100) + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + members, _, err := client.ListTeamMembers(id, gitea_sdk.ListTeamMembersOptions{ + ListOptions: gitea_sdk.ListOptions{Page: int(page), PageSize: int(pageSize)}, + }) + if err != nil { + return to.ErrorResult(fmt.Errorf("list team %d members err: %v", id, err)) + } + return to.TextResult(members) +} + +func GetTeamMemberFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called GetTeamMemberFn") + id, err := params.GetIndex(req.GetArguments(), "id") + if err != nil { + return to.ErrorResult(err) + } + username, ok := req.GetArguments()["username"].(string) + if !ok { + return to.ErrorResult(errors.New("username is required")) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + member, _, err := client.GetTeamMember(id, username) + if err != nil { + return to.ErrorResult(fmt.Errorf("get team %d member %s err: %v", id, username, err)) + } + return to.TextResult(member) +} + +func AddTeamMemberFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called AddTeamMemberFn") + id, err := params.GetIndex(req.GetArguments(), "id") + if err != nil { + return to.ErrorResult(err) + } + username, ok := req.GetArguments()["username"].(string) + if !ok { + return to.ErrorResult(errors.New("username is required")) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + _, err = client.AddTeamMember(id, username) + if err != nil { + return to.ErrorResult(fmt.Errorf("add member %s to team %d err: %v", username, id, err)) + } + return to.TextResult(map[string]any{"status": "added", "team_id": id, "username": username}) +} + +func RemoveTeamMemberFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called RemoveTeamMemberFn") + id, err := params.GetIndex(req.GetArguments(), "id") + if err != nil { + return to.ErrorResult(err) + } + username, ok := req.GetArguments()["username"].(string) + if !ok { + return to.ErrorResult(errors.New("username is required")) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + _, err = client.RemoveTeamMember(id, username) + if err != nil { + return to.ErrorResult(fmt.Errorf("remove member %s from team %d err: %v", username, id, err)) + } + return to.TextResult(map[string]any{"status": "removed", "team_id": id, "username": username}) +} + +func ListTeamReposFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called ListTeamReposFn") + id, err := params.GetIndex(req.GetArguments(), "id") + if err != nil { + return to.ErrorResult(err) + } + page := params.GetOptionalInt(req.GetArguments(), "page", 1) + pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100) + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + repos, _, err := client.ListTeamRepositories(id, gitea_sdk.ListTeamRepositoriesOptions{ + ListOptions: gitea_sdk.ListOptions{Page: int(page), PageSize: int(pageSize)}, + }) + if err != nil { + return to.ErrorResult(fmt.Errorf("list team %d repos err: %v", id, err)) + } + return to.TextResult(repos) +} + +func AddTeamRepoFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called AddTeamRepoFn") + id, err := params.GetIndex(req.GetArguments(), "id") + if err != nil { + return to.ErrorResult(err) + } + org, ok := req.GetArguments()["org"].(string) + if !ok { + return to.ErrorResult(errors.New("org is required")) + } + repo, ok := req.GetArguments()["repo"].(string) + if !ok { + return to.ErrorResult(errors.New("repo is required")) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + _, err = client.AddTeamRepository(id, org, repo) + if err != nil { + return to.ErrorResult(fmt.Errorf("add repo %s/%s to team %d err: %v", org, repo, id, err)) + } + return to.TextResult(map[string]any{"status": "added", "team_id": id, "org": org, "repo": repo}) +} + +func RemoveTeamRepoFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called RemoveTeamRepoFn") + id, err := params.GetIndex(req.GetArguments(), "id") + if err != nil { + return to.ErrorResult(err) + } + org, ok := req.GetArguments()["org"].(string) + if !ok { + return to.ErrorResult(errors.New("org is required")) + } + repo, ok := req.GetArguments()["repo"].(string) + if !ok { + return to.ErrorResult(errors.New("repo is required")) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + _, err = client.RemoveTeamRepository(id, org, repo) + if err != nil { + return to.ErrorResult(fmt.Errorf("remove repo %s/%s from team %d err: %v", org, repo, id, err)) + } + return to.TextResult(map[string]any{"status": "removed", "team_id": id, "org": org, "repo": repo}) +} diff --git a/operation/packages/packages.go b/operation/packages/packages.go new file mode 100644 index 0000000..87b3795 --- /dev/null +++ b/operation/packages/packages.go @@ -0,0 +1,242 @@ +package packages + +import ( + "context" + "errors" + "fmt" + + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/gitea" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/log" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/params" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/to" + "git.lethalbits.com/lethalbits/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 ( + ListPackagesToolName = "list_packages" + GetPackageToolName = "get_package" + DeletePackageToolName = "delete_package" + ListPackageFilesToolName = "list_package_files" + GetLatestPackageToolName = "get_latest_package" + LinkPackageToolName = "link_package" + UnlinkPackageToolName = "unlink_package" +) + +var ( + ListPackagesTool = mcp.NewTool( + ListPackagesToolName, + mcp.WithDescription("List packages for a user or organization"), + mcp.WithString("owner", mcp.Required(), mcp.Description("package owner (user or org)")), + mcp.WithString("type", mcp.Description("package type filter (e.g., generic, container, npm, pypi, rubygems, maven, nuget, debian, alpine, go, cargo, chef, composer, conan, conda, cran, pub, helm, rpm, swift, vagrant)")), + mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)), + mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)), + ) + + GetPackageTool = mcp.NewTool( + GetPackageToolName, + mcp.WithDescription("Get a specific package by type, name, and version"), + mcp.WithString("owner", mcp.Required(), mcp.Description("package owner")), + mcp.WithString("type", mcp.Required(), mcp.Description("package type (e.g., generic, container, npm)")), + mcp.WithString("name", mcp.Required(), mcp.Description("package name")), + mcp.WithString("version", mcp.Required(), mcp.Description("package version")), + ) + + DeletePackageTool = mcp.NewTool( + DeletePackageToolName, + mcp.WithDescription("Delete a specific package version"), + mcp.WithString("owner", mcp.Required(), mcp.Description("package owner")), + mcp.WithString("type", mcp.Required(), mcp.Description("package type")), + mcp.WithString("name", mcp.Required(), mcp.Description("package name")), + mcp.WithString("version", mcp.Required(), mcp.Description("package version")), + ) + + ListPackageFilesTool = mcp.NewTool( + ListPackageFilesToolName, + mcp.WithDescription("List files of a package version"), + mcp.WithString("owner", mcp.Required(), mcp.Description("package owner")), + mcp.WithString("type", mcp.Required(), mcp.Description("package type")), + mcp.WithString("name", mcp.Required(), mcp.Description("package name")), + mcp.WithString("version", mcp.Required(), mcp.Description("package version")), + ) + + GetLatestPackageTool = mcp.NewTool( + GetLatestPackageToolName, + mcp.WithDescription("Get the latest version of a package"), + mcp.WithString("owner", mcp.Required(), mcp.Description("package owner")), + mcp.WithString("type", mcp.Required(), mcp.Description("package type")), + mcp.WithString("name", mcp.Required(), mcp.Description("package name")), + ) + + LinkPackageTool = mcp.NewTool( + LinkPackageToolName, + mcp.WithDescription("Link a package to a repository"), + mcp.WithString("owner", mcp.Required(), mcp.Description("package owner")), + mcp.WithString("type", mcp.Required(), mcp.Description("package type")), + mcp.WithString("name", mcp.Required(), mcp.Description("package name")), + mcp.WithString("repo_name", mcp.Required(), mcp.Description("repository name to link to")), + ) + + UnlinkPackageTool = mcp.NewTool( + UnlinkPackageToolName, + mcp.WithDescription("Unlink a package from its repository"), + mcp.WithString("owner", mcp.Required(), mcp.Description("package owner")), + mcp.WithString("type", mcp.Required(), mcp.Description("package type")), + mcp.WithString("name", mcp.Required(), mcp.Description("package name")), + ) +) + +func init() { + Tool.RegisterRead(server.ServerTool{Tool: ListPackagesTool, Handler: ListPackagesFn}) + Tool.RegisterRead(server.ServerTool{Tool: GetPackageTool, Handler: GetPackageFn}) + Tool.RegisterRead(server.ServerTool{Tool: ListPackageFilesTool, Handler: ListPackageFilesFn}) + Tool.RegisterRead(server.ServerTool{Tool: GetLatestPackageTool, Handler: GetLatestPackageFn}) + Tool.RegisterWrite(server.ServerTool{Tool: DeletePackageTool, Handler: DeletePackageFn}) + Tool.RegisterWrite(server.ServerTool{Tool: LinkPackageTool, Handler: LinkPackageFn}) + Tool.RegisterWrite(server.ServerTool{Tool: UnlinkPackageTool, Handler: UnlinkPackageFn}) +} + +func ListPackagesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called ListPackagesFn") + owner, _ := req.GetArguments()["owner"].(string) + if owner == "" { + return to.ErrorResult(errors.New("owner is required")) + } + page := params.GetOptionalInt(req.GetArguments(), "page", 1) + pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100) + opt := gitea_sdk.ListPackagesOptions{ + ListOptions: gitea_sdk.ListOptions{Page: int(page), PageSize: int(pageSize)}, + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + packages, _, err := client.ListPackages(owner, opt) + if err != nil { + return to.ErrorResult(fmt.Errorf("list packages err: %v", err)) + } + return to.TextResult(packages) +} + +func GetPackageFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called GetPackageFn") + owner, _ := req.GetArguments()["owner"].(string) + pkgType, _ := req.GetArguments()["type"].(string) + name, _ := req.GetArguments()["name"].(string) + version, _ := req.GetArguments()["version"].(string) + if owner == "" || pkgType == "" || name == "" || version == "" { + return to.ErrorResult(errors.New("owner, type, name, and version are required")) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + pkg, _, err := client.GetPackage(owner, pkgType, name, version) + if err != nil { + return to.ErrorResult(fmt.Errorf("get package err: %v", err)) + } + return to.TextResult(pkg) +} + +func DeletePackageFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called DeletePackageFn") + owner, _ := req.GetArguments()["owner"].(string) + pkgType, _ := req.GetArguments()["type"].(string) + name, _ := req.GetArguments()["name"].(string) + version, _ := req.GetArguments()["version"].(string) + if owner == "" || pkgType == "" || name == "" || version == "" { + return to.ErrorResult(errors.New("owner, type, name, and version are required")) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + _, err = client.DeletePackage(owner, pkgType, name, version) + if err != nil { + return to.ErrorResult(fmt.Errorf("delete package err: %v", err)) + } + return to.TextResult(map[string]string{"status": "deleted"}) +} + +func ListPackageFilesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called ListPackageFilesFn") + owner, _ := req.GetArguments()["owner"].(string) + pkgType, _ := req.GetArguments()["type"].(string) + name, _ := req.GetArguments()["name"].(string) + version, _ := req.GetArguments()["version"].(string) + if owner == "" || pkgType == "" || name == "" || version == "" { + return to.ErrorResult(errors.New("owner, type, name, and version are required")) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + files, _, err := client.ListPackageFiles(owner, pkgType, name, version) + if err != nil { + return to.ErrorResult(fmt.Errorf("list package files err: %v", err)) + } + return to.TextResult(files) +} + +func GetLatestPackageFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called GetLatestPackageFn") + owner, _ := req.GetArguments()["owner"].(string) + pkgType, _ := req.GetArguments()["type"].(string) + name, _ := req.GetArguments()["name"].(string) + if owner == "" || pkgType == "" || name == "" { + return to.ErrorResult(errors.New("owner, type, and name are required")) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + pkg, _, err := client.GetLatestPackage(owner, pkgType, name) + if err != nil { + return to.ErrorResult(fmt.Errorf("get latest package err: %v", err)) + } + return to.TextResult(pkg) +} + +func LinkPackageFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called LinkPackageFn") + owner, _ := req.GetArguments()["owner"].(string) + pkgType, _ := req.GetArguments()["type"].(string) + name, _ := req.GetArguments()["name"].(string) + repoName, _ := req.GetArguments()["repo_name"].(string) + if owner == "" || pkgType == "" || name == "" || repoName == "" { + return to.ErrorResult(errors.New("owner, type, name, and repo_name are required")) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + _, err = client.LinkPackage(owner, pkgType, name, repoName) + if err != nil { + return to.ErrorResult(fmt.Errorf("link package err: %v", err)) + } + return to.TextResult(map[string]string{"status": "linked", "repo": repoName}) +} + +func UnlinkPackageFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called UnlinkPackageFn") + owner, _ := req.GetArguments()["owner"].(string) + pkgType, _ := req.GetArguments()["type"].(string) + name, _ := req.GetArguments()["name"].(string) + if owner == "" || pkgType == "" || name == "" { + return to.ErrorResult(errors.New("owner, type, and name are required")) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + _, err = client.UnlinkPackage(owner, pkgType, name) + if err != nil { + return to.ErrorResult(fmt.Errorf("unlink package err: %v", err)) + } + return to.TextResult(map[string]string{"status": "unlinked"}) +} diff --git a/operation/pull/pull.go b/operation/pull/pull.go index d55df30..64bb842 100644 --- a/operation/pull/pull.go +++ b/operation/pull/pull.go @@ -5,11 +5,11 @@ import ( "errors" "fmt" - "gitea.com/gitea/gitea-mcp/pkg/gitea" - "gitea.com/gitea/gitea-mcp/pkg/log" - "gitea.com/gitea/gitea-mcp/pkg/params" - "gitea.com/gitea/gitea-mcp/pkg/to" - "gitea.com/gitea/gitea-mcp/pkg/tool" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/gitea" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/log" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/params" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/to" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/tool" gitea_sdk "code.gitea.io/sdk/gitea" "github.com/mark3labs/mcp-go/mcp" diff --git a/operation/pull/pull_test.go b/operation/pull/pull_test.go index a6cdcdc..f181906 100644 --- a/operation/pull/pull_test.go +++ b/operation/pull/pull_test.go @@ -9,7 +9,7 @@ import ( "sync" "testing" - "gitea.com/gitea/gitea-mcp/pkg/flag" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/flag" "github.com/mark3labs/mcp-go/mcp" ) diff --git a/operation/repo/branch.go b/operation/repo/branch.go index e4dac5b..1e76cdc 100644 --- a/operation/repo/branch.go +++ b/operation/repo/branch.go @@ -5,9 +5,9 @@ import ( "errors" "fmt" - "gitea.com/gitea/gitea-mcp/pkg/gitea" - "gitea.com/gitea/gitea-mcp/pkg/log" - "gitea.com/gitea/gitea-mcp/pkg/to" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/gitea" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/log" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/to" gitea_sdk "code.gitea.io/sdk/gitea" "github.com/mark3labs/mcp-go/mcp" diff --git a/operation/repo/branch_protection.go b/operation/repo/branch_protection.go new file mode 100644 index 0000000..8e96c81 --- /dev/null +++ b/operation/repo/branch_protection.go @@ -0,0 +1,226 @@ +package repo + +import ( + "context" + "errors" + "fmt" + + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/gitea" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/log" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/params" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/to" + + gitea_sdk "code.gitea.io/sdk/gitea" + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" +) + +const ( + ListBranchProtectionsToolName = "list_branch_protections" + GetBranchProtectionToolName = "get_branch_protection" + CreateBranchProtectionToolName = "create_branch_protection" + EditBranchProtectionToolName = "edit_branch_protection" + DeleteBranchProtectionToolName = "delete_branch_protection" +) + +var ( + ListBranchProtectionsTool = mcp.NewTool( + ListBranchProtectionsToolName, + mcp.WithDescription("List branch protections for a repository"), + mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), + mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), + mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)), + mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)), + ) + + GetBranchProtectionTool = mcp.NewTool( + GetBranchProtectionToolName, + mcp.WithDescription("Get a branch protection rule by name"), + mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), + mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), + mcp.WithString("name", mcp.Required(), mcp.Description("branch protection rule name")), + ) + + CreateBranchProtectionTool = mcp.NewTool( + CreateBranchProtectionToolName, + mcp.WithDescription("Create a branch protection rule"), + mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), + mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), + mcp.WithString("rule_name", mcp.Required(), mcp.Description("rule name (glob pattern for branch matching)")), + mcp.WithBoolean("enable_push", mcp.Description("enable push to protected branch")), + mcp.WithBoolean("enable_merge_whitelist", mcp.Description("enable merge whitelist")), + mcp.WithBoolean("enable_status_check", mcp.Description("enable status check")), + mcp.WithNumber("required_approvals", mcp.Description("number of required approvals")), + mcp.WithBoolean("block_on_rejected_reviews", mcp.Description("block merge on rejected reviews")), + mcp.WithBoolean("dismiss_stale_approvals", mcp.Description("dismiss stale approvals on new commits")), + ) + + EditBranchProtectionTool = mcp.NewTool( + EditBranchProtectionToolName, + mcp.WithDescription("Edit a branch protection rule"), + mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), + mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), + mcp.WithString("name", mcp.Required(), mcp.Description("branch protection rule name")), + mcp.WithBoolean("enable_push", mcp.Description("enable push to protected branch")), + mcp.WithBoolean("enable_merge_whitelist", mcp.Description("enable merge whitelist")), + mcp.WithBoolean("enable_status_check", mcp.Description("enable status check")), + mcp.WithNumber("required_approvals", mcp.Description("number of required approvals")), + mcp.WithBoolean("block_on_rejected_reviews", mcp.Description("block merge on rejected reviews")), + mcp.WithBoolean("dismiss_stale_approvals", mcp.Description("dismiss stale approvals on new commits")), + ) + + DeleteBranchProtectionTool = mcp.NewTool( + DeleteBranchProtectionToolName, + mcp.WithDescription("Delete a branch protection rule"), + mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), + mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), + mcp.WithString("name", mcp.Required(), mcp.Description("branch protection rule name")), + ) +) + +func init() { + Tool.RegisterRead(server.ServerTool{Tool: ListBranchProtectionsTool, Handler: ListBranchProtectionsFn}) + Tool.RegisterRead(server.ServerTool{Tool: GetBranchProtectionTool, Handler: GetBranchProtectionFn}) + Tool.RegisterWrite(server.ServerTool{Tool: CreateBranchProtectionTool, Handler: CreateBranchProtectionFn}) + Tool.RegisterWrite(server.ServerTool{Tool: EditBranchProtectionTool, Handler: EditBranchProtectionFn}) + Tool.RegisterWrite(server.ServerTool{Tool: DeleteBranchProtectionTool, Handler: DeleteBranchProtectionFn}) +} + +func ListBranchProtectionsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called ListBranchProtectionsFn") + owner, _ := req.GetArguments()["owner"].(string) + repo, _ := req.GetArguments()["repo"].(string) + if owner == "" || repo == "" { + return to.ErrorResult(errors.New("owner and repo are required")) + } + page := params.GetOptionalInt(req.GetArguments(), "page", 1) + pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100) + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + bps, _, err := client.ListBranchProtections(owner, repo, gitea_sdk.ListBranchProtectionsOptions{ + ListOptions: gitea_sdk.ListOptions{Page: int(page), PageSize: int(pageSize)}, + }) + if err != nil { + return to.ErrorResult(fmt.Errorf("list branch protections err: %v", err)) + } + return to.TextResult(bps) +} + +func GetBranchProtectionFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called GetBranchProtectionFn") + owner, _ := req.GetArguments()["owner"].(string) + repo, _ := req.GetArguments()["repo"].(string) + name, _ := req.GetArguments()["name"].(string) + if owner == "" || repo == "" || name == "" { + return to.ErrorResult(errors.New("owner, repo, and name are required")) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + bp, _, err := client.GetBranchProtection(owner, repo, name) + if err != nil { + return to.ErrorResult(fmt.Errorf("get branch protection err: %v", err)) + } + return to.TextResult(bp) +} + +func CreateBranchProtectionFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called CreateBranchProtectionFn") + owner, _ := req.GetArguments()["owner"].(string) + repo, _ := req.GetArguments()["repo"].(string) + ruleName, _ := req.GetArguments()["rule_name"].(string) + if owner == "" || repo == "" || ruleName == "" { + return to.ErrorResult(errors.New("owner, repo, and rule_name are required")) + } + opt := gitea_sdk.CreateBranchProtectionOption{ + RuleName: ruleName, + } + if v, ok := req.GetArguments()["enable_push"].(bool); ok { + opt.EnablePush = v + } + if v, ok := req.GetArguments()["enable_merge_whitelist"].(bool); ok { + opt.EnableMergeWhitelist = v + } + if v, ok := req.GetArguments()["enable_status_check"].(bool); ok { + opt.EnableStatusCheck = v + } + if v, ok := req.GetArguments()["required_approvals"].(float64); ok { + opt.RequiredApprovals = int64(v) + } + if v, ok := req.GetArguments()["block_on_rejected_reviews"].(bool); ok { + opt.BlockOnRejectedReviews = v + } + if v, ok := req.GetArguments()["dismiss_stale_approvals"].(bool); ok { + opt.DismissStaleApprovals = v + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + bp, _, err := client.CreateBranchProtection(owner, repo, opt) + if err != nil { + return to.ErrorResult(fmt.Errorf("create branch protection err: %v", err)) + } + return to.TextResult(bp) +} + +func EditBranchProtectionFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called EditBranchProtectionFn") + owner, _ := req.GetArguments()["owner"].(string) + repo, _ := req.GetArguments()["repo"].(string) + name, _ := req.GetArguments()["name"].(string) + if owner == "" || repo == "" || name == "" { + return to.ErrorResult(errors.New("owner, repo, and name are required")) + } + opt := gitea_sdk.EditBranchProtectionOption{} + if v, ok := req.GetArguments()["enable_push"].(bool); ok { + opt.EnablePush = &v + } + if v, ok := req.GetArguments()["enable_merge_whitelist"].(bool); ok { + opt.EnableMergeWhitelist = &v + } + if v, ok := req.GetArguments()["enable_status_check"].(bool); ok { + opt.EnableStatusCheck = &v + } + if v, ok := req.GetArguments()["required_approvals"].(float64); ok { + approvals := int64(v) + opt.RequiredApprovals = &approvals + } + if v, ok := req.GetArguments()["block_on_rejected_reviews"].(bool); ok { + opt.BlockOnRejectedReviews = &v + } + if v, ok := req.GetArguments()["dismiss_stale_approvals"].(bool); ok { + opt.DismissStaleApprovals = &v + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + bp, _, err := client.EditBranchProtection(owner, repo, name, opt) + if err != nil { + return to.ErrorResult(fmt.Errorf("edit branch protection err: %v", err)) + } + return to.TextResult(bp) +} + +func DeleteBranchProtectionFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called DeleteBranchProtectionFn") + owner, _ := req.GetArguments()["owner"].(string) + repo, _ := req.GetArguments()["repo"].(string) + name, _ := req.GetArguments()["name"].(string) + if owner == "" || repo == "" || name == "" { + return to.ErrorResult(errors.New("owner, repo, and name are required")) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + _, err = client.DeleteBranchProtection(owner, repo, name) + if err != nil { + return to.ErrorResult(fmt.Errorf("delete branch protection err: %v", err)) + } + return to.TextResult(map[string]string{"status": "deleted"}) +} diff --git a/operation/repo/collaborator.go b/operation/repo/collaborator.go new file mode 100644 index 0000000..b4c013c --- /dev/null +++ b/operation/repo/collaborator.go @@ -0,0 +1,233 @@ +package repo + +import ( + "context" + "errors" + "fmt" + + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/gitea" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/log" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/params" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/to" + + gitea_sdk "code.gitea.io/sdk/gitea" + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" +) + +const ( + ListCollaboratorsToolName = "list_collaborators" + IsCollaboratorToolName = "is_collaborator" + CollaboratorPermissionToolName = "collaborator_permission" + AddCollaboratorToolName = "add_collaborator" + DeleteCollaboratorToolName = "delete_collaborator" + GetReviewersToolName = "get_reviewers" + GetAssigneesToolName = "get_assignees" +) + +var ( + ListCollaboratorsTool = mcp.NewTool( + ListCollaboratorsToolName, + mcp.WithDescription("List a repository's collaborators"), + mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), + mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), + mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)), + mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)), + ) + + IsCollaboratorTool = mcp.NewTool( + IsCollaboratorToolName, + mcp.WithDescription("Check if a user is a collaborator of a repository"), + mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), + mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), + mcp.WithString("collaborator", mcp.Required(), mcp.Description("username to check")), + ) + + CollaboratorPermissionTool = mcp.NewTool( + CollaboratorPermissionToolName, + mcp.WithDescription("Get a collaborator's permission level on a repository"), + mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), + mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), + mcp.WithString("collaborator", mcp.Required(), mcp.Description("collaborator username")), + ) + + AddCollaboratorTool = mcp.NewTool( + AddCollaboratorToolName, + mcp.WithDescription("Add a collaborator to a repository"), + mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), + mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), + mcp.WithString("collaborator", mcp.Required(), mcp.Description("username to add")), + mcp.WithString("permission", mcp.Description("permission level: read, write, admin (default: write)")), + ) + + DeleteCollaboratorTool = mcp.NewTool( + DeleteCollaboratorToolName, + mcp.WithDescription("Remove a collaborator from a repository"), + mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), + mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), + mcp.WithString("collaborator", mcp.Required(), mcp.Description("username to remove")), + ) + + GetReviewersTool = mcp.NewTool( + GetReviewersToolName, + mcp.WithDescription("Get the list of users who can review PRs in a repository"), + mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), + mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), + ) + + GetAssigneesTool = mcp.NewTool( + GetAssigneesToolName, + mcp.WithDescription("Get the list of users who can be assigned to issues in a repository"), + mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), + mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), + ) +) + +func init() { + Tool.RegisterRead(server.ServerTool{Tool: ListCollaboratorsTool, Handler: ListCollaboratorsFn}) + Tool.RegisterRead(server.ServerTool{Tool: IsCollaboratorTool, Handler: IsCollaboratorFn}) + Tool.RegisterRead(server.ServerTool{Tool: CollaboratorPermissionTool, Handler: CollaboratorPermissionFn}) + Tool.RegisterRead(server.ServerTool{Tool: GetReviewersTool, Handler: GetReviewersFn}) + Tool.RegisterRead(server.ServerTool{Tool: GetAssigneesTool, Handler: GetAssigneesFn}) + Tool.RegisterWrite(server.ServerTool{Tool: AddCollaboratorTool, Handler: AddCollaboratorFn}) + Tool.RegisterWrite(server.ServerTool{Tool: DeleteCollaboratorTool, Handler: DeleteCollaboratorFn}) +} + +func ListCollaboratorsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called ListCollaboratorsFn") + owner, _ := req.GetArguments()["owner"].(string) + repo, _ := req.GetArguments()["repo"].(string) + if owner == "" || repo == "" { + return to.ErrorResult(errors.New("owner and repo are required")) + } + page := params.GetOptionalInt(req.GetArguments(), "page", 1) + pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100) + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + users, _, err := client.ListCollaborators(owner, repo, gitea_sdk.ListCollaboratorsOptions{ + ListOptions: gitea_sdk.ListOptions{Page: int(page), PageSize: int(pageSize)}, + }) + if err != nil { + return to.ErrorResult(fmt.Errorf("list collaborators err: %v", err)) + } + return to.TextResult(users) +} + +func IsCollaboratorFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called IsCollaboratorFn") + owner, _ := req.GetArguments()["owner"].(string) + repo, _ := req.GetArguments()["repo"].(string) + collaborator, _ := req.GetArguments()["collaborator"].(string) + if owner == "" || repo == "" || collaborator == "" { + return to.ErrorResult(errors.New("owner, repo, and collaborator are required")) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + isCollab, _, err := client.IsCollaborator(owner, repo, collaborator) + if err != nil { + return to.ErrorResult(fmt.Errorf("check collaborator err: %v", err)) + } + return to.TextResult(map[string]any{"collaborator": collaborator, "is_collaborator": isCollab}) +} + +func CollaboratorPermissionFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called CollaboratorPermissionFn") + owner, _ := req.GetArguments()["owner"].(string) + repo, _ := req.GetArguments()["repo"].(string) + collaborator, _ := req.GetArguments()["collaborator"].(string) + if owner == "" || repo == "" || collaborator == "" { + return to.ErrorResult(errors.New("owner, repo, and collaborator are required")) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + perm, _, err := client.CollaboratorPermission(owner, repo, collaborator) + if err != nil { + return to.ErrorResult(fmt.Errorf("get collaborator permission err: %v", err)) + } + return to.TextResult(perm) +} + +func AddCollaboratorFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called AddCollaboratorFn") + owner, _ := req.GetArguments()["owner"].(string) + repo, _ := req.GetArguments()["repo"].(string) + collaborator, _ := req.GetArguments()["collaborator"].(string) + if owner == "" || repo == "" || collaborator == "" { + return to.ErrorResult(errors.New("owner, repo, and collaborator are required")) + } + opt := gitea_sdk.AddCollaboratorOption{} + if v, ok := req.GetArguments()["permission"].(string); ok { + perm := gitea_sdk.AccessMode(v) + opt.Permission = &perm + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + _, err = client.AddCollaborator(owner, repo, collaborator, opt) + if err != nil { + return to.ErrorResult(fmt.Errorf("add collaborator err: %v", err)) + } + return to.TextResult(map[string]string{"status": "added", "collaborator": collaborator}) +} + +func DeleteCollaboratorFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called DeleteCollaboratorFn") + owner, _ := req.GetArguments()["owner"].(string) + repo, _ := req.GetArguments()["repo"].(string) + collaborator, _ := req.GetArguments()["collaborator"].(string) + if owner == "" || repo == "" || collaborator == "" { + return to.ErrorResult(errors.New("owner, repo, and collaborator are required")) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + _, err = client.DeleteCollaborator(owner, repo, collaborator) + if err != nil { + return to.ErrorResult(fmt.Errorf("delete collaborator err: %v", err)) + } + return to.TextResult(map[string]string{"status": "removed", "collaborator": collaborator}) +} + +func GetReviewersFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called GetReviewersFn") + owner, _ := req.GetArguments()["owner"].(string) + repo, _ := req.GetArguments()["repo"].(string) + if owner == "" || repo == "" { + return to.ErrorResult(errors.New("owner and repo are required")) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + users, _, err := client.GetReviewers(owner, repo) + if err != nil { + return to.ErrorResult(fmt.Errorf("get reviewers err: %v", err)) + } + return to.TextResult(users) +} + +func GetAssigneesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called GetAssigneesFn") + owner, _ := req.GetArguments()["owner"].(string) + repo, _ := req.GetArguments()["repo"].(string) + if owner == "" || repo == "" { + return to.ErrorResult(errors.New("owner and repo are required")) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + users, _, err := client.GetAssignees(owner, repo) + if err != nil { + return to.ErrorResult(fmt.Errorf("get assignees err: %v", err)) + } + return to.TextResult(users) +} diff --git a/operation/repo/commit.go b/operation/repo/commit.go index e66cb4e..77df97d 100644 --- a/operation/repo/commit.go +++ b/operation/repo/commit.go @@ -5,10 +5,10 @@ import ( "errors" "fmt" - "gitea.com/gitea/gitea-mcp/pkg/gitea" - "gitea.com/gitea/gitea-mcp/pkg/log" - "gitea.com/gitea/gitea-mcp/pkg/params" - "gitea.com/gitea/gitea-mcp/pkg/to" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/gitea" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/log" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/params" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/to" gitea_sdk "code.gitea.io/sdk/gitea" "github.com/mark3labs/mcp-go/mcp" diff --git a/operation/repo/deploy_key.go b/operation/repo/deploy_key.go new file mode 100644 index 0000000..372209f --- /dev/null +++ b/operation/repo/deploy_key.go @@ -0,0 +1,161 @@ +package repo + +import ( + "context" + "errors" + "fmt" + + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/gitea" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/log" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/params" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/to" + + gitea_sdk "code.gitea.io/sdk/gitea" + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" +) + +const ( + ListDeployKeysToolName = "list_deploy_keys" + GetDeployKeyToolName = "get_deploy_key" + CreateDeployKeyToolName = "create_deploy_key" + DeleteDeployKeyToolName = "delete_deploy_key" +) + +var ( + ListDeployKeysTool = mcp.NewTool( + ListDeployKeysToolName, + mcp.WithDescription("List a repository's deploy keys"), + mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), + mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), + mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)), + mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)), + ) + + GetDeployKeyTool = mcp.NewTool( + GetDeployKeyToolName, + mcp.WithDescription("Get a deploy key by ID"), + mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), + mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), + mcp.WithNumber("key_id", mcp.Required(), mcp.Description("deploy key ID")), + ) + + CreateDeployKeyTool = mcp.NewTool( + CreateDeployKeyToolName, + mcp.WithDescription("Add a deploy key to a repository"), + mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), + mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), + mcp.WithString("title", mcp.Required(), mcp.Description("key title")), + mcp.WithString("key", mcp.Required(), mcp.Description("public key content")), + mcp.WithBoolean("read_only", mcp.Description("whether the key has read-only access (default: true)")), + ) + + DeleteDeployKeyTool = mcp.NewTool( + DeleteDeployKeyToolName, + mcp.WithDescription("Remove a deploy key from a repository"), + mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), + mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), + mcp.WithNumber("key_id", mcp.Required(), mcp.Description("deploy key ID")), + ) +) + +func init() { + Tool.RegisterRead(server.ServerTool{Tool: ListDeployKeysTool, Handler: ListDeployKeysFn}) + Tool.RegisterRead(server.ServerTool{Tool: GetDeployKeyTool, Handler: GetDeployKeyFn}) + Tool.RegisterWrite(server.ServerTool{Tool: CreateDeployKeyTool, Handler: CreateDeployKeyFn}) + Tool.RegisterWrite(server.ServerTool{Tool: DeleteDeployKeyTool, Handler: DeleteDeployKeyFn}) +} + +func ListDeployKeysFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called ListDeployKeysFn") + owner, _ := req.GetArguments()["owner"].(string) + repo, _ := req.GetArguments()["repo"].(string) + if owner == "" || repo == "" { + return to.ErrorResult(errors.New("owner and repo are required")) + } + page := params.GetOptionalInt(req.GetArguments(), "page", 1) + pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100) + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + keys, _, err := client.ListDeployKeys(owner, repo, gitea_sdk.ListDeployKeysOptions{ + ListOptions: gitea_sdk.ListOptions{Page: int(page), PageSize: int(pageSize)}, + }) + if err != nil { + return to.ErrorResult(fmt.Errorf("list deploy keys err: %v", err)) + } + return to.TextResult(keys) +} + +func GetDeployKeyFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called GetDeployKeyFn") + owner, _ := req.GetArguments()["owner"].(string) + repo, _ := req.GetArguments()["repo"].(string) + if owner == "" || repo == "" { + return to.ErrorResult(errors.New("owner and repo are required")) + } + keyID, err := params.GetIndex(req.GetArguments(), "key_id") + if err != nil { + return to.ErrorResult(err) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + key, _, err := client.GetDeployKey(owner, repo, keyID) + if err != nil { + return to.ErrorResult(fmt.Errorf("get deploy key err: %v", err)) + } + return to.TextResult(key) +} + +func CreateDeployKeyFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called CreateDeployKeyFn") + owner, _ := req.GetArguments()["owner"].(string) + repo, _ := req.GetArguments()["repo"].(string) + title, _ := req.GetArguments()["title"].(string) + key, _ := req.GetArguments()["key"].(string) + if owner == "" || repo == "" || title == "" || key == "" { + return to.ErrorResult(errors.New("owner, repo, title, and key are required")) + } + opt := gitea_sdk.CreateKeyOption{ + Title: title, + Key: key, + ReadOnly: true, + } + if v, ok := req.GetArguments()["read_only"].(bool); ok { + opt.ReadOnly = v + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + dk, _, err := client.CreateDeployKey(owner, repo, opt) + if err != nil { + return to.ErrorResult(fmt.Errorf("create deploy key err: %v", err)) + } + return to.TextResult(dk) +} + +func DeleteDeployKeyFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called DeleteDeployKeyFn") + owner, _ := req.GetArguments()["owner"].(string) + repo, _ := req.GetArguments()["repo"].(string) + if owner == "" || repo == "" { + return to.ErrorResult(errors.New("owner and repo are required")) + } + keyID, err := params.GetIndex(req.GetArguments(), "key_id") + if err != nil { + return to.ErrorResult(err) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + _, err = client.DeleteDeployKey(owner, repo, keyID) + if err != nil { + return to.ErrorResult(fmt.Errorf("delete deploy key err: %v", err)) + } + return to.TextResult(map[string]string{"status": "deleted"}) +} diff --git a/operation/repo/file.go b/operation/repo/file.go index 623678f..42056bb 100644 --- a/operation/repo/file.go +++ b/operation/repo/file.go @@ -9,9 +9,9 @@ import ( "errors" "fmt" - "gitea.com/gitea/gitea-mcp/pkg/gitea" - "gitea.com/gitea/gitea-mcp/pkg/log" - "gitea.com/gitea/gitea-mcp/pkg/to" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/gitea" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/log" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/to" gitea_sdk "code.gitea.io/sdk/gitea" "github.com/mark3labs/mcp-go/mcp" diff --git a/operation/repo/git.go b/operation/repo/git.go new file mode 100644 index 0000000..85df381 --- /dev/null +++ b/operation/repo/git.go @@ -0,0 +1,175 @@ +package repo + +import ( + "context" + "errors" + "fmt" + + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/gitea" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/log" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/to" + + gitea_sdk "code.gitea.io/sdk/gitea" + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" +) + +const ( + GetRepoRefToolName = "get_repo_ref" + ListAllGitRefsToolName = "list_all_git_refs" + GetTreeToolName = "get_tree" + GetRepoNoteToolName = "get_repo_note" + CompareCommitsToolName = "compare_commits" +) + +var ( + GetRepoRefTool = mcp.NewTool( + GetRepoRefToolName, + mcp.WithDescription("Get a git reference from a repository"), + mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), + mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), + mcp.WithString("ref", mcp.Required(), mcp.Description("git reference (e.g., refs/heads/main, refs/tags/v1.0)")), + ) + + ListAllGitRefsTool = mcp.NewTool( + ListAllGitRefsToolName, + mcp.WithDescription("List all git references in a repository"), + mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), + mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), + ) + + GetTreeTool = mcp.NewTool( + GetTreeToolName, + mcp.WithDescription("Get the tree of a repository at a given SHA"), + mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), + mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), + mcp.WithString("sha", mcp.Required(), mcp.Description("SHA of the tree")), + mcp.WithBoolean("recursive", mcp.Description("show all items recursively")), + ) + + GetRepoNoteTool = mcp.NewTool( + GetRepoNoteToolName, + mcp.WithDescription("Get a git note for a commit"), + mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), + mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), + mcp.WithString("sha", mcp.Required(), mcp.Description("commit SHA")), + ) + + CompareCommitsTool = mcp.NewTool( + CompareCommitsToolName, + mcp.WithDescription("Compare two commits in a repository"), + mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), + mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), + mcp.WithString("base", mcp.Required(), mcp.Description("base commit SHA or branch")), + mcp.WithString("head", mcp.Required(), mcp.Description("head commit SHA or branch")), + ) +) + +func init() { + Tool.RegisterRead(server.ServerTool{Tool: GetRepoRefTool, Handler: GetRepoRefFn}) + Tool.RegisterRead(server.ServerTool{Tool: ListAllGitRefsTool, Handler: ListAllGitRefsFn}) + Tool.RegisterRead(server.ServerTool{Tool: GetTreeTool, Handler: GetTreeFn}) + Tool.RegisterRead(server.ServerTool{Tool: GetRepoNoteTool, Handler: GetRepoNoteFn}) + Tool.RegisterRead(server.ServerTool{Tool: CompareCommitsTool, Handler: CompareCommitsFn}) +} + +func GetRepoRefFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called GetRepoRefFn") + owner, _ := req.GetArguments()["owner"].(string) + repo, _ := req.GetArguments()["repo"].(string) + ref, _ := req.GetArguments()["ref"].(string) + if owner == "" || repo == "" || ref == "" { + return to.ErrorResult(errors.New("owner, repo, and ref are required")) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + reference, _, err := client.GetRepoRef(owner, repo, ref) + if err != nil { + return to.ErrorResult(fmt.Errorf("get repo ref err: %v", err)) + } + return to.TextResult(reference) +} + +func ListAllGitRefsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called ListAllGitRefsFn") + owner, _ := req.GetArguments()["owner"].(string) + repo, _ := req.GetArguments()["repo"].(string) + if owner == "" || repo == "" { + return to.ErrorResult(errors.New("owner and repo are required")) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + refs, _, err := client.ListAllGitRefs(owner, repo) + if err != nil { + return to.ErrorResult(fmt.Errorf("list git refs err: %v", err)) + } + return to.TextResult(refs) +} + +func GetTreeFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called GetTreeFn") + owner, _ := req.GetArguments()["owner"].(string) + repo, _ := req.GetArguments()["repo"].(string) + sha, _ := req.GetArguments()["sha"].(string) + if owner == "" || repo == "" || sha == "" { + return to.ErrorResult(errors.New("owner, repo, and sha are required")) + } + opt := gitea_sdk.ListTreeOptions{ + Ref: sha, + } + if v, ok := req.GetArguments()["recursive"].(bool); ok && v { + opt.Recursive = true + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + tree, _, err := client.GetTrees(owner, repo, opt) + if err != nil { + return to.ErrorResult(fmt.Errorf("get tree err: %v", err)) + } + return to.TextResult(tree) +} + +func GetRepoNoteFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called GetRepoNoteFn") + owner, _ := req.GetArguments()["owner"].(string) + repo, _ := req.GetArguments()["repo"].(string) + sha, _ := req.GetArguments()["sha"].(string) + if owner == "" || repo == "" || sha == "" { + return to.ErrorResult(errors.New("owner, repo, and sha are required")) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + note, _, err := client.GetRepoNote(owner, repo, sha, gitea_sdk.GetRepoNoteOptions{}) + if err != nil { + return to.ErrorResult(fmt.Errorf("get repo note err: %v", err)) + } + return to.TextResult(note) +} + +func CompareCommitsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called CompareCommitsFn") + owner, _ := req.GetArguments()["owner"].(string) + repo, _ := req.GetArguments()["repo"].(string) + base, _ := req.GetArguments()["base"].(string) + head, _ := req.GetArguments()["head"].(string) + if owner == "" || repo == "" || base == "" || head == "" { + return to.ErrorResult(errors.New("owner, repo, base, and head are required")) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + compare, _, err := client.CompareCommits(owner, repo, base, head) + if err != nil { + return to.ErrorResult(fmt.Errorf("compare commits err: %v", err)) + } + return to.TextResult(compare) +} diff --git a/operation/repo/hook.go b/operation/repo/hook.go new file mode 100644 index 0000000..eb10af3 --- /dev/null +++ b/operation/repo/hook.go @@ -0,0 +1,227 @@ +package repo + +import ( + "context" + "errors" + "fmt" + + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/gitea" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/log" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/params" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/to" + + gitea_sdk "code.gitea.io/sdk/gitea" + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" +) + +const ( + ListRepoHooksToolName = "list_repo_hooks" + GetRepoHookToolName = "get_repo_hook" + CreateRepoHookToolName = "create_repo_hook" + EditRepoHookToolName = "edit_repo_hook" + DeleteRepoHookToolName = "delete_repo_hook" +) + +var ( + ListRepoHooksTool = mcp.NewTool( + ListRepoHooksToolName, + mcp.WithDescription("List a repository's webhooks"), + mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), + mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), + mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)), + mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)), + ) + + GetRepoHookTool = mcp.NewTool( + GetRepoHookToolName, + mcp.WithDescription("Get a repository webhook by ID"), + mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), + mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), + mcp.WithNumber("id", mcp.Required(), mcp.Description("webhook ID")), + ) + + CreateRepoHookTool = mcp.NewTool( + CreateRepoHookToolName, + mcp.WithDescription("Create a webhook for a repository"), + mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), + mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), + mcp.WithString("type", mcp.Required(), mcp.Description("hook type: gitea, gogs, slack, discord, dingtalk, telegram, msteams, feishu, wechatwork, packagist")), + mcp.WithString("url", mcp.Required(), mcp.Description("target URL for the webhook")), + mcp.WithString("content_type", mcp.Description("content type: json or form (default: json)")), + mcp.WithString("secret", mcp.Description("webhook secret")), + mcp.WithBoolean("active", mcp.Description("whether the webhook is active (default: true)")), + ) + + EditRepoHookTool = mcp.NewTool( + EditRepoHookToolName, + mcp.WithDescription("Edit a repository webhook"), + mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), + mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), + mcp.WithNumber("id", mcp.Required(), mcp.Description("webhook ID")), + mcp.WithString("url", mcp.Description("target URL")), + mcp.WithString("content_type", mcp.Description("content type: json or form")), + mcp.WithString("secret", mcp.Description("webhook secret")), + mcp.WithBoolean("active", mcp.Description("whether the webhook is active")), + ) + + DeleteRepoHookTool = mcp.NewTool( + DeleteRepoHookToolName, + mcp.WithDescription("Delete a repository webhook"), + mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), + mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), + mcp.WithNumber("id", mcp.Required(), mcp.Description("webhook ID")), + ) +) + +func init() { + Tool.RegisterRead(server.ServerTool{Tool: ListRepoHooksTool, Handler: ListRepoHooksFn}) + Tool.RegisterRead(server.ServerTool{Tool: GetRepoHookTool, Handler: GetRepoHookFn}) + Tool.RegisterWrite(server.ServerTool{Tool: CreateRepoHookTool, Handler: CreateRepoHookFn}) + Tool.RegisterWrite(server.ServerTool{Tool: EditRepoHookTool, Handler: EditRepoHookFn}) + Tool.RegisterWrite(server.ServerTool{Tool: DeleteRepoHookTool, Handler: DeleteRepoHookFn}) +} + +func ListRepoHooksFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called ListRepoHooksFn") + owner, _ := req.GetArguments()["owner"].(string) + repo, _ := req.GetArguments()["repo"].(string) + if owner == "" || repo == "" { + return to.ErrorResult(errors.New("owner and repo are required")) + } + page := params.GetOptionalInt(req.GetArguments(), "page", 1) + pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100) + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + hooks, _, err := client.ListRepoHooks(owner, repo, gitea_sdk.ListHooksOptions{ + ListOptions: gitea_sdk.ListOptions{Page: int(page), PageSize: int(pageSize)}, + }) + if err != nil { + return to.ErrorResult(fmt.Errorf("list repo hooks err: %v", err)) + } + return to.TextResult(hooks) +} + +func GetRepoHookFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called GetRepoHookFn") + owner, _ := req.GetArguments()["owner"].(string) + repo, _ := req.GetArguments()["repo"].(string) + if owner == "" || repo == "" { + return to.ErrorResult(errors.New("owner and repo are required")) + } + id, err := params.GetIndex(req.GetArguments(), "id") + if err != nil { + return to.ErrorResult(err) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + hook, _, err := client.GetRepoHook(owner, repo, id) + if err != nil { + return to.ErrorResult(fmt.Errorf("get repo hook err: %v", err)) + } + return to.TextResult(hook) +} + +func CreateRepoHookFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called CreateRepoHookFn") + owner, _ := req.GetArguments()["owner"].(string) + repo, _ := req.GetArguments()["repo"].(string) + hookType, _ := req.GetArguments()["type"].(string) + url, _ := req.GetArguments()["url"].(string) + if owner == "" || repo == "" || hookType == "" || url == "" { + return to.ErrorResult(errors.New("owner, repo, type, and url are required")) + } + contentType := "json" + if v, ok := req.GetArguments()["content_type"].(string); ok { + contentType = v + } + config := map[string]string{ + "url": url, + "content_type": contentType, + } + if v, ok := req.GetArguments()["secret"].(string); ok { + config["secret"] = v + } + opt := gitea_sdk.CreateHookOption{ + Type: gitea_sdk.HookType(hookType), + Config: config, + Active: true, + } + if v, ok := req.GetArguments()["active"].(bool); ok { + opt.Active = v + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + hook, _, err := client.CreateRepoHook(owner, repo, opt) + if err != nil { + return to.ErrorResult(fmt.Errorf("create repo hook err: %v", err)) + } + return to.TextResult(hook) +} + +func EditRepoHookFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called EditRepoHookFn") + owner, _ := req.GetArguments()["owner"].(string) + repo, _ := req.GetArguments()["repo"].(string) + if owner == "" || repo == "" { + return to.ErrorResult(errors.New("owner and repo are required")) + } + id, err := params.GetIndex(req.GetArguments(), "id") + if err != nil { + return to.ErrorResult(err) + } + opt := gitea_sdk.EditHookOption{} + config := map[string]string{} + if v, ok := req.GetArguments()["url"].(string); ok { + config["url"] = v + } + if v, ok := req.GetArguments()["content_type"].(string); ok { + config["content_type"] = v + } + if v, ok := req.GetArguments()["secret"].(string); ok { + config["secret"] = v + } + if len(config) > 0 { + opt.Config = config + } + if v, ok := req.GetArguments()["active"].(bool); ok { + opt.Active = &v + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + _, err = client.EditRepoHook(owner, repo, id, opt) + if err != nil { + return to.ErrorResult(fmt.Errorf("edit repo hook err: %v", err)) + } + return to.TextResult(map[string]string{"status": "updated"}) +} + +func DeleteRepoHookFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called DeleteRepoHookFn") + owner, _ := req.GetArguments()["owner"].(string) + repo, _ := req.GetArguments()["repo"].(string) + if owner == "" || repo == "" { + return to.ErrorResult(errors.New("owner and repo are required")) + } + id, err := params.GetIndex(req.GetArguments(), "id") + if err != nil { + return to.ErrorResult(err) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + _, err = client.DeleteRepoHook(owner, repo, id) + if err != nil { + return to.ErrorResult(fmt.Errorf("delete repo hook err: %v", err)) + } + return to.TextResult(map[string]string{"status": "deleted"}) +} diff --git a/operation/repo/mirror.go b/operation/repo/mirror.go new file mode 100644 index 0000000..b3e63f7 --- /dev/null +++ b/operation/repo/mirror.go @@ -0,0 +1,156 @@ +package repo + +import ( + "context" + "errors" + "fmt" + + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/gitea" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/log" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/to" + + gitea_sdk "code.gitea.io/sdk/gitea" + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" +) + +const ( + CreatePushMirrorToolName = "create_push_mirror" + ListPushMirrorsToolName = "list_push_mirrors" + GetPushMirrorByRemoteNameToolName = "get_push_mirror" + DeletePushMirrorToolName = "delete_push_mirror" +) + +var ( + CreatePushMirrorTool = mcp.NewTool( + CreatePushMirrorToolName, + mcp.WithDescription("Create a push mirror for a repository"), + mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), + mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), + mcp.WithString("remote_address", mcp.Required(), mcp.Description("remote git URL to push to")), + mcp.WithString("remote_username", mcp.Description("remote username for authentication")), + mcp.WithString("remote_password", mcp.Description("remote password/token for authentication")), + mcp.WithString("interval", mcp.Description("sync interval (e.g., 8h0m0s)")), + mcp.WithBoolean("sync_on_commit", mcp.Description("sync on commit")), + ) + + ListPushMirrorsTool = mcp.NewTool( + ListPushMirrorsToolName, + mcp.WithDescription("List push mirrors for a repository"), + mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), + mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), + ) + + GetPushMirrorByRemoteNameTool = mcp.NewTool( + GetPushMirrorByRemoteNameToolName, + mcp.WithDescription("Get a push mirror by remote name"), + mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), + mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), + mcp.WithString("remote_name", mcp.Required(), mcp.Description("remote name")), + ) + + DeletePushMirrorTool = mcp.NewTool( + DeletePushMirrorToolName, + mcp.WithDescription("Delete a push mirror by remote name"), + mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), + mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), + mcp.WithString("remote_name", mcp.Required(), mcp.Description("remote name to delete")), + ) +) + +func init() { + Tool.RegisterRead(server.ServerTool{Tool: ListPushMirrorsTool, Handler: ListPushMirrorsFn}) + Tool.RegisterRead(server.ServerTool{Tool: GetPushMirrorByRemoteNameTool, Handler: GetPushMirrorByRemoteNameFn}) + Tool.RegisterWrite(server.ServerTool{Tool: CreatePushMirrorTool, Handler: CreatePushMirrorFn}) + Tool.RegisterWrite(server.ServerTool{Tool: DeletePushMirrorTool, Handler: DeletePushMirrorFn}) +} + +func CreatePushMirrorFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called CreatePushMirrorFn") + owner, _ := req.GetArguments()["owner"].(string) + repo, _ := req.GetArguments()["repo"].(string) + remoteAddr, _ := req.GetArguments()["remote_address"].(string) + if owner == "" || repo == "" || remoteAddr == "" { + return to.ErrorResult(errors.New("owner, repo, and remote_address are required")) + } + opt := gitea_sdk.CreatePushMirrorOption{ + RemoteAddress: remoteAddr, + } + if v, ok := req.GetArguments()["remote_username"].(string); ok { + opt.RemoteUsername = v + } + if v, ok := req.GetArguments()["remote_password"].(string); ok { + opt.RemotePassword = v + } + if v, ok := req.GetArguments()["interval"].(string); ok { + opt.Interval = v + } + if v, ok := req.GetArguments()["sync_on_commit"].(bool); ok { + opt.SyncONCommit = v + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + mirror, _, err := client.PushMirrors(owner, repo, opt) + if err != nil { + return to.ErrorResult(fmt.Errorf("create push mirror err: %v", err)) + } + return to.TextResult(mirror) +} + +func ListPushMirrorsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called ListPushMirrorsFn") + owner, _ := req.GetArguments()["owner"].(string) + repo, _ := req.GetArguments()["repo"].(string) + if owner == "" || repo == "" { + return to.ErrorResult(errors.New("owner and repo are required")) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + mirrors, _, err := client.ListPushMirrors(owner, repo, gitea_sdk.ListOptions{}) + if err != nil { + return to.ErrorResult(fmt.Errorf("list push mirrors err: %v", err)) + } + return to.TextResult(mirrors) +} + +func GetPushMirrorByRemoteNameFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called GetPushMirrorByRemoteNameFn") + owner, _ := req.GetArguments()["owner"].(string) + repo, _ := req.GetArguments()["repo"].(string) + remoteName, _ := req.GetArguments()["remote_name"].(string) + if owner == "" || repo == "" || remoteName == "" { + return to.ErrorResult(errors.New("owner, repo, and remote_name are required")) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + mirror, _, err := client.GetPushMirrorByRemoteName(owner, repo, remoteName) + if err != nil { + return to.ErrorResult(fmt.Errorf("get push mirror err: %v", err)) + } + return to.TextResult(mirror) +} + +func DeletePushMirrorFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called DeletePushMirrorFn") + owner, _ := req.GetArguments()["owner"].(string) + repo, _ := req.GetArguments()["repo"].(string) + remoteName, _ := req.GetArguments()["remote_name"].(string) + if owner == "" || repo == "" || remoteName == "" { + return to.ErrorResult(errors.New("owner, repo, and remote_name are required")) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + _, err = client.DeletePushMirror(owner, repo, remoteName) + if err != nil { + return to.ErrorResult(fmt.Errorf("delete push mirror err: %v", err)) + } + return to.TextResult(map[string]string{"status": "deleted"}) +} diff --git a/operation/repo/release.go b/operation/repo/release.go index e8ec44c..0a4ed7c 100644 --- a/operation/repo/release.go +++ b/operation/repo/release.go @@ -6,10 +6,10 @@ import ( "fmt" "time" - "gitea.com/gitea/gitea-mcp/pkg/gitea" - "gitea.com/gitea/gitea-mcp/pkg/log" - "gitea.com/gitea/gitea-mcp/pkg/params" - "gitea.com/gitea/gitea-mcp/pkg/to" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/gitea" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/log" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/params" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/to" gitea_sdk "code.gitea.io/sdk/gitea" "github.com/mark3labs/mcp-go/mcp" diff --git a/operation/repo/repo.go b/operation/repo/repo.go index 18dc18c..beb65d9 100644 --- a/operation/repo/repo.go +++ b/operation/repo/repo.go @@ -5,11 +5,11 @@ import ( "errors" "fmt" - "gitea.com/gitea/gitea-mcp/pkg/gitea" - "gitea.com/gitea/gitea-mcp/pkg/log" - "gitea.com/gitea/gitea-mcp/pkg/params" - "gitea.com/gitea/gitea-mcp/pkg/to" - "gitea.com/gitea/gitea-mcp/pkg/tool" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/gitea" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/log" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/params" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/to" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/tool" gitea_sdk "code.gitea.io/sdk/gitea" "github.com/mark3labs/mcp-go/mcp" diff --git a/operation/repo/star_watch.go b/operation/repo/star_watch.go new file mode 100644 index 0000000..547d02f --- /dev/null +++ b/operation/repo/star_watch.go @@ -0,0 +1,216 @@ +package repo + +import ( + "context" + "errors" + "fmt" + + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/gitea" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/log" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/params" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/to" + + gitea_sdk "code.gitea.io/sdk/gitea" + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" +) + +const ( + ListStargazersToolName = "list_stargazers" + ListForksToolName = "list_forks" + GetWatchedReposToolName = "get_watched_repos" + GetMyWatchedReposToolName = "get_my_watched_repos" + CheckRepoWatchToolName = "check_repo_watch" + WatchRepoToolName = "watch_repo" + UnWatchRepoToolName = "unwatch_repo" +) + +var ( + ListStargazersTool = mcp.NewTool( + ListStargazersToolName, + mcp.WithDescription("List a repository's stargazers"), + mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), + mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), + mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)), + mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)), + ) + + ListForksTool = mcp.NewTool( + ListForksToolName, + mcp.WithDescription("List a repository's forks"), + mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), + mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), + mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)), + mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)), + ) + + GetWatchedReposTool = mcp.NewTool( + GetWatchedReposToolName, + mcp.WithDescription("List repositories watched by a user"), + mcp.WithString("username", mcp.Required(), mcp.Description("username")), + ) + + GetMyWatchedReposTool = mcp.NewTool( + GetMyWatchedReposToolName, + mcp.WithDescription("List repositories watched by the authenticated user"), + ) + + CheckRepoWatchTool = mcp.NewTool( + CheckRepoWatchToolName, + mcp.WithDescription("Check if the authenticated user is watching a repository"), + mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), + mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), + ) + + WatchRepoTool = mcp.NewTool( + WatchRepoToolName, + mcp.WithDescription("Watch a repository"), + mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), + mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), + ) + + UnWatchRepoTool = mcp.NewTool( + UnWatchRepoToolName, + mcp.WithDescription("Unwatch a repository"), + mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), + mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), + ) +) + +func init() { + Tool.RegisterRead(server.ServerTool{Tool: ListStargazersTool, Handler: ListStargazersFn}) + Tool.RegisterRead(server.ServerTool{Tool: ListForksTool, Handler: ListForksFn}) + Tool.RegisterRead(server.ServerTool{Tool: GetWatchedReposTool, Handler: GetWatchedReposFn}) + Tool.RegisterRead(server.ServerTool{Tool: GetMyWatchedReposTool, Handler: GetMyWatchedReposFn}) + Tool.RegisterRead(server.ServerTool{Tool: CheckRepoWatchTool, Handler: CheckRepoWatchFn}) + Tool.RegisterWrite(server.ServerTool{Tool: WatchRepoTool, Handler: WatchRepoFn}) + Tool.RegisterWrite(server.ServerTool{Tool: UnWatchRepoTool, Handler: UnWatchRepoFn}) +} + +func ListStargazersFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called ListStargazersFn") + owner, _ := req.GetArguments()["owner"].(string) + repo, _ := req.GetArguments()["repo"].(string) + if owner == "" || repo == "" { + return to.ErrorResult(errors.New("owner and repo are required")) + } + page := params.GetOptionalInt(req.GetArguments(), "page", 1) + pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100) + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + users, _, err := client.ListRepoStargazers(owner, repo, gitea_sdk.ListStargazersOptions{ + ListOptions: gitea_sdk.ListOptions{Page: int(page), PageSize: int(pageSize)}, + }) + if err != nil { + return to.ErrorResult(fmt.Errorf("list stargazers err: %v", err)) + } + return to.TextResult(users) +} + +func ListForksFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called ListForksFn") + owner, _ := req.GetArguments()["owner"].(string) + repo, _ := req.GetArguments()["repo"].(string) + if owner == "" || repo == "" { + return to.ErrorResult(errors.New("owner and repo are required")) + } + page := params.GetOptionalInt(req.GetArguments(), "page", 1) + pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100) + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + repos, _, err := client.ListForks(owner, repo, gitea_sdk.ListForksOptions{ + ListOptions: gitea_sdk.ListOptions{Page: int(page), PageSize: int(pageSize)}, + }) + if err != nil { + return to.ErrorResult(fmt.Errorf("list forks err: %v", err)) + } + return to.TextResult(repos) +} + +func GetWatchedReposFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called GetWatchedReposFn") + username, _ := req.GetArguments()["username"].(string) + if username == "" { + return to.ErrorResult(errors.New("username is required")) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + repos, _, err := client.GetWatchedRepos(username) + if err != nil { + return to.ErrorResult(fmt.Errorf("get watched repos err: %v", err)) + } + return to.TextResult(repos) +} + +func GetMyWatchedReposFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called GetMyWatchedReposFn") + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + repos, _, err := client.GetMyWatchedRepos() + if err != nil { + return to.ErrorResult(fmt.Errorf("get my watched repos err: %v", err)) + } + return to.TextResult(repos) +} + +func CheckRepoWatchFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called CheckRepoWatchFn") + owner, _ := req.GetArguments()["owner"].(string) + repo, _ := req.GetArguments()["repo"].(string) + if owner == "" || repo == "" { + return to.ErrorResult(errors.New("owner and repo are required")) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + watching, _, err := client.CheckRepoWatch(owner, repo) + if err != nil { + return to.ErrorResult(fmt.Errorf("check repo watch err: %v", err)) + } + return to.TextResult(map[string]any{"watching": watching}) +} + +func WatchRepoFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called WatchRepoFn") + owner, _ := req.GetArguments()["owner"].(string) + repo, _ := req.GetArguments()["repo"].(string) + if owner == "" || repo == "" { + return to.ErrorResult(errors.New("owner and repo are required")) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + _, err = client.WatchRepo(owner, repo) + if err != nil { + return to.ErrorResult(fmt.Errorf("watch repo err: %v", err)) + } + return to.TextResult(map[string]string{"status": "watching"}) +} + +func UnWatchRepoFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called UnWatchRepoFn") + owner, _ := req.GetArguments()["owner"].(string) + repo, _ := req.GetArguments()["repo"].(string) + if owner == "" || repo == "" { + return to.ErrorResult(errors.New("owner and repo are required")) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + _, err = client.UnWatchRepo(owner, repo) + if err != nil { + return to.ErrorResult(fmt.Errorf("unwatch repo err: %v", err)) + } + return to.TextResult(map[string]string{"status": "unwatched"}) +} diff --git a/operation/repo/status.go b/operation/repo/status.go new file mode 100644 index 0000000..3519279 --- /dev/null +++ b/operation/repo/status.go @@ -0,0 +1,134 @@ +package repo + +import ( + "context" + "errors" + "fmt" + + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/gitea" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/log" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/params" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/to" + + gitea_sdk "code.gitea.io/sdk/gitea" + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" +) + +const ( + CreateStatusToolName = "create_commit_status" + ListStatusesToolName = "list_commit_statuses" + GetCombinedStatusToolName = "get_combined_status" +) + +var ( + CreateStatusTool = mcp.NewTool( + CreateStatusToolName, + mcp.WithDescription("Create a commit status"), + mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), + mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), + mcp.WithString("sha", mcp.Required(), mcp.Description("commit SHA")), + mcp.WithString("state", mcp.Required(), mcp.Description("status state: pending, success, error, failure, warning")), + mcp.WithString("target_url", mcp.Description("URL for status details")), + mcp.WithString("description", mcp.Description("status description")), + mcp.WithString("context", mcp.Description("status context (e.g., ci/build)")), + ) + + ListStatusesTool = mcp.NewTool( + ListStatusesToolName, + mcp.WithDescription("List commit statuses for a ref"), + mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), + mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), + mcp.WithString("ref", mcp.Required(), mcp.Description("commit SHA, branch, or tag")), + mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)), + mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)), + ) + + GetCombinedStatusTool = mcp.NewTool( + GetCombinedStatusToolName, + mcp.WithDescription("Get the combined status for a ref"), + mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), + mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), + mcp.WithString("ref", mcp.Required(), mcp.Description("commit SHA, branch, or tag")), + ) +) + +func init() { + Tool.RegisterRead(server.ServerTool{Tool: ListStatusesTool, Handler: ListStatusesFn}) + Tool.RegisterRead(server.ServerTool{Tool: GetCombinedStatusTool, Handler: GetCombinedStatusFn}) + Tool.RegisterWrite(server.ServerTool{Tool: CreateStatusTool, Handler: CreateStatusFn}) +} + +func CreateStatusFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called CreateStatusFn") + owner, _ := req.GetArguments()["owner"].(string) + repo, _ := req.GetArguments()["repo"].(string) + sha, _ := req.GetArguments()["sha"].(string) + state, _ := req.GetArguments()["state"].(string) + if owner == "" || repo == "" || sha == "" || state == "" { + return to.ErrorResult(errors.New("owner, repo, sha, and state are required")) + } + opt := gitea_sdk.CreateStatusOption{ + State: gitea_sdk.StatusState(state), + } + if v, ok := req.GetArguments()["target_url"].(string); ok { + opt.TargetURL = v + } + if v, ok := req.GetArguments()["description"].(string); ok { + opt.Description = v + } + if v, ok := req.GetArguments()["context"].(string); ok { + opt.Context = v + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + status, _, err := client.CreateStatus(owner, repo, sha, opt) + if err != nil { + return to.ErrorResult(fmt.Errorf("create status err: %v", err)) + } + return to.TextResult(status) +} + +func ListStatusesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called ListStatusesFn") + owner, _ := req.GetArguments()["owner"].(string) + repo, _ := req.GetArguments()["repo"].(string) + ref, _ := req.GetArguments()["ref"].(string) + if owner == "" || repo == "" || ref == "" { + return to.ErrorResult(errors.New("owner, repo, and ref are required")) + } + page := params.GetOptionalInt(req.GetArguments(), "page", 1) + pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100) + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + statuses, _, err := client.ListStatuses(owner, repo, ref, gitea_sdk.ListStatusesOption{ + ListOptions: gitea_sdk.ListOptions{Page: int(page), PageSize: int(pageSize)}, + }) + if err != nil { + return to.ErrorResult(fmt.Errorf("list statuses err: %v", err)) + } + return to.TextResult(statuses) +} + +func GetCombinedStatusFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called GetCombinedStatusFn") + owner, _ := req.GetArguments()["owner"].(string) + repo, _ := req.GetArguments()["repo"].(string) + ref, _ := req.GetArguments()["ref"].(string) + if owner == "" || repo == "" || ref == "" { + return to.ErrorResult(errors.New("owner, repo, and ref are required")) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + combined, _, err := client.GetCombinedStatus(owner, repo, ref) + if err != nil { + return to.ErrorResult(fmt.Errorf("get combined status err: %v", err)) + } + return to.TextResult(combined) +} diff --git a/operation/repo/tag.go b/operation/repo/tag.go index 42803df..6d21d2d 100644 --- a/operation/repo/tag.go +++ b/operation/repo/tag.go @@ -5,10 +5,10 @@ import ( "errors" "fmt" - "gitea.com/gitea/gitea-mcp/pkg/gitea" - "gitea.com/gitea/gitea-mcp/pkg/log" - "gitea.com/gitea/gitea-mcp/pkg/params" - "gitea.com/gitea/gitea-mcp/pkg/to" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/gitea" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/log" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/params" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/to" gitea_sdk "code.gitea.io/sdk/gitea" "github.com/mark3labs/mcp-go/mcp" diff --git a/operation/repo/tag_protection.go b/operation/repo/tag_protection.go new file mode 100644 index 0000000..9a0e4a1 --- /dev/null +++ b/operation/repo/tag_protection.go @@ -0,0 +1,200 @@ +package repo + +import ( + "context" + "errors" + "fmt" + + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/gitea" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/log" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/params" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/to" + + gitea_sdk "code.gitea.io/sdk/gitea" + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" +) + +const ( + ListTagProtectionsToolName = "list_tag_protections" + GetTagProtectionToolName = "get_tag_protection" + CreateTagProtectionToolName = "create_tag_protection" + EditTagProtectionToolName = "edit_tag_protection" + DeleteTagProtectionToolName = "delete_tag_protection" +) + +var ( + ListTagProtectionsTool = mcp.NewTool( + ListTagProtectionsToolName, + mcp.WithDescription("List tag protections for a repository"), + mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), + mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), + ) + + GetTagProtectionTool = mcp.NewTool( + GetTagProtectionToolName, + mcp.WithDescription("Get a tag protection rule by ID"), + mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), + mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), + mcp.WithNumber("id", mcp.Required(), mcp.Description("tag protection ID")), + ) + + CreateTagProtectionTool = mcp.NewTool( + CreateTagProtectionToolName, + mcp.WithDescription("Create a tag protection rule"), + mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), + mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), + mcp.WithString("name_pattern", mcp.Required(), mcp.Description("glob pattern for tag name matching")), + mcp.WithString("whitelist_usernames", mcp.Description("comma-separated list of allowed usernames")), + mcp.WithString("whitelist_teams", mcp.Description("comma-separated list of allowed team names")), + ) + + EditTagProtectionTool = mcp.NewTool( + EditTagProtectionToolName, + mcp.WithDescription("Edit a tag protection rule"), + mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), + mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), + mcp.WithNumber("id", mcp.Required(), mcp.Description("tag protection ID")), + mcp.WithString("name_pattern", mcp.Description("glob pattern for tag name matching")), + mcp.WithString("whitelist_usernames", mcp.Description("comma-separated list of allowed usernames")), + mcp.WithString("whitelist_teams", mcp.Description("comma-separated list of allowed team names")), + ) + + DeleteTagProtectionTool = mcp.NewTool( + DeleteTagProtectionToolName, + mcp.WithDescription("Delete a tag protection rule"), + mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), + mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), + mcp.WithNumber("id", mcp.Required(), mcp.Description("tag protection ID")), + ) +) + +func init() { + Tool.RegisterRead(server.ServerTool{Tool: ListTagProtectionsTool, Handler: ListTagProtectionsFn}) + Tool.RegisterRead(server.ServerTool{Tool: GetTagProtectionTool, Handler: GetTagProtectionFn}) + Tool.RegisterWrite(server.ServerTool{Tool: CreateTagProtectionTool, Handler: CreateTagProtectionFn}) + Tool.RegisterWrite(server.ServerTool{Tool: EditTagProtectionTool, Handler: EditTagProtectionFn}) + Tool.RegisterWrite(server.ServerTool{Tool: DeleteTagProtectionTool, Handler: DeleteTagProtectionFn}) +} + +func ListTagProtectionsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called ListTagProtectionsFn") + owner, _ := req.GetArguments()["owner"].(string) + repo, _ := req.GetArguments()["repo"].(string) + if owner == "" || repo == "" { + return to.ErrorResult(errors.New("owner and repo are required")) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + tps, _, err := client.ListTagProtection(owner, repo, gitea_sdk.ListRepoTagProtectionsOptions{}) + if err != nil { + return to.ErrorResult(fmt.Errorf("list tag protections err: %v", err)) + } + return to.TextResult(tps) +} + +func GetTagProtectionFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called GetTagProtectionFn") + owner, _ := req.GetArguments()["owner"].(string) + repo, _ := req.GetArguments()["repo"].(string) + if owner == "" || repo == "" { + return to.ErrorResult(errors.New("owner and repo are required")) + } + id, err := params.GetIndex(req.GetArguments(), "id") + if err != nil { + return to.ErrorResult(err) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + tp, _, err := client.GetTagProtection(owner, repo, id) + if err != nil { + return to.ErrorResult(fmt.Errorf("get tag protection err: %v", err)) + } + return to.TextResult(tp) +} + +func CreateTagProtectionFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called CreateTagProtectionFn") + owner, _ := req.GetArguments()["owner"].(string) + repo, _ := req.GetArguments()["repo"].(string) + namePattern, _ := req.GetArguments()["name_pattern"].(string) + if owner == "" || repo == "" || namePattern == "" { + return to.ErrorResult(errors.New("owner, repo, and name_pattern are required")) + } + opt := gitea_sdk.CreateTagProtectionOption{ + NamePattern: namePattern, + } + if v, ok := req.GetArguments()["whitelist_usernames"].(string); ok && v != "" { + opt.WhitelistUsernames = splitAndTrim(v) + } + if v, ok := req.GetArguments()["whitelist_teams"].(string); ok && v != "" { + opt.WhitelistTeams = splitAndTrim(v) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + tp, _, err := client.CreateTagProtection(owner, repo, opt) + if err != nil { + return to.ErrorResult(fmt.Errorf("create tag protection err: %v", err)) + } + return to.TextResult(tp) +} + +func EditTagProtectionFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called EditTagProtectionFn") + owner, _ := req.GetArguments()["owner"].(string) + repo, _ := req.GetArguments()["repo"].(string) + if owner == "" || repo == "" { + return to.ErrorResult(errors.New("owner and repo are required")) + } + id, err := params.GetIndex(req.GetArguments(), "id") + if err != nil { + return to.ErrorResult(err) + } + opt := gitea_sdk.EditTagProtectionOption{} + if v, ok := req.GetArguments()["name_pattern"].(string); ok { + opt.NamePattern = &v + } + if v, ok := req.GetArguments()["whitelist_usernames"].(string); ok { + opt.WhitelistUsernames = splitAndTrim(v) + } + if v, ok := req.GetArguments()["whitelist_teams"].(string); ok { + opt.WhitelistTeams = splitAndTrim(v) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + tp, _, err := client.EditTagProtection(owner, repo, id, opt) + if err != nil { + return to.ErrorResult(fmt.Errorf("edit tag protection err: %v", err)) + } + return to.TextResult(tp) +} + +func DeleteTagProtectionFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called DeleteTagProtectionFn") + owner, _ := req.GetArguments()["owner"].(string) + repo, _ := req.GetArguments()["repo"].(string) + if owner == "" || repo == "" { + return to.ErrorResult(errors.New("owner and repo are required")) + } + id, err := params.GetIndex(req.GetArguments(), "id") + if err != nil { + return to.ErrorResult(err) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + _, err = client.DeleteTagProtection(owner, repo, id) + if err != nil { + return to.ErrorResult(fmt.Errorf("delete tag protection err: %v", err)) + } + return to.TextResult(map[string]string{"status": "deleted"}) +} diff --git a/operation/repo/topic.go b/operation/repo/topic.go new file mode 100644 index 0000000..edc7bcc --- /dev/null +++ b/operation/repo/topic.go @@ -0,0 +1,157 @@ +package repo + +import ( + "context" + "errors" + "fmt" + "strings" + + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/gitea" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/log" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/to" + + gitea_sdk "code.gitea.io/sdk/gitea" + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" +) + +const ( + ListRepoTopicsToolName = "list_repo_topics" + SetRepoTopicsToolName = "set_repo_topics" + AddRepoTopicToolName = "add_repo_topic" + DeleteRepoTopicToolName = "delete_repo_topic" +) + +var ( + ListRepoTopicsTool = mcp.NewTool( + ListRepoTopicsToolName, + mcp.WithDescription("List a repository's topics"), + mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), + mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), + ) + + SetRepoTopicsTool = mcp.NewTool( + SetRepoTopicsToolName, + mcp.WithDescription("Replace all topics of a repository with a new list"), + mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), + mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), + mcp.WithString("topics", mcp.Required(), mcp.Description("comma-separated list of topics")), + ) + + AddRepoTopicTool = mcp.NewTool( + AddRepoTopicToolName, + mcp.WithDescription("Add a topic to a repository"), + mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), + mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), + mcp.WithString("topic", mcp.Required(), mcp.Description("topic name to add")), + ) + + DeleteRepoTopicTool = mcp.NewTool( + DeleteRepoTopicToolName, + mcp.WithDescription("Remove a topic from a repository"), + mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), + mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), + mcp.WithString("topic", mcp.Required(), mcp.Description("topic name to remove")), + ) +) + +func init() { + Tool.RegisterRead(server.ServerTool{Tool: ListRepoTopicsTool, Handler: ListRepoTopicsFn}) + Tool.RegisterWrite(server.ServerTool{Tool: SetRepoTopicsTool, Handler: SetRepoTopicsFn}) + Tool.RegisterWrite(server.ServerTool{Tool: AddRepoTopicTool, Handler: AddRepoTopicFn}) + Tool.RegisterWrite(server.ServerTool{Tool: DeleteRepoTopicTool, Handler: DeleteRepoTopicFn}) +} + +func ListRepoTopicsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called ListRepoTopicsFn") + owner, _ := req.GetArguments()["owner"].(string) + repo, _ := req.GetArguments()["repo"].(string) + if owner == "" || repo == "" { + return to.ErrorResult(errors.New("owner and repo are required")) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + topics, _, err := client.ListRepoTopics(owner, repo, gitea_sdk.ListRepoTopicsOptions{}) + if err != nil { + return to.ErrorResult(fmt.Errorf("list repo topics err: %v", err)) + } + return to.TextResult(topics) +} + +func SetRepoTopicsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called SetRepoTopicsFn") + owner, _ := req.GetArguments()["owner"].(string) + repo, _ := req.GetArguments()["repo"].(string) + topicsStr, _ := req.GetArguments()["topics"].(string) + if owner == "" || repo == "" { + return to.ErrorResult(errors.New("owner and repo are required")) + } + var topics []string + if topicsStr != "" { + for _, t := range splitAndTrim(topicsStr) { + if t != "" { + topics = append(topics, t) + } + } + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + _, err = client.SetRepoTopics(owner, repo, topics) + if err != nil { + return to.ErrorResult(fmt.Errorf("set repo topics err: %v", err)) + } + return to.TextResult(map[string]any{"status": "updated", "topics": topics}) +} + +func AddRepoTopicFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called AddRepoTopicFn") + owner, _ := req.GetArguments()["owner"].(string) + repo, _ := req.GetArguments()["repo"].(string) + topic, _ := req.GetArguments()["topic"].(string) + if owner == "" || repo == "" || topic == "" { + return to.ErrorResult(errors.New("owner, repo, and topic are required")) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + _, err = client.AddRepoTopic(owner, repo, topic) + if err != nil { + return to.ErrorResult(fmt.Errorf("add repo topic err: %v", err)) + } + return to.TextResult(map[string]string{"status": "added", "topic": topic}) +} + +func DeleteRepoTopicFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called DeleteRepoTopicFn") + owner, _ := req.GetArguments()["owner"].(string) + repo, _ := req.GetArguments()["repo"].(string) + topic, _ := req.GetArguments()["topic"].(string) + if owner == "" || repo == "" || topic == "" { + return to.ErrorResult(errors.New("owner, repo, and topic are required")) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + _, err = client.DeleteRepoTopic(owner, repo, topic) + if err != nil { + return to.ErrorResult(fmt.Errorf("delete repo topic err: %v", err)) + } + return to.TextResult(map[string]string{"status": "deleted", "topic": topic}) +} + +func splitAndTrim(s string) []string { + var result []string + for _, part := range strings.Split(s, ",") { + trimmed := strings.TrimSpace(part) + if trimmed != "" { + result = append(result, trimmed) + } + } + return result +} diff --git a/operation/repo/transfer.go b/operation/repo/transfer.go new file mode 100644 index 0000000..590fbc3 --- /dev/null +++ b/operation/repo/transfer.go @@ -0,0 +1,167 @@ +package repo + +import ( + "context" + "errors" + "fmt" + + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/gitea" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/log" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/to" + + gitea_sdk "code.gitea.io/sdk/gitea" + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" +) + +const ( + TransferRepoToolName = "transfer_repo" + AcceptRepoTransferToolName = "accept_repo_transfer" + RejectRepoTransferToolName = "reject_repo_transfer" + CreateFromTemplateToolName = "create_repo_from_template" +) + +var ( + TransferRepoTool = mcp.NewTool( + TransferRepoToolName, + mcp.WithDescription("Transfer a repository to a new owner"), + mcp.WithString("owner", mcp.Required(), mcp.Description("current repository owner")), + mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), + mcp.WithString("new_owner", mcp.Required(), mcp.Description("new owner username or org")), + ) + + AcceptRepoTransferTool = mcp.NewTool( + AcceptRepoTransferToolName, + mcp.WithDescription("Accept a pending repository transfer"), + mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), + mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), + ) + + RejectRepoTransferTool = mcp.NewTool( + RejectRepoTransferToolName, + mcp.WithDescription("Reject a pending repository transfer"), + mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), + mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), + ) + + CreateFromTemplateTool = mcp.NewTool( + CreateFromTemplateToolName, + mcp.WithDescription("Create a repository from a template"), + mcp.WithString("template_owner", mcp.Required(), mcp.Description("template repository owner")), + mcp.WithString("template_repo", mcp.Required(), mcp.Description("template repository name")), + mcp.WithString("name", mcp.Required(), mcp.Description("name for the new repository")), + mcp.WithString("owner", mcp.Required(), mcp.Description("owner of the new repository (user or org)")), + mcp.WithString("description", mcp.Description("description for the new repository")), + mcp.WithBoolean("private", mcp.Description("whether the new repository is private")), + mcp.WithBoolean("git_content", mcp.Description("copy git content from template (default: true)")), + mcp.WithBoolean("topics", mcp.Description("copy topics from template")), + mcp.WithBoolean("labels", mcp.Description("copy labels from template")), + mcp.WithBoolean("webhooks", mcp.Description("copy webhooks from template")), + ) +) + +func init() { + Tool.RegisterWrite(server.ServerTool{Tool: TransferRepoTool, Handler: TransferRepoFn}) + Tool.RegisterWrite(server.ServerTool{Tool: AcceptRepoTransferTool, Handler: AcceptRepoTransferFn}) + Tool.RegisterWrite(server.ServerTool{Tool: RejectRepoTransferTool, Handler: RejectRepoTransferFn}) + Tool.RegisterWrite(server.ServerTool{Tool: CreateFromTemplateTool, Handler: CreateFromTemplateFn}) +} + +func TransferRepoFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called TransferRepoFn") + owner, _ := req.GetArguments()["owner"].(string) + repo, _ := req.GetArguments()["repo"].(string) + newOwner, _ := req.GetArguments()["new_owner"].(string) + if owner == "" || repo == "" || newOwner == "" { + return to.ErrorResult(errors.New("owner, repo, and new_owner are required")) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + repository, _, err := client.TransferRepo(owner, repo, gitea_sdk.TransferRepoOption{ + NewOwner: newOwner, + }) + if err != nil { + return to.ErrorResult(fmt.Errorf("transfer repo err: %v", err)) + } + return to.TextResult(repository) +} + +func AcceptRepoTransferFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called AcceptRepoTransferFn") + owner, _ := req.GetArguments()["owner"].(string) + repo, _ := req.GetArguments()["repo"].(string) + if owner == "" || repo == "" { + return to.ErrorResult(errors.New("owner and repo are required")) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + repository, _, err := client.AcceptRepoTransfer(owner, repo) + if err != nil { + return to.ErrorResult(fmt.Errorf("accept repo transfer err: %v", err)) + } + return to.TextResult(repository) +} + +func RejectRepoTransferFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called RejectRepoTransferFn") + owner, _ := req.GetArguments()["owner"].(string) + repo, _ := req.GetArguments()["repo"].(string) + if owner == "" || repo == "" { + return to.ErrorResult(errors.New("owner and repo are required")) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + repository, _, err := client.RejectRepoTransfer(owner, repo) + if err != nil { + return to.ErrorResult(fmt.Errorf("reject repo transfer err: %v", err)) + } + return to.TextResult(repository) +} + +func CreateFromTemplateFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called CreateFromTemplateFn") + templateOwner, _ := req.GetArguments()["template_owner"].(string) + templateRepo, _ := req.GetArguments()["template_repo"].(string) + name, _ := req.GetArguments()["name"].(string) + owner, _ := req.GetArguments()["owner"].(string) + if templateOwner == "" || templateRepo == "" || name == "" || owner == "" { + return to.ErrorResult(errors.New("template_owner, template_repo, name, and owner are required")) + } + opt := gitea_sdk.CreateRepoFromTemplateOption{ + Name: name, + Owner: owner, + } + if v, ok := req.GetArguments()["description"].(string); ok { + opt.Description = v + } + if v, ok := req.GetArguments()["private"].(bool); ok { + opt.Private = v + } + if v, ok := req.GetArguments()["git_content"].(bool); ok { + opt.GitContent = v + } + if v, ok := req.GetArguments()["topics"].(bool); ok { + opt.Topics = v + } + if v, ok := req.GetArguments()["labels"].(bool); ok { + opt.Labels = v + } + if v, ok := req.GetArguments()["webhooks"].(bool); ok { + opt.Webhooks = v + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + repository, _, err := client.CreateRepoFromTemplate(templateOwner, templateRepo, opt) + if err != nil { + return to.ErrorResult(fmt.Errorf("create from template err: %v", err)) + } + return to.TextResult(repository) +} diff --git a/operation/search/search.go b/operation/search/search.go index bbb6127..e4a8800 100644 --- a/operation/search/search.go +++ b/operation/search/search.go @@ -5,11 +5,11 @@ import ( "errors" "fmt" - "gitea.com/gitea/gitea-mcp/pkg/gitea" - "gitea.com/gitea/gitea-mcp/pkg/log" - "gitea.com/gitea/gitea-mcp/pkg/params" - "gitea.com/gitea/gitea-mcp/pkg/to" - "gitea.com/gitea/gitea-mcp/pkg/tool" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/gitea" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/log" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/params" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/to" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/tool" gitea_sdk "code.gitea.io/sdk/gitea" "github.com/mark3labs/mcp-go/mcp" diff --git a/operation/settings/settings.go b/operation/settings/settings.go new file mode 100644 index 0000000..e2673f2 --- /dev/null +++ b/operation/settings/settings.go @@ -0,0 +1,104 @@ +package settings + +import ( + "context" + "fmt" + + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/gitea" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/log" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/to" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/tool" + + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" +) + +var Tool = tool.New() + +const ( + GetAPISettingsToolName = "get_api_settings" + GetAttachmentSettingsToolName = "get_attachment_settings" + GetRepoSettingsToolName = "get_repo_settings" + GetUISettingsToolName = "get_ui_settings" +) + +var ( + GetAPISettingsTool = mcp.NewTool( + GetAPISettingsToolName, + mcp.WithDescription("Get the global API settings of the Gitea instance"), + ) + + GetAttachmentSettingsTool = mcp.NewTool( + GetAttachmentSettingsToolName, + mcp.WithDescription("Get the global attachment settings"), + ) + + GetRepoSettingsTool = mcp.NewTool( + GetRepoSettingsToolName, + mcp.WithDescription("Get the global repository settings"), + ) + + GetUISettingsTool = mcp.NewTool( + GetUISettingsToolName, + mcp.WithDescription("Get the global UI settings"), + ) +) + +func init() { + Tool.RegisterRead(server.ServerTool{Tool: GetAPISettingsTool, Handler: GetAPISettingsFn}) + Tool.RegisterRead(server.ServerTool{Tool: GetAttachmentSettingsTool, Handler: GetAttachmentSettingsFn}) + Tool.RegisterRead(server.ServerTool{Tool: GetRepoSettingsTool, Handler: GetRepoSettingsFn}) + Tool.RegisterRead(server.ServerTool{Tool: GetUISettingsTool, Handler: GetUISettingsFn}) +} + +func GetAPISettingsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called GetAPISettingsFn") + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + settings, _, err := client.GetGlobalAPISettings() + if err != nil { + return to.ErrorResult(fmt.Errorf("get API settings err: %v", err)) + } + return to.TextResult(settings) +} + +func GetAttachmentSettingsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called GetAttachmentSettingsFn") + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + settings, _, err := client.GetGlobalAttachmentSettings() + if err != nil { + return to.ErrorResult(fmt.Errorf("get attachment settings err: %v", err)) + } + return to.TextResult(settings) +} + +func GetRepoSettingsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called GetRepoSettingsFn") + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + settings, _, err := client.GetGlobalRepoSettings() + if err != nil { + return to.ErrorResult(fmt.Errorf("get repo settings err: %v", err)) + } + return to.TextResult(settings) +} + +func GetUISettingsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called GetUISettingsFn") + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + settings, _, err := client.GetGlobalUISettings() + if err != nil { + return to.ErrorResult(fmt.Errorf("get UI settings err: %v", err)) + } + return to.TextResult(settings) +} diff --git a/operation/timetracking/timetracking.go b/operation/timetracking/timetracking.go index fb82b8b..e6554fe 100644 --- a/operation/timetracking/timetracking.go +++ b/operation/timetracking/timetracking.go @@ -7,11 +7,11 @@ import ( "fmt" gitea_sdk "code.gitea.io/sdk/gitea" - "gitea.com/gitea/gitea-mcp/pkg/gitea" - "gitea.com/gitea/gitea-mcp/pkg/log" - "gitea.com/gitea/gitea-mcp/pkg/params" - "gitea.com/gitea/gitea-mcp/pkg/to" - "gitea.com/gitea/gitea-mcp/pkg/tool" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/gitea" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/log" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/params" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/to" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/tool" "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" diff --git a/operation/user/blocks.go b/operation/user/blocks.go new file mode 100644 index 0000000..f21ed58 --- /dev/null +++ b/operation/user/blocks.go @@ -0,0 +1,125 @@ +package user + +import ( + "context" + "errors" + "fmt" + + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/gitea" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/log" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/params" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/to" + + gitea_sdk "code.gitea.io/sdk/gitea" + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" +) + +const ( + ListMyBlocksToolName = "list_my_blocks" + CheckUserBlockToolName = "check_user_block" + BlockUserToolName = "block_user" + UnblockUserToolName = "unblock_user" +) + +var ( + ListMyBlocksTool = mcp.NewTool( + ListMyBlocksToolName, + mcp.WithDescription("List users blocked by the authenticated user"), + mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)), + mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)), + ) + + CheckUserBlockTool = mcp.NewTool( + CheckUserBlockToolName, + mcp.WithDescription("Check if the authenticated user has blocked a user"), + mcp.WithString("username", mcp.Required(), mcp.Description("username to check")), + ) + + BlockUserTool = mcp.NewTool( + BlockUserToolName, + mcp.WithDescription("Block a user"), + mcp.WithString("username", mcp.Required(), mcp.Description("username to block")), + ) + + UnblockUserTool = mcp.NewTool( + UnblockUserToolName, + mcp.WithDescription("Unblock a user"), + mcp.WithString("username", mcp.Required(), mcp.Description("username to unblock")), + ) +) + +func init() { + Tool.RegisterRead(server.ServerTool{Tool: ListMyBlocksTool, Handler: ListMyBlocksFn}) + Tool.RegisterRead(server.ServerTool{Tool: CheckUserBlockTool, Handler: CheckUserBlockFn}) + Tool.RegisterWrite(server.ServerTool{Tool: BlockUserTool, Handler: BlockUserFn}) + Tool.RegisterWrite(server.ServerTool{Tool: UnblockUserTool, Handler: UnblockUserFn}) +} + +func ListMyBlocksFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called ListMyBlocksFn") + page := params.GetOptionalInt(req.GetArguments(), "page", 1) + pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100) + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + users, _, err := client.ListMyBlocks(gitea_sdk.ListUserBlocksOptions{ + ListOptions: gitea_sdk.ListOptions{Page: int(page), PageSize: int(pageSize)}, + }) + if err != nil { + return to.ErrorResult(fmt.Errorf("list my blocks err: %v", err)) + } + return to.TextResult(users) +} + +func CheckUserBlockFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called CheckUserBlockFn") + username, ok := req.GetArguments()["username"].(string) + if !ok { + return to.ErrorResult(errors.New("username is required")) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + isBlocked, _, err := client.CheckUserBlock(username) + if err != nil { + return to.ErrorResult(fmt.Errorf("check block %s err: %v", username, err)) + } + return to.TextResult(map[string]any{"username": username, "is_blocked": isBlocked}) +} + +func BlockUserFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called BlockUserFn") + username, ok := req.GetArguments()["username"].(string) + if !ok { + return to.ErrorResult(errors.New("username is required")) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + _, err = client.BlockUser(username) + if err != nil { + return to.ErrorResult(fmt.Errorf("block user %s err: %v", username, err)) + } + return to.TextResult(map[string]string{"status": "blocked", "username": username}) +} + +func UnblockUserFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called UnblockUserFn") + username, ok := req.GetArguments()["username"].(string) + if !ok { + return to.ErrorResult(errors.New("username is required")) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + _, err = client.UnblockUser(username) + if err != nil { + return to.ErrorResult(fmt.Errorf("unblock user %s err: %v", username, err)) + } + return to.TextResult(map[string]string{"status": "unblocked", "username": username}) +} diff --git a/operation/user/emails.go b/operation/user/emails.go new file mode 100644 index 0000000..e305aa9 --- /dev/null +++ b/operation/user/emails.go @@ -0,0 +1,119 @@ +package user + +import ( + "context" + "fmt" + + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/gitea" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/log" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/params" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/to" + + gitea_sdk "code.gitea.io/sdk/gitea" + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" +) + +const ( + ListEmailsToolName = "list_emails" + AddEmailToolName = "add_email" + DeleteEmailToolName = "delete_email" +) + +var ( + ListEmailsTool = mcp.NewTool( + ListEmailsToolName, + mcp.WithDescription("List the authenticated user's email addresses"), + mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)), + mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)), + ) + + AddEmailTool = mcp.NewTool( + AddEmailToolName, + mcp.WithDescription("Add email addresses for the authenticated user"), + mcp.WithArray("emails", mcp.Required(), mcp.Description("email addresses to add"), mcp.Items(map[string]any{"type": "string"})), + ) + + DeleteEmailTool = mcp.NewTool( + DeleteEmailToolName, + mcp.WithDescription("Delete email addresses for the authenticated user"), + mcp.WithArray("emails", mcp.Required(), mcp.Description("email addresses to delete"), mcp.Items(map[string]any{"type": "string"})), + ) +) + +func init() { + Tool.RegisterRead(server.ServerTool{Tool: ListEmailsTool, Handler: ListEmailsFn}) + Tool.RegisterWrite(server.ServerTool{Tool: AddEmailTool, Handler: AddEmailFn}) + Tool.RegisterWrite(server.ServerTool{Tool: DeleteEmailTool, Handler: DeleteEmailFn}) +} + +func ListEmailsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called ListEmailsFn") + page := params.GetOptionalInt(req.GetArguments(), "page", 1) + pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100) + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + emails, _, err := client.ListEmails(gitea_sdk.ListEmailsOptions{ + ListOptions: gitea_sdk.ListOptions{Page: int(page), PageSize: int(pageSize)}, + }) + if err != nil { + return to.ErrorResult(fmt.Errorf("list emails err: %v", err)) + } + return to.TextResult(emails) +} + +func AddEmailFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called AddEmailFn") + emailsArg, exists := req.GetArguments()["emails"] + if !exists { + return to.ErrorResult(fmt.Errorf("emails is required")) + } + emailsSlice, ok := emailsArg.([]any) + if !ok { + return to.ErrorResult(fmt.Errorf("emails must be an array")) + } + emails := make([]string, 0, len(emailsSlice)) + for _, e := range emailsSlice { + if s, ok := e.(string); ok { + emails = append(emails, s) + } + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + result, _, err := client.AddEmail(gitea_sdk.CreateEmailOption{Emails: emails}) + if err != nil { + return to.ErrorResult(fmt.Errorf("add emails err: %v", err)) + } + return to.TextResult(result) +} + +func DeleteEmailFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called DeleteEmailFn") + emailsArg, exists := req.GetArguments()["emails"] + if !exists { + return to.ErrorResult(fmt.Errorf("emails is required")) + } + emailsSlice, ok := emailsArg.([]any) + if !ok { + return to.ErrorResult(fmt.Errorf("emails must be an array")) + } + emails := make([]string, 0, len(emailsSlice)) + for _, e := range emailsSlice { + if s, ok := e.(string); ok { + emails = append(emails, s) + } + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + _, err = client.DeleteEmail(gitea_sdk.DeleteEmailOption{Emails: emails}) + if err != nil { + return to.ErrorResult(fmt.Errorf("delete emails err: %v", err)) + } + return to.TextResult(map[string]any{"status": "deleted", "emails": emails}) +} diff --git a/operation/user/followers.go b/operation/user/followers.go new file mode 100644 index 0000000..3cbaddd --- /dev/null +++ b/operation/user/followers.go @@ -0,0 +1,210 @@ +package user + +import ( + "context" + "errors" + "fmt" + + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/gitea" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/log" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/params" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/to" + + gitea_sdk "code.gitea.io/sdk/gitea" + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" +) + +const ( + ListMyFollowersToolName = "list_my_followers" + ListMyFollowingToolName = "list_my_following" + ListUserFollowersToolName = "list_user_followers" + ListUserFollowingToolName = "list_user_following" + CheckFollowingToolName = "check_following" + FollowUserToolName = "follow_user" + UnfollowUserToolName = "unfollow_user" +) + +var ( + ListMyFollowersTool = mcp.NewTool( + ListMyFollowersToolName, + mcp.WithDescription("List the authenticated user's followers"), + mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)), + mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)), + ) + + ListMyFollowingTool = mcp.NewTool( + ListMyFollowingToolName, + mcp.WithDescription("List users the authenticated user is following"), + mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)), + mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)), + ) + + ListUserFollowersTool = mcp.NewTool( + ListUserFollowersToolName, + mcp.WithDescription("List a user's followers"), + mcp.WithString("username", mcp.Required(), mcp.Description("username")), + mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)), + mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)), + ) + + ListUserFollowingTool = mcp.NewTool( + ListUserFollowingToolName, + mcp.WithDescription("List users a user is following"), + mcp.WithString("username", mcp.Required(), mcp.Description("username")), + mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)), + mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)), + ) + + CheckFollowingTool = mcp.NewTool( + CheckFollowingToolName, + mcp.WithDescription("Check if the authenticated user is following a user"), + mcp.WithString("username", mcp.Required(), mcp.Description("username to check")), + ) + + FollowUserTool = mcp.NewTool( + FollowUserToolName, + mcp.WithDescription("Follow a user"), + mcp.WithString("username", mcp.Required(), mcp.Description("username to follow")), + ) + + UnfollowUserTool = mcp.NewTool( + UnfollowUserToolName, + mcp.WithDescription("Unfollow a user"), + mcp.WithString("username", mcp.Required(), mcp.Description("username to unfollow")), + ) +) + +func init() { + Tool.RegisterRead(server.ServerTool{Tool: ListMyFollowersTool, Handler: ListMyFollowersFn}) + Tool.RegisterRead(server.ServerTool{Tool: ListMyFollowingTool, Handler: ListMyFollowingFn}) + Tool.RegisterRead(server.ServerTool{Tool: ListUserFollowersTool, Handler: ListUserFollowersFn}) + Tool.RegisterRead(server.ServerTool{Tool: ListUserFollowingTool, Handler: ListUserFollowingFn}) + Tool.RegisterRead(server.ServerTool{Tool: CheckFollowingTool, Handler: CheckFollowingFn}) + Tool.RegisterWrite(server.ServerTool{Tool: FollowUserTool, Handler: FollowUserFn}) + Tool.RegisterWrite(server.ServerTool{Tool: UnfollowUserTool, Handler: UnfollowUserFn}) +} + +func ListMyFollowersFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called ListMyFollowersFn") + page := params.GetOptionalInt(req.GetArguments(), "page", 1) + pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100) + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + followers, _, err := client.ListMyFollowers(gitea_sdk.ListFollowersOptions{ + ListOptions: gitea_sdk.ListOptions{Page: int(page), PageSize: int(pageSize)}, + }) + if err != nil { + return to.ErrorResult(fmt.Errorf("list my followers err: %v", err)) + } + return to.TextResult(followers) +} + +func ListMyFollowingFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called ListMyFollowingFn") + page := params.GetOptionalInt(req.GetArguments(), "page", 1) + pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100) + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + following, _, err := client.ListMyFollowing(gitea_sdk.ListFollowingOptions{ + ListOptions: gitea_sdk.ListOptions{Page: int(page), PageSize: int(pageSize)}, + }) + if err != nil { + return to.ErrorResult(fmt.Errorf("list my following err: %v", err)) + } + return to.TextResult(following) +} + +func ListUserFollowersFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called ListUserFollowersFn") + username, ok := req.GetArguments()["username"].(string) + if !ok { + return to.ErrorResult(errors.New("username is required")) + } + page := params.GetOptionalInt(req.GetArguments(), "page", 1) + pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100) + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + followers, _, err := client.ListFollowers(username, gitea_sdk.ListFollowersOptions{ + ListOptions: gitea_sdk.ListOptions{Page: int(page), PageSize: int(pageSize)}, + }) + if err != nil { + return to.ErrorResult(fmt.Errorf("list %s followers err: %v", username, err)) + } + return to.TextResult(followers) +} + +func ListUserFollowingFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called ListUserFollowingFn") + username, ok := req.GetArguments()["username"].(string) + if !ok { + return to.ErrorResult(errors.New("username is required")) + } + page := params.GetOptionalInt(req.GetArguments(), "page", 1) + pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100) + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + following, _, err := client.ListFollowing(username, gitea_sdk.ListFollowingOptions{ + ListOptions: gitea_sdk.ListOptions{Page: int(page), PageSize: int(pageSize)}, + }) + if err != nil { + return to.ErrorResult(fmt.Errorf("list %s following err: %v", username, err)) + } + return to.TextResult(following) +} + +func CheckFollowingFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called CheckFollowingFn") + username, ok := req.GetArguments()["username"].(string) + if !ok { + return to.ErrorResult(errors.New("username is required")) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + isFollowing, _ := client.IsFollowing(username) + return to.TextResult(map[string]any{"username": username, "is_following": isFollowing}) +} + +func FollowUserFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called FollowUserFn") + username, ok := req.GetArguments()["username"].(string) + if !ok { + return to.ErrorResult(errors.New("username is required")) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + _, err = client.Follow(username) + if err != nil { + return to.ErrorResult(fmt.Errorf("follow user %s err: %v", username, err)) + } + return to.TextResult(map[string]string{"status": "following", "username": username}) +} + +func UnfollowUserFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called UnfollowUserFn") + username, ok := req.GetArguments()["username"].(string) + if !ok { + return to.ErrorResult(errors.New("username is required")) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + _, err = client.Unfollow(username) + if err != nil { + return to.ErrorResult(fmt.Errorf("unfollow user %s err: %v", username, err)) + } + return to.TextResult(map[string]string{"status": "unfollowed", "username": username}) +} diff --git a/operation/user/keys.go b/operation/user/keys.go new file mode 100644 index 0000000..3a30e3c --- /dev/null +++ b/operation/user/keys.go @@ -0,0 +1,169 @@ +package user + +import ( + "context" + "errors" + "fmt" + + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/gitea" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/log" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/params" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/to" + + gitea_sdk "code.gitea.io/sdk/gitea" + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" +) + +const ( + ListMyPublicKeysToolName = "list_my_public_keys" + ListUserPublicKeysToolName = "list_user_public_keys" + GetPublicKeyToolName = "get_public_key" + CreatePublicKeyToolName = "create_public_key" + DeletePublicKeyToolName = "delete_public_key" +) + +var ( + ListMyPublicKeysTool = mcp.NewTool( + ListMyPublicKeysToolName, + mcp.WithDescription("List the authenticated user's SSH public keys"), + mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)), + mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)), + ) + + ListUserPublicKeysTool = mcp.NewTool( + ListUserPublicKeysToolName, + mcp.WithDescription("List a user's SSH public keys"), + mcp.WithString("username", mcp.Required(), mcp.Description("username")), + mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)), + mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)), + ) + + GetPublicKeyTool = mcp.NewTool( + GetPublicKeyToolName, + mcp.WithDescription("Get an SSH public key by ID"), + mcp.WithNumber("id", mcp.Required(), mcp.Description("key ID")), + ) + + CreatePublicKeyTool = mcp.NewTool( + CreatePublicKeyToolName, + mcp.WithDescription("Create an SSH public key for the authenticated user"), + mcp.WithString("title", mcp.Required(), mcp.Description("key title")), + mcp.WithString("key", mcp.Required(), mcp.Description("SSH public key content")), + mcp.WithBoolean("read_only", mcp.Description("whether the key is read-only")), + ) + + DeletePublicKeyTool = mcp.NewTool( + DeletePublicKeyToolName, + mcp.WithDescription("Delete an SSH public key"), + mcp.WithNumber("id", mcp.Required(), mcp.Description("key ID")), + ) +) + +func init() { + Tool.RegisterRead(server.ServerTool{Tool: ListMyPublicKeysTool, Handler: ListMyPublicKeysFn}) + Tool.RegisterRead(server.ServerTool{Tool: ListUserPublicKeysTool, Handler: ListUserPublicKeysFn}) + Tool.RegisterRead(server.ServerTool{Tool: GetPublicKeyTool, Handler: GetPublicKeyFn}) + Tool.RegisterWrite(server.ServerTool{Tool: CreatePublicKeyTool, Handler: CreatePublicKeyFn}) + Tool.RegisterWrite(server.ServerTool{Tool: DeletePublicKeyTool, Handler: DeletePublicKeyFn}) +} + +func ListMyPublicKeysFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called ListMyPublicKeysFn") + page := params.GetOptionalInt(req.GetArguments(), "page", 1) + pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100) + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + keys, _, err := client.ListMyPublicKeys(gitea_sdk.ListPublicKeysOptions{ + ListOptions: gitea_sdk.ListOptions{Page: int(page), PageSize: int(pageSize)}, + }) + if err != nil { + return to.ErrorResult(fmt.Errorf("list my public keys err: %v", err)) + } + return to.TextResult(keys) +} + +func ListUserPublicKeysFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called ListUserPublicKeysFn") + username, ok := req.GetArguments()["username"].(string) + if !ok { + return to.ErrorResult(errors.New("username is required")) + } + page := params.GetOptionalInt(req.GetArguments(), "page", 1) + pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100) + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + keys, _, err := client.ListPublicKeys(username, gitea_sdk.ListPublicKeysOptions{ + ListOptions: gitea_sdk.ListOptions{Page: int(page), PageSize: int(pageSize)}, + }) + if err != nil { + return to.ErrorResult(fmt.Errorf("list %s public keys err: %v", username, err)) + } + return to.TextResult(keys) +} + +func GetPublicKeyFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called GetPublicKeyFn") + 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)) + } + key, _, err := client.GetPublicKey(id) + if err != nil { + return to.ErrorResult(fmt.Errorf("get public key %d err: %v", id, err)) + } + return to.TextResult(key) +} + +func CreatePublicKeyFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called CreatePublicKeyFn") + title, ok := req.GetArguments()["title"].(string) + if !ok { + return to.ErrorResult(errors.New("title is required")) + } + key, ok := req.GetArguments()["key"].(string) + if !ok { + return to.ErrorResult(errors.New("key is required")) + } + opt := gitea_sdk.CreateKeyOption{ + Title: title, + Key: key, + } + if v, ok := req.GetArguments()["read_only"].(bool); ok { + opt.ReadOnly = v + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + pubKey, _, err := client.CreatePublicKey(opt) + if err != nil { + return to.ErrorResult(fmt.Errorf("create public key err: %v", err)) + } + return to.TextResult(pubKey) +} + +func DeletePublicKeyFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called DeletePublicKeyFn") + id, err := params.GetIndex(req.GetArguments(), "id") + if err != nil { + return to.ErrorResult(err) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + _, err = client.DeletePublicKey(id) + if err != nil { + return to.ErrorResult(fmt.Errorf("delete public key %d err: %v", id, err)) + } + return to.TextResult(map[string]any{"status": "deleted", "key_id": id}) +} diff --git a/operation/user/profile.go b/operation/user/profile.go new file mode 100644 index 0000000..afe8493 --- /dev/null +++ b/operation/user/profile.go @@ -0,0 +1,121 @@ +package user + +import ( + "context" + "errors" + "fmt" + + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/gitea" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/log" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/to" + + gitea_sdk "code.gitea.io/sdk/gitea" + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" +) + +const ( + GetUserInfoToolName = "get_user_info" + GetUserSettingsToolName = "get_user_settings" + UpdateUserSettingsToolName = "update_user_settings" +) + +var ( + GetUserInfoTool = mcp.NewTool( + GetUserInfoToolName, + mcp.WithDescription("Get a user's public information by username"), + mcp.WithString("username", mcp.Required(), mcp.Description("username")), + ) + + GetUserSettingsTool = mcp.NewTool( + GetUserSettingsToolName, + mcp.WithDescription("Get the authenticated user's settings"), + ) + + UpdateUserSettingsTool = mcp.NewTool( + UpdateUserSettingsToolName, + mcp.WithDescription("Update the authenticated user's settings"), + mcp.WithString("full_name", mcp.Description("full name")), + mcp.WithString("description", mcp.Description("user bio/description")), + mcp.WithString("website", mcp.Description("website URL")), + mcp.WithString("location", mcp.Description("location")), + mcp.WithString("language", mcp.Description("language preference")), + mcp.WithString("theme", mcp.Description("UI theme")), + mcp.WithBoolean("hide_email", mcp.Description("hide email from profile")), + mcp.WithBoolean("hide_activity", mcp.Description("hide activity from profile")), + ) +) + +func init() { + Tool.RegisterRead(server.ServerTool{Tool: GetUserInfoTool, Handler: GetUserByNameFn}) + Tool.RegisterRead(server.ServerTool{Tool: GetUserSettingsTool, Handler: GetUserSettingsFn}) + Tool.RegisterWrite(server.ServerTool{Tool: UpdateUserSettingsTool, Handler: UpdateUserSettingsFn}) +} + +func GetUserByNameFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called GetUserByNameFn") + username, ok := req.GetArguments()["username"].(string) + if !ok { + return to.ErrorResult(errors.New("username is required")) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + user, _, err := client.GetUserInfo(username) + if err != nil { + return to.ErrorResult(fmt.Errorf("get user %s err: %v", username, err)) + } + return to.TextResult(user) +} + +func GetUserSettingsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called GetUserSettingsFn") + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + settings, _, err := client.GetUserSettings() + if err != nil { + return to.ErrorResult(fmt.Errorf("get user settings err: %v", err)) + } + return to.TextResult(settings) +} + +func UpdateUserSettingsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called UpdateUserSettingsFn") + opt := gitea_sdk.UserSettingsOptions{} + if v, ok := req.GetArguments()["full_name"].(string); ok { + opt.FullName = &v + } + if v, ok := req.GetArguments()["description"].(string); ok { + opt.Description = &v + } + if v, ok := req.GetArguments()["website"].(string); ok { + opt.Website = &v + } + if v, ok := req.GetArguments()["location"].(string); ok { + opt.Location = &v + } + if v, ok := req.GetArguments()["language"].(string); ok { + opt.Language = &v + } + if v, ok := req.GetArguments()["theme"].(string); ok { + opt.Theme = &v + } + if v, ok := req.GetArguments()["hide_email"].(bool); ok { + opt.HideEmail = &v + } + if v, ok := req.GetArguments()["hide_activity"].(bool); ok { + opt.HideActivity = &v + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + settings, _, err := client.UpdateUserSettings(opt) + if err != nil { + return to.ErrorResult(fmt.Errorf("update user settings err: %v", err)) + } + return to.TextResult(settings) +} diff --git a/operation/user/repos.go b/operation/user/repos.go new file mode 100644 index 0000000..51eeec6 --- /dev/null +++ b/operation/user/repos.go @@ -0,0 +1,254 @@ +package user + +import ( + "context" + "errors" + "fmt" + + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/gitea" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/log" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/params" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/to" + + gitea_sdk "code.gitea.io/sdk/gitea" + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" +) + +const ( + ListUserReposToolName = "list_user_repos" + ListMyStarredToolName = "list_my_starred" + StarRepoToolName = "star_repo" + UnstarRepoToolName = "unstar_repo" + CheckStarredToolName = "check_starred" + ListMySubscriptionsToolName = "list_my_subscriptions" + ListUserHeatmapToolName = "list_user_heatmap" + ListUserActivityToolName = "list_user_activity" +) + +var ( + ListUserReposTool = mcp.NewTool( + ListUserReposToolName, + mcp.WithDescription("List a user's repositories"), + mcp.WithString("username", mcp.Required(), mcp.Description("username")), + mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)), + mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)), + ) + + ListMyStarredTool = mcp.NewTool( + ListMyStarredToolName, + mcp.WithDescription("List repositories starred by the authenticated user"), + mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)), + mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)), + ) + + StarRepoTool = mcp.NewTool( + StarRepoToolName, + mcp.WithDescription("Star a repository"), + mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), + mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), + ) + + UnstarRepoTool = mcp.NewTool( + UnstarRepoToolName, + mcp.WithDescription("Unstar a repository"), + mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), + mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), + ) + + CheckStarredTool = mcp.NewTool( + CheckStarredToolName, + mcp.WithDescription("Check if the authenticated user has starred a repository"), + mcp.WithString("owner", mcp.Required(), mcp.Description("repository owner")), + mcp.WithString("repo", mcp.Required(), mcp.Description("repository name")), + ) + + ListMySubscriptionsTool = mcp.NewTool( + ListMySubscriptionsToolName, + mcp.WithDescription("List repositories watched by the authenticated user"), + ) + + ListUserHeatmapTool = mcp.NewTool( + ListUserHeatmapToolName, + mcp.WithDescription("Get a user's contribution heatmap data"), + mcp.WithString("username", mcp.Required(), mcp.Description("username")), + ) + + ListUserActivityTool = mcp.NewTool( + ListUserActivityToolName, + mcp.WithDescription("List a user's activity feeds"), + mcp.WithString("username", mcp.Required(), mcp.Description("username")), + mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)), + mcp.WithNumber("pageSize", mcp.Description("page size"), mcp.DefaultNumber(100)), + ) +) + +func init() { + Tool.RegisterRead(server.ServerTool{Tool: ListUserReposTool, Handler: ListUserReposFn}) + Tool.RegisterRead(server.ServerTool{Tool: ListMyStarredTool, Handler: ListMyStarredFn}) + Tool.RegisterRead(server.ServerTool{Tool: CheckStarredTool, Handler: CheckStarredFn}) + Tool.RegisterRead(server.ServerTool{Tool: ListMySubscriptionsTool, Handler: ListMySubscriptionsFn}) + Tool.RegisterRead(server.ServerTool{Tool: ListUserHeatmapTool, Handler: ListUserHeatmapFn}) + Tool.RegisterRead(server.ServerTool{Tool: ListUserActivityTool, Handler: ListUserActivityFn}) + Tool.RegisterWrite(server.ServerTool{Tool: StarRepoTool, Handler: StarRepoFn}) + Tool.RegisterWrite(server.ServerTool{Tool: UnstarRepoTool, Handler: UnstarRepoFn}) +} + +func ListUserReposFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called ListUserReposFn") + username, ok := req.GetArguments()["username"].(string) + if !ok { + return to.ErrorResult(errors.New("username is required")) + } + page := params.GetOptionalInt(req.GetArguments(), "page", 1) + pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100) + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + repos, _, err := client.ListUserRepos(username, gitea_sdk.ListReposOptions{ + ListOptions: gitea_sdk.ListOptions{Page: int(page), PageSize: int(pageSize)}, + }) + if err != nil { + return to.ErrorResult(fmt.Errorf("list %s repos err: %v", username, err)) + } + return to.TextResult(repos) +} + +func ListMyStarredFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called ListMyStarredFn") + page := params.GetOptionalInt(req.GetArguments(), "page", 1) + pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100) + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + repos, _, err := client.GetMyStarredRepos() + if err != nil { + return to.ErrorResult(fmt.Errorf("list my starred repos err: %v", err)) + } + // Manual pagination since SDK returns all + start := (int(page) - 1) * int(pageSize) + if start >= len(repos) { + return to.TextResult([]*gitea_sdk.Repository{}) + } + end := start + int(pageSize) + if end > len(repos) { + end = len(repos) + } + return to.TextResult(repos[start:end]) +} + +func StarRepoFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called StarRepoFn") + owner, ok := req.GetArguments()["owner"].(string) + if !ok { + return to.ErrorResult(errors.New("owner is required")) + } + repo, ok := req.GetArguments()["repo"].(string) + if !ok { + return to.ErrorResult(errors.New("repo is required")) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + _, err = client.StarRepo(owner, repo) + if err != nil { + return to.ErrorResult(fmt.Errorf("star %s/%s err: %v", owner, repo, err)) + } + return to.TextResult(map[string]string{"status": "starred", "owner": owner, "repo": repo}) +} + +func UnstarRepoFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called UnstarRepoFn") + owner, ok := req.GetArguments()["owner"].(string) + if !ok { + return to.ErrorResult(errors.New("owner is required")) + } + repo, ok := req.GetArguments()["repo"].(string) + if !ok { + return to.ErrorResult(errors.New("repo is required")) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + _, err = client.UnStarRepo(owner, repo) + if err != nil { + return to.ErrorResult(fmt.Errorf("unstar %s/%s err: %v", owner, repo, err)) + } + return to.TextResult(map[string]string{"status": "unstarred", "owner": owner, "repo": repo}) +} + +func CheckStarredFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called CheckStarredFn") + owner, ok := req.GetArguments()["owner"].(string) + if !ok { + return to.ErrorResult(errors.New("owner is required")) + } + repo, ok := req.GetArguments()["repo"].(string) + if !ok { + return to.ErrorResult(errors.New("repo is required")) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + isStarred, _, err := client.IsRepoStarring(owner, repo) + if err != nil { + return to.ErrorResult(fmt.Errorf("check starred %s/%s err: %v", owner, repo, err)) + } + return to.TextResult(map[string]any{"owner": owner, "repo": repo, "is_starred": isStarred}) +} + +func ListMySubscriptionsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called ListMySubscriptionsFn") + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + repos, _, err := client.GetMyWatchedRepos() + if err != nil { + return to.ErrorResult(fmt.Errorf("list my subscriptions err: %v", err)) + } + return to.TextResult(repos) +} + +func ListUserHeatmapFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called ListUserHeatmapFn") + username, ok := req.GetArguments()["username"].(string) + if !ok { + return to.ErrorResult(errors.New("username is required")) + } + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + heatmap, _, err := client.GetUserHeatmap(username) + if err != nil { + return to.ErrorResult(fmt.Errorf("get %s heatmap err: %v", username, err)) + } + return to.TextResult(heatmap) +} + +func ListUserActivityFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + log.Debugf("Called ListUserActivityFn") + username, ok := req.GetArguments()["username"].(string) + if !ok { + return to.ErrorResult(errors.New("username is required")) + } + page := params.GetOptionalInt(req.GetArguments(), "page", 1) + pageSize := params.GetOptionalInt(req.GetArguments(), "pageSize", 100) + client, err := gitea.ClientFromContext(ctx) + if err != nil { + return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err)) + } + feeds, _, err := client.ListUserActivityFeeds(username, gitea_sdk.ListUserActivityFeedsOptions{ + ListOptions: gitea_sdk.ListOptions{Page: int(page), PageSize: int(pageSize)}, + }) + if err != nil { + return to.ErrorResult(fmt.Errorf("list %s activity err: %v", username, err)) + } + return to.TextResult(feeds) +} diff --git a/operation/user/user.go b/operation/user/user.go index 21447bf..7088693 100644 --- a/operation/user/user.go +++ b/operation/user/user.go @@ -4,11 +4,11 @@ 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" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/gitea" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/log" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/params" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/to" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/tool" gitea_sdk "code.gitea.io/sdk/gitea" "github.com/mark3labs/mcp-go/mcp" diff --git a/operation/version/version.go b/operation/version/version.go index 72907b5..16325d8 100644 --- a/operation/version/version.go +++ b/operation/version/version.go @@ -4,10 +4,10 @@ import ( "context" "fmt" - "gitea.com/gitea/gitea-mcp/pkg/flag" - "gitea.com/gitea/gitea-mcp/pkg/log" - "gitea.com/gitea/gitea-mcp/pkg/to" - "gitea.com/gitea/gitea-mcp/pkg/tool" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/flag" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/log" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/to" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/tool" "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" diff --git a/operation/wiki/wiki.go b/operation/wiki/wiki.go index c0bf9e8..007198d 100644 --- a/operation/wiki/wiki.go +++ b/operation/wiki/wiki.go @@ -6,10 +6,10 @@ import ( "fmt" "net/url" - "gitea.com/gitea/gitea-mcp/pkg/gitea" - "gitea.com/gitea/gitea-mcp/pkg/log" - "gitea.com/gitea/gitea-mcp/pkg/to" - "gitea.com/gitea/gitea-mcp/pkg/tool" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/gitea" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/log" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/to" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/tool" "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" diff --git a/pkg/gitea/gitea.go b/pkg/gitea/gitea.go index 3f3a4d7..e16dc5b 100644 --- a/pkg/gitea/gitea.go +++ b/pkg/gitea/gitea.go @@ -7,8 +7,8 @@ import ( "net/http" "code.gitea.io/sdk/gitea" - mcpContext "gitea.com/gitea/gitea-mcp/pkg/context" - "gitea.com/gitea/gitea-mcp/pkg/flag" + mcpContext "git.lethalbits.com/lethalbits/gitea-mcp/pkg/context" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/flag" ) func NewClient(token string) (*gitea.Client, error) { diff --git a/pkg/gitea/rest.go b/pkg/gitea/rest.go index 982dfef..f808c47 100644 --- a/pkg/gitea/rest.go +++ b/pkg/gitea/rest.go @@ -13,8 +13,8 @@ import ( "strings" "time" - mcpContext "gitea.com/gitea/gitea-mcp/pkg/context" - "gitea.com/gitea/gitea-mcp/pkg/flag" + mcpContext "git.lethalbits.com/lethalbits/gitea-mcp/pkg/context" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/flag" ) type HTTPError struct { diff --git a/pkg/gitea/rest_test.go b/pkg/gitea/rest_test.go index 4d3d841..24c3272 100644 --- a/pkg/gitea/rest_test.go +++ b/pkg/gitea/rest_test.go @@ -4,8 +4,8 @@ import ( "context" "testing" - mcpContext "gitea.com/gitea/gitea-mcp/pkg/context" - "gitea.com/gitea/gitea-mcp/pkg/flag" + mcpContext "git.lethalbits.com/lethalbits/gitea-mcp/pkg/context" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/flag" ) func TestTokenFromContext(t *testing.T) { diff --git a/pkg/log/log.go b/pkg/log/log.go index caeb4bb..3bdf694 100644 --- a/pkg/log/log.go +++ b/pkg/log/log.go @@ -5,7 +5,7 @@ import ( "sync" "time" - "gitea.com/gitea/gitea-mcp/pkg/flag" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/flag" "go.uber.org/zap" "go.uber.org/zap/zapcore" "gopkg.in/natefinch/lumberjack.v2" diff --git a/pkg/to/to.go b/pkg/to/to.go index 9622c80..8b1358f 100644 --- a/pkg/to/to.go +++ b/pkg/to/to.go @@ -4,7 +4,7 @@ import ( "encoding/json" "fmt" - "gitea.com/gitea/gitea-mcp/pkg/log" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/log" "github.com/mark3labs/mcp-go/mcp" ) diff --git a/pkg/tool/tool.go b/pkg/tool/tool.go index c91205e..5732e0b 100644 --- a/pkg/tool/tool.go +++ b/pkg/tool/tool.go @@ -1,7 +1,7 @@ package tool import ( - "gitea.com/gitea/gitea-mcp/pkg/flag" + "git.lethalbits.com/lethalbits/gitea-mcp/pkg/flag" "github.com/mark3labs/mcp-go/server" )