@@ -228,6 +228,32 @@ type ListIssuesQueryTypeWithLabelsWithSince struct {
228228 } `graphql:"repository(owner: $owner, name: $repo)"`
229229}
230230
231+ // SearchIssueResult wraps a REST search hit with its custom issue field values, fetched in a follow-up GraphQL nodes() query.
232+ type SearchIssueResult struct {
233+ * github.Issue
234+ FieldValues []MinimalIssueFieldValue `json:"field_values,omitempty"`
235+ }
236+
237+ // SearchIssuesResponse mirrors the REST IssuesSearchResult JSON shape and adds field_values per item.
238+ type SearchIssuesResponse struct {
239+ Total * int `json:"total_count,omitempty"`
240+ IncompleteResults * bool `json:"incomplete_results,omitempty"`
241+ Items []SearchIssueResult `json:"items"`
242+ }
243+
244+ // searchIssuesNodesQuery batches a nodes(ids:) lookup over the REST search results to retrieve
245+ // each issue's custom field values in a single GraphQL request.
246+ type searchIssuesNodesQuery struct {
247+ Nodes []struct {
248+ Issue struct {
249+ ID githubv4.ID
250+ IssueFieldValues struct {
251+ Nodes []IssueFieldValueFragment
252+ } `graphql:"issueFieldValues(first: 25)"` // 25 exceeds the practical max of custom fields per issue in GitHub Projects
253+ } `graphql:"... on Issue"`
254+ } `graphql:"nodes(ids: $ids)"`
255+ }
256+
231257// Implement the interface for all query types
232258func (q * ListIssuesQueryTypeWithLabels ) GetIssueFragment () IssueQueryFragment {
233259 return q .Repository .Issues
@@ -986,6 +1012,114 @@ func ReprioritizeSubIssue(ctx context.Context, client *github.Client, owner stri
9861012 return utils .NewToolResultText (string (r )), nil
9871013}
9881014
1015+ // fetchIssueFieldValuesByNodeID runs one GraphQL nodes() query for the given REST issues and
1016+ // returns a map of node_id -> flattened field values. Issues without a node_id are skipped, and
1017+ // an empty result set short-circuits the round-trip.
1018+ func fetchIssueFieldValuesByNodeID (ctx context.Context , gqlClient * githubv4.Client , issues []* github.Issue ) (map [string ][]MinimalIssueFieldValue , error ) {
1019+ ids := make ([]githubv4.ID , 0 , len (issues ))
1020+ for _ , iss := range issues {
1021+ if iss == nil || iss .NodeID == nil || * iss .NodeID == "" {
1022+ continue
1023+ }
1024+ ids = append (ids , githubv4 .ID (* iss .NodeID ))
1025+ }
1026+ if len (ids ) == 0 {
1027+ return nil , nil
1028+ }
1029+
1030+ var q searchIssuesNodesQuery
1031+ if err := gqlClient .Query (ctx , & q , map [string ]any {"ids" : ids }); err != nil {
1032+ return nil , err
1033+ }
1034+
1035+ result := make (map [string ][]MinimalIssueFieldValue , len (q .Nodes ))
1036+ for _ , n := range q .Nodes {
1037+ if n .Issue .ID == nil {
1038+ continue
1039+ }
1040+ idStr := fmt .Sprintf ("%v" , n .Issue .ID )
1041+ if idStr == "" {
1042+ continue
1043+ }
1044+ vals := make ([]MinimalIssueFieldValue , 0 , len (n .Issue .IssueFieldValues .Nodes ))
1045+ for _ , fv := range n .Issue .IssueFieldValues .Nodes {
1046+ if m , ok := fragmentToMinimalIssueFieldValue (fv ); ok {
1047+ vals = append (vals , m )
1048+ }
1049+ }
1050+ result [idStr ] = vals
1051+ }
1052+ return result , nil
1053+ }
1054+
1055+ // searchIssuesHandler runs the REST issues search and enriches each hit with custom field values
1056+ // fetched via a single follow-up GraphQL nodes() query.
1057+ func searchIssuesHandler (ctx context.Context , deps ToolDependencies , args map [string ]any ) (* mcp.CallToolResult , error ) {
1058+ const errorPrefix = "failed to search issues"
1059+
1060+ query , opts , err := prepareSearchArgs (args , "issue" )
1061+ if err != nil {
1062+ return utils .NewToolResultError (err .Error ()), nil
1063+ }
1064+
1065+ client , err := deps .GetClient (ctx )
1066+ if err != nil {
1067+ return utils .NewToolResultErrorFromErr (errorPrefix + ": failed to get GitHub client" , err ), nil
1068+ }
1069+ result , resp , err := client .Search .Issues (ctx , query , opts )
1070+ if err != nil {
1071+ return utils .NewToolResultErrorFromErr (errorPrefix , err ), nil
1072+ }
1073+ defer func () { _ = resp .Body .Close () }()
1074+
1075+ if resp .StatusCode != http .StatusOK {
1076+ body , err := io .ReadAll (resp .Body )
1077+ if err != nil {
1078+ return utils .NewToolResultErrorFromErr (errorPrefix + ": failed to read response body" , err ), nil
1079+ }
1080+ return ghErrors .NewGitHubAPIStatusErrorResponse (ctx , errorPrefix , resp , body ), nil
1081+ }
1082+
1083+ var fieldValuesByID map [string ][]MinimalIssueFieldValue
1084+ if len (result .Issues ) > 0 {
1085+ gqlClient , err := deps .GetGQLClient (ctx )
1086+ if err != nil {
1087+ return utils .NewToolResultErrorFromErr (errorPrefix + ": failed to get GitHub GraphQL client" , err ), nil
1088+ }
1089+ fieldValuesByID , err = fetchIssueFieldValuesByNodeID (ctx , gqlClient , result .Issues )
1090+ if err != nil {
1091+ return ghErrors .NewGitHubGraphQLErrorResponse (ctx , errorPrefix + ": failed to fetch issue field values" , err ), nil
1092+ }
1093+ }
1094+
1095+ items := make ([]SearchIssueResult , 0 , len (result .Issues ))
1096+ for _ , iss := range result .Issues {
1097+ hit := SearchIssueResult {Issue : iss }
1098+ if iss != nil && iss .NodeID != nil {
1099+ hit .FieldValues = fieldValuesByID [* iss .NodeID ]
1100+ }
1101+ items = append (items , hit )
1102+ }
1103+
1104+ response := SearchIssuesResponse {
1105+ Total : result .Total ,
1106+ IncompleteResults : result .IncompleteResults ,
1107+ Items : items ,
1108+ }
1109+
1110+ r , err := json .Marshal (response )
1111+ if err != nil {
1112+ return utils .NewToolResultErrorFromErr (errorPrefix + ": failed to marshal response" , err ), nil
1113+ }
1114+
1115+ callResult := utils .NewToolResultText (string (r ))
1116+ if deps .GetFlags (ctx ).InsidersMode {
1117+ fn := searchIssuesIFCPostProcess (deps )
1118+ fn (ctx , result , callResult )
1119+ }
1120+ return callResult , nil
1121+ }
1122+
9891123// SearchIssues creates a tool to search for issues.
9901124func SearchIssues (t translations.TranslationHelperFunc ) inventory.ServerTool {
9911125 schema := & jsonschema.Schema {
@@ -1043,11 +1177,7 @@ func SearchIssues(t translations.TranslationHelperFunc) inventory.ServerTool {
10431177 },
10441178 []scopes.Scope {scopes .Repo },
10451179 func (ctx context.Context , deps ToolDependencies , _ * mcp.CallToolRequest , args map [string ]any ) (* mcp.CallToolResult , any , error ) {
1046- var options []searchOption
1047- if deps .GetFlags (ctx ).InsidersMode {
1048- options = append (options , withSearchPostProcess (searchIssuesIFCPostProcess (deps )))
1049- }
1050- result , err := searchHandler (ctx , deps .GetClient , args , "issue" , "failed to search issues" , options ... )
1180+ result , err := searchIssuesHandler (ctx , deps , args )
10511181 return result , nil , err
10521182 })
10531183}
0 commit comments