@@ -1047,7 +1047,7 @@ func SearchIssues(t translations.TranslationHelperFunc) inventory.ServerTool {
10471047 if deps .GetFlags (ctx ).InsidersMode {
10481048 options = append (options , withSearchPostProcess (searchIssuesIFCPostProcess (deps )))
10491049 }
1050- result , err := searchHandler (ctx , deps . GetClient , args , "issue" , "failed to search issues" , options ... )
1050+ result , err := searchIssuesHandler (ctx , deps , args , options ... )
10511051 return result , nil , err
10521052 })
10531053}
@@ -1134,6 +1134,142 @@ func parseRepositoryURL(repoURL string) (string, string, bool) {
11341134 return parts [0 ], parts [1 ], true
11351135}
11361136
1137+ // SearchIssueResult wraps a REST search hit with its custom issue field values, fetched in a follow-up GraphQL nodes() query.
1138+ type SearchIssueResult struct {
1139+ * github.Issue
1140+ FieldValues []MinimalIssueFieldValue `json:"field_values,omitempty"`
1141+ }
1142+
1143+ // SearchIssuesResponse mirrors the REST IssuesSearchResult JSON shape and adds field_values
1144+ // per item, sourced from a single GraphQL nodes() round-trip.
1145+ type SearchIssuesResponse struct {
1146+ Total * int `json:"total_count,omitempty"`
1147+ IncompleteResults * bool `json:"incomplete_results,omitempty"`
1148+ Items []SearchIssueResult `json:"items"`
1149+ }
1150+
1151+ // searchIssuesNodesQuery batches a nodes(ids:) lookup over the REST search results to retrieve
1152+ // each issue's custom field values in a single GraphQL request.
1153+ type searchIssuesNodesQuery struct {
1154+ Nodes []struct {
1155+ Issue struct {
1156+ ID githubv4.ID
1157+ IssueFieldValues struct {
1158+ Nodes []IssueFieldValueFragment
1159+ } `graphql:"issueFieldValues(first: 25)"`
1160+ } `graphql:"... on Issue"`
1161+ } `graphql:"nodes(ids: $ids)"`
1162+ }
1163+
1164+ // fetchIssueFieldValuesByNodeID runs one GraphQL nodes() query for the given REST issues and
1165+ // returns a map of node_id -> flattened field values. Issues without a node_id are skipped, and
1166+ // an empty result set short-circuits the round-trip.
1167+ func fetchIssueFieldValuesByNodeID (ctx context.Context , gqlClient * githubv4.Client , issues []* github.Issue ) (map [string ][]MinimalIssueFieldValue , error ) {
1168+ ids := make ([]githubv4.ID , 0 , len (issues ))
1169+ for _ , iss := range issues {
1170+ if iss == nil || iss .NodeID == nil || * iss .NodeID == "" {
1171+ continue
1172+ }
1173+ ids = append (ids , githubv4 .ID (* iss .NodeID ))
1174+ }
1175+ if len (ids ) == 0 {
1176+ return nil , nil
1177+ }
1178+
1179+ var q searchIssuesNodesQuery
1180+ if err := gqlClient .Query (ctx , & q , map [string ]any {"ids" : ids }); err != nil {
1181+ return nil , err
1182+ }
1183+
1184+ result := make (map [string ][]MinimalIssueFieldValue , len (q .Nodes ))
1185+ for _ , n := range q .Nodes {
1186+ idStr , ok := n .Issue .ID .(string )
1187+ if ! ok || idStr == "" {
1188+ continue
1189+ }
1190+ vals := make ([]MinimalIssueFieldValue , 0 , len (n .Issue .IssueFieldValues .Nodes ))
1191+ for _ , fv := range n .Issue .IssueFieldValues .Nodes {
1192+ if m , ok := fragmentToMinimalIssueFieldValue (fv ); ok {
1193+ vals = append (vals , m )
1194+ }
1195+ }
1196+ result [idStr ] = vals
1197+ }
1198+ return result , nil
1199+ }
1200+
1201+ // searchIssuesHandler runs the REST issues search, enriches each hit with custom field values
1202+ // fetched via a single follow-up GraphQL nodes() query, and applies any post-process options
1203+ // (e.g. IFC labelling).
1204+ func searchIssuesHandler (ctx context.Context , deps ToolDependencies , args map [string ]any , options ... searchOption ) (* mcp.CallToolResult , error ) {
1205+ const errorPrefix = "failed to search issues"
1206+
1207+ query , opts , err := prepareSearchArgs (args , "issue" )
1208+ if err != nil {
1209+ return utils .NewToolResultError (err .Error ()), nil
1210+ }
1211+
1212+ client , err := deps .GetClient (ctx )
1213+ if err != nil {
1214+ return utils .NewToolResultErrorFromErr (errorPrefix + ": failed to get GitHub client" , err ), nil
1215+ }
1216+ result , resp , err := client .Search .Issues (ctx , query , opts )
1217+ if err != nil {
1218+ return utils .NewToolResultErrorFromErr (errorPrefix , err ), nil
1219+ }
1220+ defer func () { _ = resp .Body .Close () }()
1221+
1222+ if resp .StatusCode != http .StatusOK {
1223+ body , err := io .ReadAll (resp .Body )
1224+ if err != nil {
1225+ return utils .NewToolResultErrorFromErr (errorPrefix + ": failed to read response body" , err ), nil
1226+ }
1227+ return ghErrors .NewGitHubAPIStatusErrorResponse (ctx , errorPrefix , resp , body ), nil
1228+ }
1229+
1230+ var fieldValuesByID map [string ][]MinimalIssueFieldValue
1231+ if len (result .Issues ) > 0 {
1232+ gqlClient , err := deps .GetGQLClient (ctx )
1233+ if err != nil {
1234+ return utils .NewToolResultErrorFromErr (errorPrefix + ": failed to get GitHub GraphQL client" , err ), nil
1235+ }
1236+ fieldValuesByID , err = fetchIssueFieldValuesByNodeID (ctx , gqlClient , result .Issues )
1237+ if err != nil {
1238+ return ghErrors .NewGitHubGraphQLErrorResponse (ctx , errorPrefix + ": failed to fetch issue field values" , err ), nil
1239+ }
1240+ }
1241+
1242+ items := make ([]SearchIssueResult , 0 , len (result .Issues ))
1243+ for _ , iss := range result .Issues {
1244+ hit := SearchIssueResult {Issue : iss }
1245+ if iss != nil && iss .NodeID != nil {
1246+ hit .FieldValues = fieldValuesByID [* iss .NodeID ]
1247+ }
1248+ items = append (items , hit )
1249+ }
1250+
1251+ response := SearchIssuesResponse {
1252+ Total : result .Total ,
1253+ IncompleteResults : result .IncompleteResults ,
1254+ Items : items ,
1255+ }
1256+
1257+ r , err := json .Marshal (response )
1258+ if err != nil {
1259+ return utils .NewToolResultErrorFromErr (errorPrefix + ": failed to marshal response" , err ), nil
1260+ }
1261+
1262+ callResult := utils .NewToolResultText (string (r ))
1263+ cfg := searchConfig {}
1264+ for _ , opt := range options {
1265+ opt (& cfg )
1266+ }
1267+ if cfg .postProcess != nil {
1268+ cfg .postProcess (ctx , result , callResult )
1269+ }
1270+ return callResult , nil
1271+ }
1272+
11371273// IssueWrite creates a tool to create a new or update an existing issue in a GitHub repository.
11381274// IssueWriteUIResourceURI is the URI for the issue_write tool's MCP App UI resource.
11391275const IssueWriteUIResourceURI = "ui://github-mcp-server/issue-write"
0 commit comments