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) }