Skip to content

Commit 6e36183

Browse files
committed
feat: add GitHub search as fallback for broader MCP server discovery
- New `--all` flag: `mcphub search xxx --all` searches both official MCP Registry and GitHub repositories - If Registry returns < 3 results, automatically falls back to GitHub - GitHub results marked with [GitHub] tag in output - MCP server mode (mcphub-mcp) now uses SearchAll by default
1 parent 11a3f56 commit 6e36183

3 files changed

Lines changed: 149 additions & 6 deletions

File tree

internal/cli/search.go

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,28 @@ import (
1111

1212
var searchJSON bool
1313
var searchLimit int
14+
var searchAll bool
1415

1516
var searchCmd = &cobra.Command{
1617
Use: "search <query>",
1718
Short: "Search for MCP servers",
18-
Long: "Search the MCP registry for servers matching your query.",
19-
Args: cobra.ExactArgs(1),
19+
Long: `Search the MCP registry for servers matching your query.
20+
21+
By default searches the official MCP Registry only.
22+
Use --all to also search GitHub for MCP server repositories.`,
23+
Args: cobra.ExactArgs(1),
2024
RunE: func(cmd *cobra.Command, args []string) error {
2125
query := args[0]
2226
client := registry.NewClient()
2327

24-
entries, err := client.Search(query, searchLimit)
28+
var entries []registry.ServerEntry
29+
var err error
30+
31+
if searchAll {
32+
entries, err = client.SearchAll(query, searchLimit)
33+
} else {
34+
entries, err = client.Search(query, searchLimit)
35+
}
2536
if err != nil {
2637
return fmt.Errorf("search failed: %w", err)
2738
}
@@ -32,9 +43,15 @@ var searchCmd = &cobra.Command{
3243
return nil
3344
}
3445

35-
fmt.Printf("\n Found %s servers matching %s\n\n",
46+
source := "registry"
47+
if searchAll {
48+
source = "registry + GitHub"
49+
}
50+
51+
fmt.Printf("\n Found %s servers matching %s (%s)\n\n",
3652
ui.Bold(fmt.Sprintf("%d", len(entries))),
3753
ui.Cyan(fmt.Sprintf("%q", query)),
54+
ui.Dim(source),
3855
)
3956

4057
headers := []string{"Name", "Description", "Version", "Transport"}
@@ -47,8 +64,16 @@ var searchCmd = &cobra.Command{
4764
} else if len(s.Packages) > 0 {
4865
transport = s.Packages[0].Transport.Type
4966
}
67+
68+
// Mark GitHub results
69+
source := ""
70+
if meta, ok := e.Meta["source"]; ok && meta == "github" {
71+
source = " [GitHub]"
72+
transport = "—"
73+
}
74+
5075
rows = append(rows, []string{
51-
s.ShortName(),
76+
s.ShortName() + source,
5277
ui.Truncate(s.Description, 50),
5378
s.Version,
5479
transport,
@@ -64,4 +89,5 @@ var searchCmd = &cobra.Command{
6489
func init() {
6590
searchCmd.Flags().BoolVar(&searchJSON, "json", false, "Output results as JSON")
6691
searchCmd.Flags().IntVar(&searchLimit, "limit", 20, "Maximum number of results")
92+
searchCmd.Flags().BoolVar(&searchAll, "all", false, "Also search GitHub for MCP servers")
6793
}

internal/registry/github.go

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
package registry
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"io"
7+
"net/http"
8+
"net/url"
9+
)
10+
11+
// GitHubSearchResult represents the GitHub search API response.
12+
type GitHubSearchResult struct {
13+
TotalCount int `json:"total_count"`
14+
Items []GitHubRepo `json:"items"`
15+
}
16+
17+
// GitHubRepo represents a GitHub repository from search results.
18+
type GitHubRepo struct {
19+
FullName string `json:"full_name"`
20+
Description string `json:"description"`
21+
HTMLURL string `json:"html_url"`
22+
StarCount int `json:"stargazers_count"`
23+
Language string `json:"language"`
24+
UpdatedAt string `json:"updated_at"`
25+
}
26+
27+
// SearchGitHub searches GitHub for MCP server repositories.
28+
// Used as a fallback when the official registry returns no results.
29+
func (c *Client) SearchGitHub(query string, limit int) ([]ServerEntry, error) {
30+
if limit <= 0 {
31+
limit = 10
32+
}
33+
34+
// Search GitHub for MCP server repos
35+
ghQuery := fmt.Sprintf("mcp server %s in:name,description,readme", query)
36+
params := url.Values{}
37+
params.Set("q", ghQuery)
38+
params.Set("sort", "stars")
39+
params.Set("order", "desc")
40+
params.Set("per_page", fmt.Sprintf("%d", limit))
41+
42+
reqURL := fmt.Sprintf("https://api.github.com/search/repositories?%s", params.Encode())
43+
44+
req, err := http.NewRequest("GET", reqURL, nil)
45+
if err != nil {
46+
return nil, err
47+
}
48+
req.Header.Set("Accept", "application/vnd.github.v3+json")
49+
req.Header.Set("User-Agent", "mcphub")
50+
51+
resp, err := c.httpClient.Do(req)
52+
if err != nil {
53+
return nil, fmt.Errorf("GitHub search failed: %w", err)
54+
}
55+
defer resp.Body.Close()
56+
57+
if resp.StatusCode != http.StatusOK {
58+
body, _ := io.ReadAll(resp.Body)
59+
return nil, fmt.Errorf("GitHub API returned %d: %s", resp.StatusCode, string(body))
60+
}
61+
62+
var result GitHubSearchResult
63+
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
64+
return nil, fmt.Errorf("failed to parse GitHub response: %w", err)
65+
}
66+
67+
// Convert GitHub repos to ServerEntry format
68+
var entries []ServerEntry
69+
for _, repo := range result.Items {
70+
desc := repo.Description
71+
if desc == "" {
72+
desc = "MCP server from GitHub"
73+
}
74+
75+
entries = append(entries, ServerEntry{
76+
Server: ServerDetail{
77+
Name: fmt.Sprintf("github.com/%s", repo.FullName),
78+
Description: fmt.Sprintf("%s (%d stars)", desc, repo.StarCount),
79+
Repository: &Repository{
80+
URL: repo.HTMLURL,
81+
Source: "github",
82+
},
83+
},
84+
Meta: map[string]interface{}{
85+
"source": "github",
86+
"stars": repo.StarCount,
87+
"language": repo.Language,
88+
},
89+
})
90+
}
91+
92+
return entries, nil
93+
}
94+
95+
// SearchAll searches both the official registry and GitHub, merging results.
96+
func (c *Client) SearchAll(query string, limit int) ([]ServerEntry, error) {
97+
// First search official registry
98+
entries, err := c.Search(query, limit)
99+
if err != nil {
100+
entries = nil // Don't fail, try GitHub
101+
}
102+
103+
// If registry returned few results, supplement with GitHub
104+
if len(entries) < 3 {
105+
ghEntries, ghErr := c.SearchGitHub(query, limit-len(entries))
106+
if ghErr == nil {
107+
entries = append(entries, ghEntries...)
108+
}
109+
}
110+
111+
// Cap at limit
112+
if len(entries) > limit {
113+
entries = entries[:limit]
114+
}
115+
116+
return entries, nil
117+
}

mcp/server.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ func handleSearch(args json.RawMessage, client *registry.Client) interface{} {
199199
params.Limit = 10
200200
}
201201

202-
entries, err := client.Search(params.Query, params.Limit)
202+
entries, err := client.SearchAll(params.Query, params.Limit)
203203
if err != nil {
204204
return toolError("search failed: " + err.Error())
205205
}

0 commit comments

Comments
 (0)