Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 91 additions & 12 deletions pkg/github/search.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,34 @@ import (
"github.com/mark3labs/mcp-go/server"
)

// MinimalCodeResult is a compact representation of a code search result
// optimized for LLM consumption.
type MinimalCodeResult struct {
Name string `json:"name"`
Path string `json:"path"`
HTMLURL string `json:"html_url"`
Repository string `json:"repository"` // "owner/repo" format
TextMatches []string `json:"text_matches,omitempty"` // code fragments
}

// MinimalCodeSearchResult is a compact representation of a code search response.
type MinimalCodeSearchResult struct {
TotalCount int `json:"total_count"`
IncompleteResults bool `json:"incomplete_results"`
Items []MinimalCodeResult `json:"items"`
}

// 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", "Search for GitHub repositories")),
mcp.WithDescription(t("TOOL_SEARCH_REPOSITORIES_DESCRIPTION", `Search for GitHub repositories by name, description, topics, or other metadata.

Useful for discovering projects, finding repos by topic, or locating specific repositories.

Examples:
- "machine learning" language:python stars:>100
- topic:kubernetes org:myorg
- "payment" in:name org:myorg`)),
mcp.WithToolAnnotation(mcp.ToolAnnotation{
Title: t("TOOL_SEARCH_REPOSITORIES_USER_TITLE", "Search repositories"),
ReadOnlyHint: toBoolPtr(true),
Expand Down Expand Up @@ -82,14 +106,40 @@ func SearchRepositories(getClient GetClientFn, t translations.TranslationHelperF
// 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", "Search for code across GitHub repositories")),
mcp.WithDescription(t("TOOL_SEARCH_CODE_DESCRIPTION", `Search for code across GitHub repositories using the REST API.

IMPORTANT: This tool uses GitHub's legacy code search API. Only the qualifiers listed below are supported.
Do NOT use: content:, symbol:, is:, NOT, OR, parentheses, /regex/, or glob patterns — they will silently fail.

Supported qualifiers:
- org:NAME or user:NAME — scope to an organization (ALWAYS include this for broad searches)
- repo:OWNER/NAME — scope to a specific repository
- language:NAME — filter by programming language
- path:DIRECTORY — filter by directory path (basic only, no glob)
- filename:NAME — find files by name
- extension:EXT — find files by extension
- in:file or in:path — search file contents vs file paths
- size:N — filter by file size (e.g. size:>1000)
- "exact phrase" — quoted exact string match
- fork:true — include results from forked repositories
- Multiple terms are AND'd automatically

Rate limit: 10 searches per minute. Plan your query carefully before searching.

Examples:
- "class AuthHandler" language:python org:myorg
- filename:Dockerfile org:myorg
- "import express" extension:ts repo:owner/repo
- path:src/api "middleware" language:go org:myorg

After finding files, use get_file_contents to read the full source code.`)),
mcp.WithToolAnnotation(mcp.ToolAnnotation{
Title: t("TOOL_SEARCH_CODE_USER_TITLE", "Search code"),
ReadOnlyHint: toBoolPtr(true),
}),
mcp.WithString("q",
mcp.WithString("query",
mcp.Required(),
mcp.Description("Search query using GitHub code search syntax"),
mcp.Description("Search query using GitHub code search qualifiers. Always scope with org: or repo: for best results."),
),
mcp.WithString("sort",
mcp.Description("Sort field ('indexed' only)"),
Expand All @@ -101,7 +151,7 @@ func SearchCode(getClient GetClientFn, t translations.TranslationHelperFunc) (to
WithPagination(),
),
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
query, err := requiredParam[string](request, "q")
query, err := requiredParam[string](request, "query")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
Expand All @@ -128,8 +178,9 @@ func SearchCode(getClient GetClientFn, t translations.TranslationHelperFunc) (to
}

opts := &github.SearchOptions{
Sort: sort,
Order: order,
Sort: sort,
Order: order,
TextMatch: true,
ListOptions: github.ListOptions{
PerPage: pagination.perPage,
Page: pagination.page,
Expand All @@ -155,7 +206,31 @@ func SearchCode(getClient GetClientFn, t translations.TranslationHelperFunc) (to
return mcp.NewToolResultError(fmt.Sprintf("failed to search code: %s", string(body))), nil
}

r, err := json.Marshal(result)
// Format as minimal results for LLM consumption
minimalResults := make([]MinimalCodeResult, 0, len(result.CodeResults))
for _, cr := range result.CodeResults {
mr := MinimalCodeResult{
Name: cr.GetName(),
Path: cr.GetPath(),
HTMLURL: cr.GetHTMLURL(),
Repository: cr.GetRepository().GetFullName(),
}
// Extract text match fragments
for _, tm := range cr.TextMatches {
if tm.Fragment != nil {
mr.TextMatches = append(mr.TextMatches, *tm.Fragment)
}
}
minimalResults = append(minimalResults, mr)
}

minimalResult := MinimalCodeSearchResult{
TotalCount: result.GetTotal(),
IncompleteResults: result.GetIncompleteResults(),
Items: minimalResults,
}

r, err := json.Marshal(minimalResult)
if err != nil {
return nil, fmt.Errorf("failed to marshal response: %w", err)
}
Expand All @@ -167,14 +242,18 @@ func SearchCode(getClient GetClientFn, t translations.TranslationHelperFunc) (to
// 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", "Search for GitHub users")),
mcp.WithDescription(t("TOOL_SEARCH_USERS_DESCRIPTION", `Search for GitHub users by username, name, location, or other profile information.

Examples:
- "john" location:seattle
- followers:>100 language:go`)),
mcp.WithToolAnnotation(mcp.ToolAnnotation{
Title: t("TOOL_SEARCH_USERS_USER_TITLE", "Search users"),
ReadOnlyHint: toBoolPtr(true),
}),
mcp.WithString("q",
mcp.WithString("query",
mcp.Required(),
mcp.Description("Search query using GitHub users search syntax"),
mcp.Description("Search query for GitHub users. Examples: 'location:seattle', 'followers:>100'."),
),
mcp.WithString("sort",
mcp.Description("Sort field by category"),
Expand All @@ -187,7 +266,7 @@ func SearchUsers(getClient GetClientFn, t translations.TranslationHelperFunc) (t
WithPagination(),
),
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
query, err := requiredParam[string](request, "q")
query, err := requiredParam[string](request, "query")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
Expand Down
Loading