Make search_issues semantic by default (#2964) · github/github-mcp-server@f3cb662 · GitHub
Skip to content

Commit f3cb662

Browse files
Make search_issues semantic by default (#2964)
* Make search_issues semantic by default * initialize description depending on the host --------- Co-authored-by: Iulia B <iulia-b@github.com> Co-authored-by: Iulia Bejan <64602043+iulia-b@users.noreply.github.com>
1 parent e6e3a4e commit f3cb662

14 files changed

Lines changed: 402 additions & 61 deletions

File tree

‎README.md‎

Lines changed: 1 addition & 1 deletion

‎internal/ghmcp/server.go‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,11 @@ func NewStdioMCPServer(ctx context.Context, cfg github.MCPServerConfig) (*mcp.Se
138138
return nil, fmt.Errorf("failed to parse API host: %w", err)
139139
}
140140

141+
hostType, err := utils.ParseHostType(cfg.Host)
142+
if err != nil {
143+
return nil, fmt.Errorf("failed to classify API host: %w", err)
144+
}
145+
141146
clients, err := createGitHubClients(cfg, apiHost)
142147
if err != nil {
143148
return nil, fmt.Errorf("failed to create GitHub clients: %w", err)
@@ -165,7 +170,7 @@ func NewStdioMCPServer(ctx context.Context, cfg github.MCPServerConfig) (*mcp.Se
165170
obs,
166171
)
167172
// Build and register the tool/resource/prompt inventory
168-
inventoryBuilder := github.NewInventory(cfg.Translator).
173+
inventoryBuilder := github.NewInventory(cfg.Translator, github.WithHost(hostType)).
169174
WithDeprecatedAliases(github.DeprecatedToolAliases).
170175
WithReadOnly(cfg.ReadOnly).
171176
WithToolsets(github.ResolvedEnabledToolsets(cfg.EnabledToolsets, cfg.EnabledTools)).

‎pkg/github/__toolsnaps__/search_issues.snap‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"readOnlyHint": true,
55
"title": "Search issues"
66
},
7-
"description": "Search for issues in GitHub repositories using issues search syntax already scoped to is:issue",
7+
"description": "Search issues using natural-language semantic matching. Best for conceptual or paraphrased queries (e.g. \"login fails after password reset\"). Already scoped to is:issue.",
88
"inputSchema": {
99
"properties": {
1010
"fields": {
@@ -64,7 +64,7 @@
6464
"type": "number"
6565
},
6666
"query": {
67-
"description": "Search query using GitHub issues search syntax",
67+
"description": "The search query, as natural language. When the user gives alternative wordings, include them as plain words rather than joining them with OR.",
6868
"type": "string"
6969
},
7070
"repo": {

‎pkg/github/inventory.go‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ import (
1010
// This function is stateless - no dependencies are captured.
1111
// Handlers are generated on-demand during registration via RegisterAll(ctx, server, deps).
1212
// The "default" keyword in WithToolsets will expand to toolsets marked with Default: true.
13-
func NewInventory(t translations.TranslationHelperFunc) *inventory.Builder {
13+
func NewInventory(t translations.TranslationHelperFunc, opts ...ToolOption) *inventory.Builder {
1414
return inventory.NewBuilder().
15-
SetTools(AllTools(t)).
15+
SetTools(AllTools(t, opts...)).
1616
SetResources(AllResources(t)).
1717
SetPrompts(AllPrompts(t))
1818
}

‎pkg/github/issues.go‎

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1610,14 +1610,43 @@ func ReprioritizeSubIssue(ctx context.Context, client *github.Client, owner stri
16101610
return utils.NewToolResultText(string(r)), nil
16111611
}
16121612

1613+
// The two search engines want opposite things from a caller, so steering advice
1614+
// for one is counterproductive for the other: semantic rewards paraphrased
1615+
// natural language and degrades on boolean operators, while lexical needs the
1616+
// caller's literal keywords and handles OR fine. The description has to describe
1617+
// the engine the host will actually use.
1618+
const (
1619+
searchIssuesSemanticDescription = "Search issues using natural-language semantic matching. Best for conceptual or paraphrased queries (e.g. \"login fails after password reset\"). Already scoped to is:issue."
1620+
searchIssuesLexicalDescription = "Search for issues in GitHub repositories using issues search syntax already scoped to is:issue"
1621+
1622+
searchIssuesSemanticQueryDescription = "The search query, as natural language. When the user gives alternative wordings, include them as plain words rather than joining them with OR."
1623+
searchIssuesLexicalQueryDescription = "Search query using GitHub issues search syntax"
1624+
)
1625+
16131626
// SearchIssues creates a tool to search for issues.
1614-
func SearchIssues(t translations.TranslationHelperFunc) inventory.ServerTool {
1627+
func SearchIssues(t translations.TranslationHelperFunc, opts ...ToolOption) inventory.ServerTool {
1628+
cfg := newToolConfig(opts)
1629+
1630+
// Semantic is the default; however as it is not available on GHES, we fall back to
1631+
// lexical search for that host type.
1632+
mode := searchModeSemantic
1633+
if cfg.hostType == utils.HostTypeGHES {
1634+
mode = searchModeLexical
1635+
}
1636+
1637+
toolDescription := searchIssuesSemanticDescription
1638+
queryDescription := searchIssuesSemanticQueryDescription
1639+
if mode == searchModeLexical {
1640+
toolDescription = searchIssuesLexicalDescription
1641+
queryDescription = searchIssuesLexicalQueryDescription
1642+
}
1643+
16151644
schema := &jsonschema.Schema{
16161645
Type: "object",
16171646
Properties: map[string]*jsonschema.Schema{
16181647
"query": {
16191648
Type: "string",
1620-
Description: "Search query using GitHub issues search syntax",
1649+
Description: queryDescription,
16211650
},
16221651
"owner": {
16231652
Type: "string",
@@ -1662,7 +1691,7 @@ func SearchIssues(t translations.TranslationHelperFunc) inventory.ServerTool {
16621691
ToolsetMetadataIssues,
16631692
mcp.Tool{
16641693
Name: "search_issues",
1665-
Description: t("TOOL_SEARCH_ISSUES_DESCRIPTION", "Search for issues in GitHub repositories using issues search syntax already scoped to is:issue"),
1694+
Description: t("TOOL_SEARCH_ISSUES_DESCRIPTION", toolDescription),
16661695
Annotations: &mcp.ToolAnnotations{
16671696
Title: t("TOOL_SEARCH_ISSUES_USER_TITLE", "Search issues"),
16681697
ReadOnlyHint: true,
@@ -1677,7 +1706,7 @@ func SearchIssues(t translations.TranslationHelperFunc) inventory.ServerTool {
16771706
return utils.NewToolResultError(err.Error()), nil, nil
16781707
}
16791708
options = append(options, withFieldsFiltering(deps, "search_issues", fields))
1680-
result, err := searchIssuesHandler(ctx, deps, args, options...)
1709+
result, err := searchIssuesHandler(ctx, deps, args, mode, options...)
16811710
return result, nil, err
16821711
})
16831712
}
@@ -1991,10 +2020,10 @@ func fetchIssueReadEnrichment(ctx context.Context, gqlClient *githubv4.Client, n
19912020
// searchIssuesHandler runs the REST issues search, enriches each hit with custom field values
19922021
// fetched via a single follow-up GraphQL nodes() query, and applies any post-process options
19932022
// (e.g. IFC labelling).
1994-
func searchIssuesHandler(ctx context.Context, deps ToolDependencies, args map[string]any, options ...searchOption) (*mcp.CallToolResult, error) {
2023+
func searchIssuesHandler(ctx context.Context, deps ToolDependencies, args map[string]any, mode searchMode, options ...searchOption) (*mcp.CallToolResult, error) {
19952024
const errorPrefix = "failed to search issues"
19962025

1997-
query, opts, err := prepareSearchArgs(args, "issue")
2026+
query, opts, err := prepareSearchArgs(args, "issue", mode)
19982027
if err != nil {
19992028
return utils.NewToolResultError(err.Error()), nil
20002029
}

‎pkg/github/issues_test.go‎

Lines changed: 42 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1113,11 +1113,12 @@ func Test_SearchIssues(t *testing.T) {
11131113
GetSearchIssues: expectQueryParams(
11141114
t,
11151115
map[string]string{
1116-
"q": "is:issue repo:owner/repo is:open",
1117-
"sort": "created",
1118-
"order": "desc",
1119-
"page": "1",
1120-
"per_page": "30",
1116+
"q": "is:issue repo:owner/repo is:open",
1117+
"sort": "created",
1118+
"order": "desc",
1119+
"page": "1",
1120+
"per_page": "30",
1121+
"search_type": "semantic",
11211122
},
11221123
).andThen(
11231124
mockResponse(t, http.StatusOK, mockSearchResult),
@@ -1139,11 +1140,12 @@ func Test_SearchIssues(t *testing.T) {
11391140
GetSearchIssues: expectQueryParams(
11401141
t,
11411142
map[string]string{
1142-
"q": "repo:test-owner/test-repo is:issue is:open",
1143-
"sort": "created",
1144-
"order": "asc",
1145-
"page": "1",
1146-
"per_page": "30",
1143+
"q": "repo:test-owner/test-repo is:issue is:open",
1144+
"sort": "created",
1145+
"order": "asc",
1146+
"page": "1",
1147+
"per_page": "30",
1148+
"search_type": "semantic",
11471149
},
11481150
).andThen(
11491151
mockResponse(t, http.StatusOK, mockSearchResult),
@@ -1165,9 +1167,10 @@ func Test_SearchIssues(t *testing.T) {
11651167
GetSearchIssues: expectQueryParams(
11661168
t,
11671169
map[string]string{
1168-
"q": "is:issue bug",
1169-
"page": "1",
1170-
"per_page": "30",
1170+
"q": "is:issue bug",
1171+
"page": "1",
1172+
"per_page": "30",
1173+
"search_type": "semantic",
11711174
},
11721175
).andThen(
11731176
mockResponse(t, http.StatusOK, mockSearchResult),
@@ -1186,9 +1189,10 @@ func Test_SearchIssues(t *testing.T) {
11861189
GetSearchIssues: expectQueryParams(
11871190
t,
11881191
map[string]string{
1189-
"q": "is:issue feature",
1190-
"page": "1",
1191-
"per_page": "30",
1192+
"q": "is:issue feature",
1193+
"page": "1",
1194+
"per_page": "30",
1195+
"search_type": "semantic",
11921196
},
11931197
).andThen(
11941198
mockResponse(t, http.StatusOK, mockSearchResult),
@@ -1218,9 +1222,10 @@ func Test_SearchIssues(t *testing.T) {
12181222
GetSearchIssues: expectQueryParams(
12191223
t,
12201224
map[string]string{
1221-
"q": "repo:github/github-mcp-server is:issue is:open (label:critical OR label:urgent)",
1222-
"page": "1",
1223-
"per_page": "30",
1225+
"q": "repo:github/github-mcp-server is:issue is:open (label:critical OR label:urgent)",
1226+
"page": "1",
1227+
"per_page": "30",
1228+
"search_type": "semantic",
12241229
},
12251230
).andThen(
12261231
mockResponse(t, http.StatusOK, mockSearchResult),
@@ -1238,9 +1243,10 @@ func Test_SearchIssues(t *testing.T) {
12381243
GetSearchIssues: expectQueryParams(
12391244
t,
12401245
map[string]string{
1241-
"q": "is:issue repo:github/github-mcp-server critical",
1242-
"page": "1",
1243-
"per_page": "30",
1246+
"q": "is:issue repo:github/github-mcp-server critical",
1247+
"page": "1",
1248+
"per_page": "30",
1249+
"search_type": "semantic",
12441250
},
12451251
).andThen(
12461252
mockResponse(t, http.StatusOK, mockSearchResult),
@@ -1260,9 +1266,10 @@ func Test_SearchIssues(t *testing.T) {
12601266
GetSearchIssues: expectQueryParams(
12611267
t,
12621268
map[string]string{
1263-
"q": "is:issue repo:octocat/Hello-World bug",
1264-
"page": "1",
1265-
"per_page": "30",
1269+
"q": "is:issue repo:octocat/Hello-World bug",
1270+
"page": "1",
1271+
"per_page": "30",
1272+
"search_type": "semantic",
12661273
},
12671274
).andThen(
12681275
mockResponse(t, http.StatusOK, mockSearchResult),
@@ -1280,9 +1287,10 @@ func Test_SearchIssues(t *testing.T) {
12801287
GetSearchIssues: expectQueryParams(
12811288
t,
12821289
map[string]string{
1283-
"q": "repo:github/github-mcp-server is:issue (label:critical OR label:urgent OR label:high-priority OR label:blocker)",
1284-
"page": "1",
1285-
"per_page": "30",
1290+
"q": "repo:github/github-mcp-server is:issue (label:critical OR label:urgent OR label:high-priority OR label:blocker)",
1291+
"page": "1",
1292+
"per_page": "30",
1293+
"search_type": "semantic",
12861294
},
12871295
).andThen(
12881296
mockResponse(t, http.StatusOK, mockSearchResult),
@@ -1303,6 +1311,7 @@ func Test_SearchIssues(t *testing.T) {
13031311
"q": "is:issue field.priority:P1",
13041312
"page": "1",
13051313
"per_page": "30",
1314+
"search_type": "semantic",
13061315
"advanced_search": "true",
13071316
},
13081317
).andThen(
@@ -1316,14 +1325,15 @@ func Test_SearchIssues(t *testing.T) {
13161325
expectedResult: mockSearchResult,
13171326
},
13181327
{
1319-
name: "query without field. qualifier does not set advanced_search",
1328+
name: "semantic search sets search_type",
13201329
mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
13211330
GetSearchIssues: expectQueryParams(
13221331
t,
13231332
map[string]string{
1324-
"q": "is:issue is:open",
1325-
"page": "1",
1326-
"per_page": "30",
1333+
"q": "is:issue is:open",
1334+
"page": "1",
1335+
"per_page": "30",
1336+
"search_type": "semantic",
13271337
},
13281338
).andThen(
13291339
mockResponse(t, http.StatusOK, mockSearchResult),

‎pkg/github/search_semantic_test.go‎

Lines changed: 87 additions & 0 deletions

0 commit comments

Comments
 (0)