package tools import ( "context" "encoding/json" "fmt" "strings" "gitea.d-ma.be/mathias/gitea-mcp/internal/allowlist" "gitea.d-ma.be/mathias/gitea-mcp/internal/gitea" "gitea.d-ma.be/mathias/gitea-mcp/internal/registry" ) type RepoSearch struct { c *gitea.Client a *allowlist.Allowlist } func NewRepoSearch(c *gitea.Client, a *allowlist.Allowlist) *RepoSearch { return &RepoSearch{c: c, a: a} } func (t *RepoSearch) Descriptor() registry.ToolDescriptor { return registry.ToolDescriptor{ Name: "repo_search", Description: "Search repos by query string. Filters results by owner allowlist.", InputSchema: json.RawMessage(`{ "type":"object", "properties":{ "q":{"type":"string"}, "owner":{"type":"string"}, "page":{"type":"integer","minimum":1}, "limit":{"type":"integer","minimum":1,"maximum":50} }, "required":["q"] }`), } } type repoSearchArgs struct { Q string `json:"q"` Owner string `json:"owner"` Page int `json:"page"` Limit int `json:"limit"` } func (t *RepoSearch) Call(ctx context.Context, raw json.RawMessage) (json.RawMessage, error) { var args repoSearchArgs if err := parseArgs(raw, &args); err != nil { return nil, err } if args.Q == "" { return nil, fmt.Errorf("q is required: %w", gitea.ErrValidation) } if args.Owner != "" { if err := t.a.Check(args.Owner); err != nil { return nil, err } } if args.Page < 1 { args.Page = 1 } args.Limit = capLimit(args.Limit, 30) repos, err := t.c.SearchRepos(ctx, args.Q, args.Owner, args.Page, args.Limit) if err != nil { return nil, err } // Post-filter when owner not specified — only allowlisted owners survive. if args.Owner == "" { filtered := make([]gitea.Repo, 0, len(repos)) for _, r := range repos { parts := strings.SplitN(r.FullName, "/", 2) if len(parts) != 2 { continue } if t.a.Check(parts[0]) == nil { filtered = append(filtered, r) } } repos = filtered } out := map[string]any{"repos": repos} if len(repos) == args.Limit { out["next_page"] = args.Page + 1 } return textOK(out) }