From 20cee3266a4c363beab1734fb3e416ddbf3808d7 Mon Sep 17 00:00:00 2001 From: beejak Date: Wed, 22 Apr 2026 19:26:33 +0530 Subject: [PATCH] fix(tools): clarify search_code path: vs filename: (#2343) Document path: (directory prefix) vs filename: in the query schema. When GitHub returns zero code results and the query uses path: with a file-like token, append a second text block so agents see why results may be empty. Made-with: Cursor --- pkg/github/__toolsnaps__/search_code.snap | 2 +- pkg/github/search.go | 37 +++++++++++++- pkg/github/search_test.go | 59 +++++++++++++++++++++++ 3 files changed, 96 insertions(+), 2 deletions(-) diff --git a/pkg/github/__toolsnaps__/search_code.snap b/pkg/github/__toolsnaps__/search_code.snap index 8b5510aa61..89357e727d 100644 --- a/pkg/github/__toolsnaps__/search_code.snap +++ b/pkg/github/__toolsnaps__/search_code.snap @@ -26,7 +26,7 @@ "type": "number" }, "query": { - "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.", + "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. Important: GitHub's path: qualifier matches directory path prefixes, not file names. To target a specific file (e.g. WaitUtils.cs), use filename:WaitUtils.cs instead of path:WaitUtils.cs. Using path: with a file-like token often returns zero results with no API error.", "type": "string" }, "sort": { diff --git a/pkg/github/search.go b/pkg/github/search.go index d5ddb4a72a..a4699a63b9 100644 --- a/pkg/github/search.go +++ b/pkg/github/search.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "net/http" + "strings" ghErrors "github.com/github/github-mcp-server/pkg/errors" "github.com/github/github-mcp-server/pkg/inventory" @@ -166,6 +167,25 @@ func SearchRepositories(t translations.TranslationHelperFunc) inventory.ServerTo ) } +// pathQualifierLooksLikeFilename reports whether the query uses a path: token whose +// value looks like a file name (contains a dot, no path separators). GitHub's code +// search treats path: as a directory prefix; filtering by file name requires filename:. +func pathQualifierLooksLikeFilename(q string) bool { + for _, part := range strings.Fields(q) { + after, ok := strings.CutPrefix(part, "path:") + if !ok || after == "" { + continue + } + if strings.ContainsAny(after, "/\\") { + continue + } + if strings.Contains(after, ".") { + return true + } + } + return false +} + // SearchCode creates a tool to search for code across GitHub repositories. func SearchCode(t translations.TranslationHelperFunc) inventory.ServerTool { schema := &jsonschema.Schema{ @@ -173,7 +193,7 @@ func SearchCode(t translations.TranslationHelperFunc) inventory.ServerTool { Properties: map[string]*jsonschema.Schema{ "query": { Type: "string", - 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.", + 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. Important: GitHub's path: qualifier matches directory path prefixes, not file names. To target a specific file (e.g. WaitUtils.cs), use filename:WaitUtils.cs instead of path:WaitUtils.cs. Using path: with a file-like token often returns zero results with no API error.", }, "sort": { Type: "string", @@ -256,6 +276,21 @@ func SearchCode(t translations.TranslationHelperFunc) inventory.ServerTool { return utils.NewToolResultErrorFromErr("failed to marshal response", err), nil, nil } + total := 0 + if result.Total != nil { + total = *result.Total + } + if total == 0 && pathQualifierLooksLikeFilename(query) { + return &mcp.CallToolResult{ + Content: []mcp.Content{ + &mcp.TextContent{Text: string(r)}, + &mcp.TextContent{ + Text: "Note: GitHub code search treats `path:` as a directory path prefix, not a file name. If you meant to search inside a specific file, use the `filename:` qualifier (for example, `filename:WaitUtils.cs`) instead of `path:WaitUtils.cs`.", + }, + }, + }, nil, nil + } + return utils.NewToolResultText(string(r)), nil, nil }, ) diff --git a/pkg/github/search_test.go b/pkg/github/search_test.go index 85eb21bcb5..58db871085 100644 --- a/pkg/github/search_test.go +++ b/pkg/github/search_test.go @@ -10,6 +10,7 @@ import ( "github.com/github/github-mcp-server/pkg/translations" "github.com/google/go-github/v82/github" "github.com/google/jsonschema-go/jsonschema" + "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -380,6 +381,64 @@ func Test_SearchCode(t *testing.T) { } } +func TestPathQualifierLooksLikeFilename(t *testing.T) { + t.Parallel() + tests := []struct { + query string + want bool + }{ + {"WaitForElement path:WaitUtils.cs repo:o/r", true}, + {"foo path:src/pkg file", false}, + {"path:WaitUtils.cs", true}, + {"filename:WaitUtils.cs", false}, + {"path:", false}, + {"path:dir/sub file.cs", false}, + } + for _, tc := range tests { + t.Run(tc.query, func(t *testing.T) { + t.Parallel() + assert.Equal(t, tc.want, pathQualifierLooksLikeFilename(tc.query), tc.query) + }) + } +} + +func Test_SearchCode_addsHintWhenPathLooksLikeFilenameAndNoResults(t *testing.T) { + serverTool := SearchCode(translations.NullTranslationHelper) + empty := &github.CodeSearchResult{ + Total: github.Ptr(0), + IncompleteResults: github.Ptr(false), + CodeResults: []*github.CodeResult{}, + } + client := github.NewClient(MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ + GetSearchCode: expectQueryParams(t, map[string]string{ + "q": "term path:WaitUtils.cs repo:o/r", + "page": "1", + "per_page": "30", + }).andThen( + mockResponse(t, http.StatusOK, empty), + ), + })) + deps := BaseDeps{Client: client} + handler := serverTool.Handler(deps) + request := createMCPRequest(map[string]any{ + "query": "term path:WaitUtils.cs repo:o/r", + }) + result, err := handler(ContextWithDeps(context.Background(), deps), &request) + require.NoError(t, err) + require.False(t, result.IsError) + require.Len(t, result.Content, 2) + first, ok := result.Content[0].(*mcp.TextContent) + require.True(t, ok) + second, ok := result.Content[1].(*mcp.TextContent) + require.True(t, ok) + var returned github.CodeSearchResult + require.NoError(t, json.Unmarshal([]byte(first.Text), &returned)) + require.NotNil(t, returned.Total) + assert.Equal(t, 0, *returned.Total) + assert.Contains(t, second.Text, "filename:") + assert.Contains(t, second.Text, "path:") +} + func Test_SearchUsers(t *testing.T) { // Verify tool definition once serverTool := SearchUsers(translations.NullTranslationHelper)