Files
gitea-mcp/operation/search/search.go
silverwind 9056a5ef27 Add get_commit, get_repository_tree, and search_issues tools (#162)
Add three new read-only tools inspired by the GitHub MCP server:

- `get_commit`: Get details of a specific commit by SHA, branch, or tag
- `get_repository_tree`: Get the file tree of a repository with optional recursive traversal, pagination, and ref support
- `search_issues`: Search issues and pull requests across all accessible repositories with filters for state, type, labels, and owner

---------

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-on: https://gitea.com/gitea/gitea-mcp/pulls/162
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-committed-by: silverwind <me@silverwind.io>
2026-03-24 17:12:58 +00:00

235 lines
7.6 KiB
Go

package search
import (
"context"
"fmt"
"strings"
"gitea.com/gitea/gitea-mcp/pkg/gitea"
"gitea.com/gitea/gitea-mcp/pkg/log"
"gitea.com/gitea/gitea-mcp/pkg/params"
"gitea.com/gitea/gitea-mcp/pkg/to"
"gitea.com/gitea/gitea-mcp/pkg/tool"
gitea_sdk "code.gitea.io/sdk/gitea"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
var Tool = tool.New()
const (
SearchUsersToolName = "search_users"
SearchOrgTeamsToolName = "search_org_teams"
SearchReposToolName = "search_repos"
SearchIssuesToolName = "search_issues"
)
var (
SearchUsersTool = mcp.NewTool(
SearchUsersToolName,
mcp.WithDescription("search users"),
mcp.WithString("keyword", mcp.Required(), mcp.Description("Keyword")),
mcp.WithNumber("page", mcp.Description("Page"), mcp.DefaultNumber(1)),
mcp.WithNumber("perPage", mcp.Description("results per page"), mcp.DefaultNumber(30)),
)
SearOrgTeamsTool = mcp.NewTool(
SearchOrgTeamsToolName,
mcp.WithDescription("search organization teams"),
mcp.WithString("org", mcp.Required(), mcp.Description("organization name")),
mcp.WithString("query", mcp.Required(), mcp.Description("search organization teams")),
mcp.WithBoolean("includeDescription", mcp.Description("include description?")),
mcp.WithNumber("page", mcp.Description("Page"), mcp.DefaultNumber(1)),
mcp.WithNumber("perPage", mcp.Description("results per page"), mcp.DefaultNumber(30)),
)
SearchReposTool = mcp.NewTool(
SearchReposToolName,
mcp.WithDescription("search repos"),
mcp.WithString("keyword", mcp.Required(), mcp.Description("Keyword")),
mcp.WithBoolean("keywordIsTopic", mcp.Description("KeywordIsTopic")),
mcp.WithBoolean("keywordInDescription", mcp.Description("KeywordInDescription")),
mcp.WithNumber("ownerID", mcp.Description("OwnerID")),
mcp.WithBoolean("isPrivate", mcp.Description("IsPrivate")),
mcp.WithBoolean("isArchived", mcp.Description("IsArchived")),
mcp.WithString("sort", mcp.Description("Sort")),
mcp.WithString("order", mcp.Description("Order")),
mcp.WithNumber("page", mcp.Description("Page"), mcp.DefaultNumber(1)),
mcp.WithNumber("perPage", mcp.Description("results per page"), mcp.DefaultNumber(30)),
)
SearchIssuesTool = mcp.NewTool(
SearchIssuesToolName,
mcp.WithDescription("Search for issues and pull requests across all accessible repositories"),
mcp.WithString("query", mcp.Required(), mcp.Description("search keyword")),
mcp.WithString("state", mcp.Description("filter by state: open, closed, all"), mcp.Enum("open", "closed", "all")),
mcp.WithString("type", mcp.Description("filter by type: issues, pulls"), mcp.Enum("issues", "pulls")),
mcp.WithString("labels", mcp.Description("comma-separated list of label names")),
mcp.WithString("owner", mcp.Description("filter by repository owner")),
mcp.WithNumber("page", mcp.Description("page number"), mcp.DefaultNumber(1)),
mcp.WithNumber("perPage", mcp.Description("results per page"), mcp.DefaultNumber(30)),
)
)
func init() {
Tool.RegisterRead(server.ServerTool{
Tool: SearchUsersTool,
Handler: UsersFn,
})
Tool.RegisterRead(server.ServerTool{
Tool: SearOrgTeamsTool,
Handler: OrgTeamsFn,
})
Tool.RegisterRead(server.ServerTool{
Tool: SearchReposTool,
Handler: ReposFn,
})
Tool.RegisterRead(server.ServerTool{
Tool: SearchIssuesTool,
Handler: IssuesFn,
})
}
func UsersFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called UsersFn")
keyword, err := params.GetString(req.GetArguments(), "keyword")
if err != nil {
return to.ErrorResult(err)
}
page, pageSize := params.GetPagination(req.GetArguments(), 30)
opt := gitea_sdk.SearchUsersOption{
KeyWord: keyword,
ListOptions: gitea_sdk.ListOptions{
Page: page,
PageSize: pageSize,
},
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
users, _, err := client.SearchUsers(opt)
if err != nil {
return to.ErrorResult(fmt.Errorf("search users err: %v", err))
}
return to.TextResult(slimUserDetails(users))
}
func OrgTeamsFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called OrgTeamsFn")
org, err := params.GetString(req.GetArguments(), "org")
if err != nil {
return to.ErrorResult(err)
}
query, err := params.GetString(req.GetArguments(), "query")
if err != nil {
return to.ErrorResult(err)
}
includeDescription, _ := req.GetArguments()["includeDescription"].(bool)
page, pageSize := params.GetPagination(req.GetArguments(), 30)
opt := gitea_sdk.SearchTeamsOptions{
Query: query,
IncludeDescription: includeDescription,
ListOptions: gitea_sdk.ListOptions{
Page: page,
PageSize: pageSize,
},
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
teams, _, err := client.SearchOrgTeams(org, &opt)
if err != nil {
return to.ErrorResult(fmt.Errorf("search organization teams error: %v", err))
}
return to.TextResult(slimTeams(teams))
}
func ReposFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ReposFn")
keyword, err := params.GetString(req.GetArguments(), "keyword")
if err != nil {
return to.ErrorResult(err)
}
keywordIsTopic, _ := req.GetArguments()["keywordIsTopic"].(bool)
keywordInDescription, _ := req.GetArguments()["keywordInDescription"].(bool)
ownerID := params.GetOptionalInt(req.GetArguments(), "ownerID", 0)
var pIsPrivate *bool
isPrivate, ok := req.GetArguments()["isPrivate"].(bool)
if ok {
pIsPrivate = new(isPrivate)
}
var pIsArchived *bool
isArchived, ok := req.GetArguments()["isArchived"].(bool)
if ok {
pIsArchived = new(isArchived)
}
sort, _ := req.GetArguments()["sort"].(string)
order, _ := req.GetArguments()["order"].(string)
page, pageSize := params.GetPagination(req.GetArguments(), 30)
opt := gitea_sdk.SearchRepoOptions{
Keyword: keyword,
KeywordIsTopic: keywordIsTopic,
KeywordInDescription: keywordInDescription,
OwnerID: ownerID,
IsPrivate: pIsPrivate,
IsArchived: pIsArchived,
Sort: sort,
Order: order,
ListOptions: gitea_sdk.ListOptions{
Page: page,
PageSize: pageSize,
},
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
repos, _, err := client.SearchRepos(opt)
if err != nil {
return to.ErrorResult(fmt.Errorf("search repos error: %v", err))
}
return to.TextResult(slimRepos(repos))
}
func IssuesFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called IssuesFn")
args := req.GetArguments()
query, err := params.GetString(args, "query")
if err != nil {
return to.ErrorResult(err)
}
page, pageSize := params.GetPagination(args, 30)
opt := gitea_sdk.ListIssueOption{
KeyWord: query,
ListOptions: gitea_sdk.ListOptions{
Page: page,
PageSize: pageSize,
},
}
if state, ok := args["state"].(string); ok {
opt.State = gitea_sdk.StateType(state)
}
if issueType, ok := args["type"].(string); ok {
opt.Type = gitea_sdk.IssueType(issueType)
}
if labels, ok := args["labels"].(string); ok && labels != "" {
opt.Labels = strings.Split(labels, ",")
}
if owner, ok := args["owner"].(string); ok {
opt.Owner = owner
}
client, err := gitea.ClientFromContext(ctx)
if err != nil {
return to.ErrorResult(fmt.Errorf("get gitea client err: %v", err))
}
issues, _, err := client.ListIssues(opt)
if err != nil {
return to.ErrorResult(fmt.Errorf("search issues err: %v", err))
}
return to.TextResult(slimIssues(issues))
}