Skip to content

feat: add fork filter parameter to repository search functionality #250

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,7 @@ export GITHUB_MCP_TOOL_ADD_ISSUE_COMMENT_DESCRIPTION="an alternative description
- `query`: Search query (string, required)
- `sort`: Sort field (string, optional)
- `order`: Sort order (string, optional)
- `fork`: Fork filter: 'true' to include forks, 'only' to show only forks, 'false' to exclude forks (default) (string, optional)
- `page`: Page number (number, optional)
- `perPage`: Results per page (number, optional)

Expand Down
27 changes: 27 additions & 0 deletions pkg/github/search.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,21 @@ import (
"encoding/json"
"fmt"
"io"
"strings"

"github.com/github/github-mcp-server/pkg/translations"
"github.com/google/go-github/v69/github"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)

// containsForkFilter checks if a query already contains a fork filter.
// This prevents duplicate or conflicting fork filters in the query.
func containsForkFilter(query string) bool {
query = strings.ToLower(query)
return strings.Contains(query, " fork:") || strings.HasPrefix(query, "fork:")
}

// 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",
Expand All @@ -20,18 +28,37 @@ func SearchRepositories(getClient GetClientFn, t translations.TranslationHelperF
mcp.Required(),
mcp.Description("Search query"),
),
mcp.WithString("fork",
mcp.Description("Fork filter: 'true' to include forks, 'only' to show only forks, 'false' to exclude forks (default)"),
mcp.Enum("true", "only", "false"),
),
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
}
fork, err := OptionalParam[string](request, "fork")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
pagination, err := OptionalPaginationParams(request)
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}

// Modify query to include fork parameter if specified
if fork != "" {
// GitHub API supports 'fork:true', 'fork:only', and absence for excluding forks
if fork == "true" || fork == "only" {
// Check if the query already contains a fork filter
if !containsForkFilter(query) {
query = query + " fork:" + fork
}
}
}

opts := &github.SearchOptions{
ListOptions: github.ListOptions{
Page: pagination.page,
Expand Down
65 changes: 65 additions & 0 deletions pkg/github/search_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ func Test_SearchRepositories(t *testing.T) {
"q": "golang test",
"page": "1",
"per_page": "30",
// Note: Not specifying 'fork' parameter here, which verifies that
// when 'fork' is not provided in the request, it's not added to the query parameters
}).andThen(
mockResponse(t, http.StatusOK, mockSearchResult),
),
Expand All @@ -99,6 +101,69 @@ func Test_SearchRepositories(t *testing.T) {
expectError: false,
expectedResult: mockSearchResult,
},
{
name: "repository search with fork parameter",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
mock.GetSearchRepositories,
expectQueryParams(t, map[string]string{
"q": "golang test fork:only",
"page": "1",
"per_page": "30",
}).andThen(
mockResponse(t, http.StatusOK, mockSearchResult),
),
),
),
requestArgs: map[string]interface{}{
"query": "golang test",
"fork": "only",
},
expectError: false,
expectedResult: mockSearchResult,
},
{
name: "repository search with fork parameter (true)",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
mock.GetSearchRepositories,
expectQueryParams(t, map[string]string{
"q": "golang test fork:true",
"page": "1",
"per_page": "30",
}).andThen(
mockResponse(t, http.StatusOK, mockSearchResult),
),
),
),
requestArgs: map[string]interface{}{
"query": "golang test",
"fork": "true",
},
expectError: false,
expectedResult: mockSearchResult,
},
{
name: "repository search with fork parameter (false)",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
mock.GetSearchRepositories,
expectQueryParams(t, map[string]string{
"q": "golang test",
"page": "1",
"per_page": "30",
}).andThen(
mockResponse(t, http.StatusOK, mockSearchResult),
),
),
),
requestArgs: map[string]interface{}{
"query": "golang test",
"fork": "false",
},
expectError: false,
expectedResult: mockSearchResult,
},
{
name: "search fails",
mockedClient: mock.NewMockedHTTPClient(
Expand Down