- errorlint: fmt.Errorf %v→%w across all handlers (errors now wrap the underlying SDK/HTTP error for errors.Is/As); errors.Is for context.Canceled in cmd - staticcheck quickfixes: Fprintf over WriteString(Sprintf), tagged switches, omit inferred 'any'; QF1001 resolved by hoisting hex checks into isHex predicates (normalizeColor, validateSHA) - govet: reflect.Ptr → reflect.Pointer in pkg/ptr - errcheck: explicit '_ =' for fs.Parse (ExitOnError) and deferred zap Sync - repair four test assertions the autofix broke (it dropped the 're.Code != X' half of combined type-assert checks, inverting the test); all errors.As sites use 'var re *resource.ResourceError' golangci-lint: 0 issues. staticcheck, vet, tests: clean.
248 lines
7.9 KiB
Go
248 lines
7.9 KiB
Go
package org
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"codeberg.org/goern/forgejo-mcp/v2/operation/params"
|
|
"codeberg.org/goern/forgejo-mcp/v2/pkg/forgejo"
|
|
"codeberg.org/goern/forgejo-mcp/v2/pkg/log"
|
|
"codeberg.org/goern/forgejo-mcp/v2/pkg/to"
|
|
forgejo_sdk "codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v3"
|
|
|
|
"github.com/mark3labs/mcp-go/mcp"
|
|
)
|
|
|
|
const (
|
|
ListOrgTeamsToolName = "list_org_teams"
|
|
CreateOrgTeamToolName = "create_org_team"
|
|
AddTeamMemberToolName = "add_team_member"
|
|
RemoveTeamMemberToolName = "remove_team_member"
|
|
AddTeamRepoToolName = "add_team_repo"
|
|
RemoveTeamRepoToolName = "remove_team_repo"
|
|
)
|
|
|
|
var (
|
|
ListOrgTeamsTool = mcp.NewTool(
|
|
ListOrgTeamsToolName,
|
|
mcp.WithDescription("List teams in an organization"),
|
|
mcp.WithString("org", mcp.Required(), mcp.Description(params.Org)),
|
|
mcp.WithNumber("page", mcp.Required(), mcp.Description(params.Page), mcp.DefaultNumber(1), mcp.Min(1)),
|
|
mcp.WithNumber("limit", mcp.Required(), mcp.Description(params.Limit), mcp.DefaultNumber(100), mcp.Min(1)),
|
|
)
|
|
|
|
CreateOrgTeamTool = mcp.NewTool(
|
|
CreateOrgTeamToolName,
|
|
mcp.WithDescription("Create a team in an organization"),
|
|
mcp.WithString("org", mcp.Required(), mcp.Description(params.Org)),
|
|
mcp.WithString("name", mcp.Required(), mcp.Description("Team name")),
|
|
mcp.WithString("description", mcp.Description(params.Description)),
|
|
mcp.WithString("permission", mcp.Description("Access level: read, write, or admin (default: read)")),
|
|
mcp.WithBoolean("can_create_org_repo", mcp.Description("Whether members can create repos in the org")),
|
|
mcp.WithBoolean("includes_all_repositories", mcp.Description("Whether team has access to all org repos")),
|
|
)
|
|
|
|
AddTeamMemberTool = mcp.NewTool(
|
|
AddTeamMemberToolName,
|
|
mcp.WithDescription("Add a user to a team"),
|
|
mcp.WithNumber("team_id", mcp.Required(), mcp.Description("Team ID")),
|
|
mcp.WithString("user", mcp.Required(), mcp.Description(params.User)),
|
|
)
|
|
|
|
RemoveTeamMemberTool = mcp.NewTool(
|
|
RemoveTeamMemberToolName,
|
|
mcp.WithDescription("Remove a user from a team"),
|
|
mcp.WithNumber("team_id", mcp.Required(), mcp.Description("Team ID")),
|
|
mcp.WithString("user", mcp.Required(), mcp.Description(params.User)),
|
|
)
|
|
|
|
AddTeamRepoTool = mcp.NewTool(
|
|
AddTeamRepoToolName,
|
|
mcp.WithDescription("Add a repository to a team"),
|
|
mcp.WithNumber("team_id", mcp.Required(), mcp.Description("Team ID")),
|
|
mcp.WithString("org", mcp.Required(), mcp.Description(params.Org)),
|
|
mcp.WithString("repo", mcp.Required(), mcp.Description(params.Repo)),
|
|
)
|
|
|
|
RemoveTeamRepoTool = mcp.NewTool(
|
|
RemoveTeamRepoToolName,
|
|
mcp.WithDescription("Remove a repository from a team"),
|
|
mcp.WithNumber("team_id", mcp.Required(), mcp.Description("Team ID")),
|
|
mcp.WithString("org", mcp.Required(), mcp.Description(params.Org)),
|
|
mcp.WithString("repo", mcp.Required(), mcp.Description(params.Repo)),
|
|
)
|
|
)
|
|
|
|
func ListOrgTeamsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
log.Debugf("Called ListOrgTeamsFn")
|
|
orgName, ok := req.GetArguments()["org"].(string)
|
|
if !ok {
|
|
return to.ErrorResult(errors.New("organization name is required"))
|
|
}
|
|
page, ok := req.GetArguments()["page"].(float64)
|
|
if !ok {
|
|
page = 1
|
|
}
|
|
limit, ok := req.GetArguments()["limit"].(float64)
|
|
if !ok {
|
|
limit = 100
|
|
}
|
|
|
|
opt := forgejo_sdk.ListTeamsOptions{
|
|
ListOptions: forgejo_sdk.ListOptions{
|
|
Page: int(page),
|
|
PageSize: int(limit),
|
|
},
|
|
}
|
|
client, err := forgejo.Client(ctx)
|
|
if err != nil {
|
|
return to.ErrorResult(err)
|
|
}
|
|
teams, _, err := client.ListOrgTeams(orgName, opt)
|
|
if err != nil {
|
|
return to.ErrorResult(fmt.Errorf("list org teams err: %w", err))
|
|
}
|
|
return to.TextResult(teams)
|
|
}
|
|
|
|
func CreateOrgTeamFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
log.Debugf("Called CreateOrgTeamFn")
|
|
orgName, ok := req.GetArguments()["org"].(string)
|
|
if !ok {
|
|
return to.ErrorResult(errors.New("organization name is required"))
|
|
}
|
|
name, ok := req.GetArguments()["name"].(string)
|
|
if !ok {
|
|
return to.ErrorResult(errors.New("team name is required"))
|
|
}
|
|
|
|
permission := forgejo_sdk.AccessModeRead
|
|
if v, ok := req.GetArguments()["permission"].(string); ok && v != "" {
|
|
permission = forgejo_sdk.AccessMode(v)
|
|
}
|
|
|
|
opt := forgejo_sdk.CreateTeamOption{
|
|
Name: name,
|
|
Permission: permission,
|
|
}
|
|
if v, ok := req.GetArguments()["description"].(string); ok {
|
|
opt.Description = 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 := forgejo.Client(ctx)
|
|
if err != nil {
|
|
return to.ErrorResult(err)
|
|
}
|
|
team, _, err := client.CreateTeam(orgName, opt)
|
|
if err != nil {
|
|
return to.ErrorResult(fmt.Errorf("create org team err: %w", err))
|
|
}
|
|
return to.TextResult(team)
|
|
}
|
|
|
|
func AddTeamMemberFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
log.Debugf("Called AddTeamMemberFn")
|
|
teamIDFloat, ok := req.GetArguments()["team_id"].(float64)
|
|
if !ok {
|
|
return to.ErrorResult(errors.New("team_id is required"))
|
|
}
|
|
teamID := int64(teamIDFloat)
|
|
user, ok := req.GetArguments()["user"].(string)
|
|
if !ok {
|
|
return to.ErrorResult(errors.New("username is required"))
|
|
}
|
|
|
|
client, err := forgejo.Client(ctx)
|
|
if err != nil {
|
|
return to.ErrorResult(err)
|
|
}
|
|
_, err = client.AddTeamMember(teamID, user)
|
|
if err != nil {
|
|
return to.ErrorResult(fmt.Errorf("add team member err: %w", err))
|
|
}
|
|
return to.TextResult(fmt.Sprintf("User '%s' added to team %d", user, teamID))
|
|
}
|
|
|
|
func RemoveTeamMemberFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
log.Debugf("Called RemoveTeamMemberFn")
|
|
teamIDFloat, ok := req.GetArguments()["team_id"].(float64)
|
|
if !ok {
|
|
return to.ErrorResult(errors.New("team_id is required"))
|
|
}
|
|
teamID := int64(teamIDFloat)
|
|
user, ok := req.GetArguments()["user"].(string)
|
|
if !ok {
|
|
return to.ErrorResult(errors.New("username is required"))
|
|
}
|
|
|
|
client, err := forgejo.Client(ctx)
|
|
if err != nil {
|
|
return to.ErrorResult(err)
|
|
}
|
|
_, err = client.RemoveTeamMember(teamID, user)
|
|
if err != nil {
|
|
return to.ErrorResult(fmt.Errorf("remove team member err: %w", err))
|
|
}
|
|
return to.TextResult(fmt.Sprintf("User '%s' removed from team %d", user, teamID))
|
|
}
|
|
|
|
func AddTeamRepoFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
log.Debugf("Called AddTeamRepoFn")
|
|
teamIDFloat, ok := req.GetArguments()["team_id"].(float64)
|
|
if !ok {
|
|
return to.ErrorResult(errors.New("team_id is required"))
|
|
}
|
|
teamID := int64(teamIDFloat)
|
|
orgName, ok := req.GetArguments()["org"].(string)
|
|
if !ok {
|
|
return to.ErrorResult(errors.New("organization name is required"))
|
|
}
|
|
repo, ok := req.GetArguments()["repo"].(string)
|
|
if !ok {
|
|
return to.ErrorResult(errors.New("repository name is required"))
|
|
}
|
|
|
|
client, err := forgejo.Client(ctx)
|
|
if err != nil {
|
|
return to.ErrorResult(err)
|
|
}
|
|
_, err = client.AddTeamRepository(teamID, orgName, repo)
|
|
if err != nil {
|
|
return to.ErrorResult(fmt.Errorf("add team repo err: %w", err))
|
|
}
|
|
return to.TextResult(fmt.Sprintf("Repository '%s/%s' added to team %d", orgName, repo, teamID))
|
|
}
|
|
|
|
func RemoveTeamRepoFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
log.Debugf("Called RemoveTeamRepoFn")
|
|
teamIDFloat, ok := req.GetArguments()["team_id"].(float64)
|
|
if !ok {
|
|
return to.ErrorResult(errors.New("team_id is required"))
|
|
}
|
|
teamID := int64(teamIDFloat)
|
|
orgName, ok := req.GetArguments()["org"].(string)
|
|
if !ok {
|
|
return to.ErrorResult(errors.New("organization name is required"))
|
|
}
|
|
repo, ok := req.GetArguments()["repo"].(string)
|
|
if !ok {
|
|
return to.ErrorResult(errors.New("repository name is required"))
|
|
}
|
|
|
|
client, err := forgejo.Client(ctx)
|
|
if err != nil {
|
|
return to.ErrorResult(err)
|
|
}
|
|
_, err = client.RemoveTeamRepository(teamID, orgName, repo)
|
|
if err != nil {
|
|
return to.ErrorResult(fmt.Errorf("remove team repo err: %w", err))
|
|
}
|
|
return to.TextResult(fmt.Sprintf("Repository '%s/%s' removed from team %d", orgName, repo, teamID))
|
|
}
|