d52c1d4ece
* Enhance search functionality with new query filters and utility functions - Added multiple test cases for searching issues with various query filters in `issues_test.go`. - Introduced utility functions in `search_utils.go` to check for specific filters and extract repository information from queries. - Created comprehensive tests in `search_utils_test.go` to validate the new filtering logic and ensure accurate query parsing. * Refactor repository filter extraction in search utilities - Renamed `extractRepoFilter` to `hasRepoFilter` to simplify the function's purpose. - Updated test cases in `search_utils_test.go` to reflect the new function name and logic. - Adjusted tests to focus on the presence of the `repo:` filter rather than extracting owner and repo details. * Enhance search functionality with additional query filters and utility functions - Added new test cases for searching pull requests and users with various query filters in `pullrequests_test.go` and `search_test.go`. - Implemented a utility function `hasTypeFilter` in `search_utils.go` to check for the presence of `type:` filters in queries. - Updated the search handler to conditionally prepend the `type:` filter based on the presence of existing filters, improving query handling. * Updated both regex patterns to handle this edge case
310 lines
11 KiB
Go
310 lines
11 KiB
Go
package github
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
|
|
ghErrors "github.com/github/github-mcp-server/pkg/errors"
|
|
"github.com/github/github-mcp-server/pkg/translations"
|
|
"github.com/google/go-github/v74/github"
|
|
"github.com/mark3labs/mcp-go/mcp"
|
|
"github.com/mark3labs/mcp-go/server"
|
|
)
|
|
|
|
// SearchRepositories creates a tool to search for GitHub repositories.
|
|
func SearchRepositories(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
|
|
return mcp.NewTool("search_repositories",
|
|
mcp.WithDescription(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.")),
|
|
|
|
mcp.WithToolAnnotation(mcp.ToolAnnotation{
|
|
Title: t("TOOL_SEARCH_REPOSITORIES_USER_TITLE", "Search repositories"),
|
|
ReadOnlyHint: ToBoolPtr(true),
|
|
}),
|
|
mcp.WithString("query",
|
|
mcp.Required(),
|
|
mcp.Description("Repository search query. Examples: 'machine learning in:name stars:>1000 language:python', 'topic:react', 'user:facebook'. Supports advanced search syntax for precise filtering."),
|
|
),
|
|
WithPagination(),
|
|
),
|
|
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
query, err := RequiredParam[string](request, "query")
|
|
if err != nil {
|
|
return mcp.NewToolResultError(err.Error()), nil
|
|
}
|
|
pagination, err := OptionalPaginationParams(request)
|
|
if err != nil {
|
|
return mcp.NewToolResultError(err.Error()), nil
|
|
}
|
|
|
|
opts := &github.SearchOptions{
|
|
ListOptions: github.ListOptions{
|
|
Page: pagination.Page,
|
|
PerPage: pagination.PerPage,
|
|
},
|
|
}
|
|
|
|
client, err := getClient(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get GitHub client: %w", err)
|
|
}
|
|
result, resp, err := client.Search.Repositories(ctx, query, opts)
|
|
if err != nil {
|
|
return ghErrors.NewGitHubAPIErrorResponse(ctx,
|
|
fmt.Sprintf("failed to search repositories with query '%s'", query),
|
|
resp,
|
|
err,
|
|
), nil
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
if resp.StatusCode != 200 {
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read response body: %w", err)
|
|
}
|
|
return mcp.NewToolResultError(fmt.Sprintf("failed to search repositories: %s", string(body))), nil
|
|
}
|
|
|
|
r, err := json.Marshal(result)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to marshal response: %w", err)
|
|
}
|
|
|
|
return mcp.NewToolResultText(string(r)), nil
|
|
}
|
|
}
|
|
|
|
// SearchCode creates a tool to search for code across GitHub repositories.
|
|
func SearchCode(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
|
|
return mcp.NewTool("search_code",
|
|
mcp.WithDescription(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.")),
|
|
mcp.WithToolAnnotation(mcp.ToolAnnotation{
|
|
Title: t("TOOL_SEARCH_CODE_USER_TITLE", "Search code"),
|
|
ReadOnlyHint: ToBoolPtr(true),
|
|
}),
|
|
mcp.WithString("query",
|
|
mcp.Required(),
|
|
mcp.Description("Search query using GitHub's powerful code search syntax. Examples: 'content:Skill language:Java org:github', 'NOT is:archived language:Python OR language:go', 'repo:github/github-mcp-server'. Supports exact matching, language filters, path filters, and more."),
|
|
),
|
|
mcp.WithString("sort",
|
|
mcp.Description("Sort field ('indexed' only)"),
|
|
),
|
|
mcp.WithString("order",
|
|
mcp.Description("Sort order for results"),
|
|
mcp.Enum("asc", "desc"),
|
|
),
|
|
WithPagination(),
|
|
),
|
|
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
query, err := RequiredParam[string](request, "query")
|
|
if err != nil {
|
|
return mcp.NewToolResultError(err.Error()), nil
|
|
}
|
|
sort, err := OptionalParam[string](request, "sort")
|
|
if err != nil {
|
|
return mcp.NewToolResultError(err.Error()), nil
|
|
}
|
|
order, err := OptionalParam[string](request, "order")
|
|
if err != nil {
|
|
return mcp.NewToolResultError(err.Error()), nil
|
|
}
|
|
pagination, err := OptionalPaginationParams(request)
|
|
if err != nil {
|
|
return mcp.NewToolResultError(err.Error()), nil
|
|
}
|
|
|
|
opts := &github.SearchOptions{
|
|
Sort: sort,
|
|
Order: order,
|
|
ListOptions: github.ListOptions{
|
|
PerPage: pagination.PerPage,
|
|
Page: pagination.Page,
|
|
},
|
|
}
|
|
|
|
client, err := getClient(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get GitHub client: %w", err)
|
|
}
|
|
|
|
result, resp, err := client.Search.Code(ctx, query, opts)
|
|
if err != nil {
|
|
return ghErrors.NewGitHubAPIErrorResponse(ctx,
|
|
fmt.Sprintf("failed to search code with query '%s'", query),
|
|
resp,
|
|
err,
|
|
), nil
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
if resp.StatusCode != 200 {
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read response body: %w", err)
|
|
}
|
|
return mcp.NewToolResultError(fmt.Sprintf("failed to search code: %s", string(body))), nil
|
|
}
|
|
|
|
r, err := json.Marshal(result)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to marshal response: %w", err)
|
|
}
|
|
|
|
return mcp.NewToolResultText(string(r)), nil
|
|
}
|
|
}
|
|
|
|
// MinimalUser is the output type for user and organization search results.
|
|
type MinimalUser struct {
|
|
Login string `json:"login"`
|
|
ID int64 `json:"id,omitempty"`
|
|
ProfileURL string `json:"profile_url,omitempty"`
|
|
AvatarURL string `json:"avatar_url,omitempty"`
|
|
Details *UserDetails `json:"details,omitempty"` // Optional field for additional user details
|
|
}
|
|
|
|
type MinimalSearchUsersResult struct {
|
|
TotalCount int `json:"total_count"`
|
|
IncompleteResults bool `json:"incomplete_results"`
|
|
Items []MinimalUser `json:"items"`
|
|
}
|
|
|
|
func userOrOrgHandler(accountType string, getClient GetClientFn) server.ToolHandlerFunc {
|
|
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
query, err := RequiredParam[string](request, "query")
|
|
if err != nil {
|
|
return mcp.NewToolResultError(err.Error()), nil
|
|
}
|
|
sort, err := OptionalParam[string](request, "sort")
|
|
if err != nil {
|
|
return mcp.NewToolResultError(err.Error()), nil
|
|
}
|
|
order, err := OptionalParam[string](request, "order")
|
|
if err != nil {
|
|
return mcp.NewToolResultError(err.Error()), nil
|
|
}
|
|
pagination, err := OptionalPaginationParams(request)
|
|
if err != nil {
|
|
return mcp.NewToolResultError(err.Error()), nil
|
|
}
|
|
|
|
opts := &github.SearchOptions{
|
|
Sort: sort,
|
|
Order: order,
|
|
ListOptions: github.ListOptions{
|
|
PerPage: pagination.PerPage,
|
|
Page: pagination.Page,
|
|
},
|
|
}
|
|
|
|
client, err := getClient(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get GitHub client: %w", err)
|
|
}
|
|
|
|
searchQuery := query
|
|
if !hasTypeFilter(query) {
|
|
searchQuery = "type:" + accountType + " " + query
|
|
}
|
|
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, query),
|
|
resp,
|
|
err,
|
|
), nil
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
if resp.StatusCode != 200 {
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read response body: %w", err)
|
|
}
|
|
return mcp.NewToolResultError(fmt.Sprintf("failed to search %ss: %s", accountType, string(body))), 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 nil, fmt.Errorf("failed to marshal response: %w", err)
|
|
}
|
|
return mcp.NewToolResultText(string(r)), nil
|
|
}
|
|
}
|
|
|
|
// SearchUsers creates a tool to search for GitHub users.
|
|
func SearchUsers(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
|
|
return mcp.NewTool("search_users",
|
|
mcp.WithDescription(t("TOOL_SEARCH_USERS_DESCRIPTION", "Find GitHub users by username, real name, or other profile information. Useful for locating developers, contributors, or team members.")),
|
|
mcp.WithToolAnnotation(mcp.ToolAnnotation{
|
|
Title: t("TOOL_SEARCH_USERS_USER_TITLE", "Search users"),
|
|
ReadOnlyHint: ToBoolPtr(true),
|
|
}),
|
|
mcp.WithString("query",
|
|
mcp.Required(),
|
|
mcp.Description("User search query. Examples: 'john smith', 'location:seattle', 'followers:>100'. Search is automatically scoped to type:user."),
|
|
),
|
|
mcp.WithString("sort",
|
|
mcp.Description("Sort users by number of followers or repositories, or when the person joined GitHub."),
|
|
mcp.Enum("followers", "repositories", "joined"),
|
|
),
|
|
mcp.WithString("order",
|
|
mcp.Description("Sort order"),
|
|
mcp.Enum("asc", "desc"),
|
|
),
|
|
WithPagination(),
|
|
), userOrOrgHandler("user", getClient)
|
|
}
|
|
|
|
// SearchOrgs creates a tool to search for GitHub organizations.
|
|
func SearchOrgs(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
|
|
return mcp.NewTool("search_orgs",
|
|
mcp.WithDescription(t("TOOL_SEARCH_ORGS_DESCRIPTION", "Find GitHub organizations by name, location, or other organization metadata. Ideal for discovering companies, open source foundations, or teams.")),
|
|
|
|
mcp.WithToolAnnotation(mcp.ToolAnnotation{
|
|
Title: t("TOOL_SEARCH_ORGS_USER_TITLE", "Search organizations"),
|
|
ReadOnlyHint: ToBoolPtr(true),
|
|
}),
|
|
mcp.WithString("query",
|
|
mcp.Required(),
|
|
mcp.Description("Organization search query. Examples: 'microsoft', 'location:california', 'created:>=2025-01-01'. Search is automatically scoped to type:org."),
|
|
),
|
|
mcp.WithString("sort",
|
|
mcp.Description("Sort field by category"),
|
|
mcp.Enum("followers", "repositories", "joined"),
|
|
),
|
|
mcp.WithString("order",
|
|
mcp.Description("Sort order"),
|
|
mcp.Enum("asc", "desc"),
|
|
),
|
|
WithPagination(),
|
|
), userOrOrgHandler("org", getClient)
|
|
}
|