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."), ), mcp.WithString("sort", mcp.Description("Sort repositories by field, defaults to best match"), mcp.Enum("stars", "forks", "help-wanted-issues", "updated"), ), mcp.WithString("order", mcp.Description("Sort order"), mcp.Enum("asc", "desc"), ), mcp.WithBoolean("minimal_output", mcp.Description("Return minimal repository information (default: true). When false, returns full GitHub API repository objects."), mcp.DefaultBool(true), ), 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 } minimalOutput, err := OptionalBoolParamWithDefault(request, "minimal_output", true) if err != nil { return mcp.NewToolResultError(err.Error()), nil } opts := &github.SearchOptions{ Sort: sort, Order: order, 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 } // Return either minimal or full response based on parameter var r []byte if 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 nil, fmt.Errorf("failed to marshal minimal response: %w", err) } } else { r, err = json.Marshal(result) if err != nil { return nil, fmt.Errorf("failed to marshal full 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 } } 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) }