Files
github--github-mcp-server/pkg/github/search.go
copilot-swe-agent[bot] ad64c47110 feat: wire up all search tools to use typed inputs
- SearchRepositories: SearchRepositoriesInput (already done)
- SearchCode: SearchCodeInput
- SearchUsers: SearchUsersInput (with generic userOrOrgHandlerTyped)
- SearchOrgs: SearchOrgsInput (with generic userOrOrgHandlerTyped)

Created userOrOrgSearchInput interface with generic handler for shared
user/org search functionality. Updated all search tests to use typed inputs.
Updated toolsnaps for all search tools.

Co-authored-by: SamMorrowDrums <4811358+SamMorrowDrums@users.noreply.github.com>
2025-12-04 16:26:51 +00:00

287 lines
10 KiB
Go

package github
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
ghErrors "github.com/github/github-mcp-server/pkg/errors"
"github.com/github/github-mcp-server/pkg/translations"
"github.com/github/github-mcp-server/pkg/utils"
"github.com/google/go-github/v79/github"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
// SearchRepositories creates a tool to search for GitHub repositories.
func SearchRepositories(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[SearchRepositoriesInput, any]) {
return mcp.Tool{
Name: "search_repositories",
Description: t("TOOL_SEARCH_REPOSITORIES_DESCRIPTION", "Find GitHub repositories by name, description, readme, topics, or other metadata. Perfect for discovering projects, finding examples, or locating specific repositories across GitHub."),
Annotations: &mcp.ToolAnnotations{
Title: t("TOOL_SEARCH_REPOSITORIES_USER_TITLE", "Search repositories"),
ReadOnlyHint: true,
},
InputSchema: SearchRepositoriesInput{}.MCPSchema(),
},
func(ctx context.Context, _ *mcp.CallToolRequest, input SearchRepositoriesInput) (*mcp.CallToolResult, any, error) {
opts := &github.SearchOptions{
Sort: input.Sort,
Order: input.Order,
ListOptions: github.ListOptions{
Page: input.Page,
PerPage: input.PerPage,
},
}
client, err := getClient(ctx)
if err != nil {
return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil
}
result, resp, err := client.Search.Repositories(ctx, input.Query, opts)
if err != nil {
return ghErrors.NewGitHubAPIErrorResponse(ctx,
fmt.Sprintf("failed to search repositories with query '%s'", input.Query),
resp,
err,
), nil, nil
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != http.StatusOK {
body, err := io.ReadAll(resp.Body)
if err != nil {
return utils.NewToolResultErrorFromErr("failed to read response body", err), nil, nil
}
return utils.NewToolResultError(fmt.Sprintf("failed to search repositories: %s", string(body))), nil, nil
}
// Return either minimal or full response based on parameter
var r []byte
if input.MinimalOutput {
minimalRepos := make([]MinimalRepository, 0, len(result.Repositories))
for _, repo := range result.Repositories {
minimalRepo := MinimalRepository{
ID: repo.GetID(),
Name: repo.GetName(),
FullName: repo.GetFullName(),
Description: repo.GetDescription(),
HTMLURL: repo.GetHTMLURL(),
Language: repo.GetLanguage(),
Stars: repo.GetStargazersCount(),
Forks: repo.GetForksCount(),
OpenIssues: repo.GetOpenIssuesCount(),
Private: repo.GetPrivate(),
Fork: repo.GetFork(),
Archived: repo.GetArchived(),
DefaultBranch: repo.GetDefaultBranch(),
}
if repo.UpdatedAt != nil {
minimalRepo.UpdatedAt = repo.UpdatedAt.Format("2006-01-02T15:04:05Z")
}
if repo.CreatedAt != nil {
minimalRepo.CreatedAt = repo.CreatedAt.Format("2006-01-02T15:04:05Z")
}
if repo.Topics != nil {
minimalRepo.Topics = repo.Topics
}
minimalRepos = append(minimalRepos, minimalRepo)
}
minimalResult := &MinimalSearchRepositoriesResult{
TotalCount: result.GetTotal(),
IncompleteResults: result.GetIncompleteResults(),
Items: minimalRepos,
}
r, err = json.Marshal(minimalResult)
if err != nil {
return utils.NewToolResultErrorFromErr("failed to marshal minimal response", err), nil, nil
}
} else {
r, err = json.Marshal(result)
if err != nil {
return utils.NewToolResultErrorFromErr("failed to marshal full response", err), nil, nil
}
}
return utils.NewToolResultText(string(r)), nil, nil
}
}
// SearchCode creates a tool to search for code across GitHub repositories.
func SearchCode(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[SearchCodeInput, any]) {
return mcp.Tool{
Name: "search_code",
Description: t("TOOL_SEARCH_CODE_DESCRIPTION", "Fast and precise code search across ALL GitHub repositories using GitHub's native search engine. Best for finding exact symbols, functions, classes, or specific code patterns."),
Annotations: &mcp.ToolAnnotations{
Title: t("TOOL_SEARCH_CODE_USER_TITLE", "Search code"),
ReadOnlyHint: true,
},
InputSchema: SearchCodeInput{}.MCPSchema(),
},
func(ctx context.Context, _ *mcp.CallToolRequest, input SearchCodeInput) (*mcp.CallToolResult, any, error) {
opts := &github.SearchOptions{
Sort: input.Sort,
Order: input.Order,
ListOptions: github.ListOptions{
PerPage: input.PerPage,
Page: input.Page,
},
}
client, err := getClient(ctx)
if err != nil {
return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil
}
result, resp, err := client.Search.Code(ctx, input.Query, opts)
if err != nil {
return ghErrors.NewGitHubAPIErrorResponse(ctx,
fmt.Sprintf("failed to search code with query '%s'", input.Query),
resp,
err,
), nil, nil
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != http.StatusOK {
body, err := io.ReadAll(resp.Body)
if err != nil {
return utils.NewToolResultErrorFromErr("failed to read response body", err), nil, nil
}
return utils.NewToolResultError(fmt.Sprintf("failed to search code: %s", string(body))), nil, nil
}
r, err := json.Marshal(result)
if err != nil {
return utils.NewToolResultErrorFromErr("failed to marshal response", err), nil, nil
}
return utils.NewToolResultText(string(r)), nil, nil
}
}
// userOrOrgSearchInput is a common interface for user/org search inputs
type userOrOrgSearchInput interface {
GetQuery() string
GetSort() string
GetOrder() string
GetPage() int
GetPerPage() int
}
// Add getters to SearchUsersInput
func (s SearchUsersInput) GetQuery() string { return s.Query }
func (s SearchUsersInput) GetSort() string { return s.Sort }
func (s SearchUsersInput) GetOrder() string { return s.Order }
func (s SearchUsersInput) GetPage() int { return s.Page }
func (s SearchUsersInput) GetPerPage() int { return s.PerPage }
// Add getters to SearchOrgsInput
func (s SearchOrgsInput) GetQuery() string { return s.Query }
func (s SearchOrgsInput) GetSort() string { return s.Sort }
func (s SearchOrgsInput) GetOrder() string { return s.Order }
func (s SearchOrgsInput) GetPage() int { return s.Page }
func (s SearchOrgsInput) GetPerPage() int { return s.PerPage }
func userOrOrgHandlerTyped[T userOrOrgSearchInput](accountType string, getClient GetClientFn) mcp.ToolHandlerFor[T, any] {
return func(ctx context.Context, _ *mcp.CallToolRequest, input T) (*mcp.CallToolResult, any, error) {
opts := &github.SearchOptions{
Sort: input.GetSort(),
Order: input.GetOrder(),
ListOptions: github.ListOptions{
PerPage: input.GetPerPage(),
Page: input.GetPage(),
},
}
client, err := getClient(ctx)
if err != nil {
return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil
}
searchQuery := input.GetQuery()
if !hasTypeFilter(searchQuery) {
searchQuery = "type:" + accountType + " " + searchQuery
}
result, resp, err := client.Search.Users(ctx, searchQuery, opts)
if err != nil {
return ghErrors.NewGitHubAPIErrorResponse(ctx,
fmt.Sprintf("failed to search %ss with query '%s'", accountType, input.GetQuery()),
resp,
err,
), nil, nil
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != http.StatusOK {
body, err := io.ReadAll(resp.Body)
if err != nil {
return utils.NewToolResultErrorFromErr("failed to read response body", err), nil, nil
}
return utils.NewToolResultError(fmt.Sprintf("failed to search %ss: %s", accountType, string(body))), nil, nil
}
minimalUsers := make([]MinimalUser, 0, len(result.Users))
for _, user := range result.Users {
if user.Login != nil {
mu := MinimalUser{
Login: user.GetLogin(),
ID: user.GetID(),
ProfileURL: user.GetHTMLURL(),
AvatarURL: user.GetAvatarURL(),
}
minimalUsers = append(minimalUsers, mu)
}
}
minimalResp := &MinimalSearchUsersResult{
TotalCount: result.GetTotal(),
IncompleteResults: result.GetIncompleteResults(),
Items: minimalUsers,
}
if result.Total != nil {
minimalResp.TotalCount = *result.Total
}
if result.IncompleteResults != nil {
minimalResp.IncompleteResults = *result.IncompleteResults
}
r, err := json.Marshal(minimalResp)
if err != nil {
return utils.NewToolResultErrorFromErr("failed to marshal response", err), nil, nil
}
return utils.NewToolResultText(string(r)), nil, nil
}
}
// SearchUsers creates a tool to search for GitHub users.
func SearchUsers(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[SearchUsersInput, any]) {
return mcp.Tool{
Name: "search_users",
Description: t("TOOL_SEARCH_USERS_DESCRIPTION", "Find GitHub users by username, real name, or other profile information. Useful for locating developers, contributors, or team members."),
Annotations: &mcp.ToolAnnotations{
Title: t("TOOL_SEARCH_USERS_USER_TITLE", "Search users"),
ReadOnlyHint: true,
},
InputSchema: SearchUsersInput{}.MCPSchema(),
}, userOrOrgHandlerTyped[SearchUsersInput]("user", getClient)
}
// SearchOrgs creates a tool to search for GitHub organizations.
func SearchOrgs(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, mcp.ToolHandlerFor[SearchOrgsInput, any]) {
return mcp.Tool{
Name: "search_orgs",
Description: t("TOOL_SEARCH_ORGS_DESCRIPTION", "Find GitHub organizations by name, location, or other organization metadata. Ideal for discovering companies, open source foundations, or teams."),
Annotations: &mcp.ToolAnnotations{
Title: t("TOOL_SEARCH_ORGS_USER_TITLE", "Search organizations"),
ReadOnlyHint: true,
},
InputSchema: SearchOrgsInput{}.MCPSchema(),
}, userOrOrgHandlerTyped[SearchOrgsInput]("org", getClient)
}