55 "encoding/json"
66 "fmt"
77 "maps"
8+ "strconv"
89 "strings"
910
1011 ghErrors "github.com/github/github-mcp-server/pkg/errors"
@@ -1009,6 +1010,10 @@ func GranularSetIssueFields(t translations.TranslationHelperFunc) inventory.Serv
10091010 return utils .NewToolResultError ("fields array must not be empty" ), nil , nil
10101011 }
10111012
1013+ // needsREST is set when any field has rationale or suggest, since
1014+ // those are not yet supported on the GraphQL setIssueFieldValue
1015+ // mutation. The whole request is then dispatched via REST.
1016+ needsREST := false
10121017 issueFields := make ([]IssueFieldCreateOrUpdateInput , 0 , len (fieldMaps ))
10131018 for _ , fieldMap := range fieldMaps {
10141019 fieldID , err := RequiredParam [string ](fieldMap , "field_id" )
@@ -1070,6 +1075,7 @@ func GranularSetIssueFields(t translations.TranslationHelperFunc) inventory.Serv
10701075 }
10711076 if rationale != "" {
10721077 input .Rationale = githubv4 .NewString (githubv4 .String (rationale ))
1078+ needsREST = true
10731079 }
10741080 }
10751081
@@ -1080,11 +1086,16 @@ func GranularSetIssueFields(t translations.TranslationHelperFunc) inventory.Serv
10801086 if isSuggestion {
10811087 suggestVal := githubv4 .Boolean (true )
10821088 input .Suggest = & suggestVal
1089+ needsREST = true
10831090 }
10841091
10851092 issueFields = append (issueFields , input )
10861093 }
10871094
1095+ if needsREST {
1096+ return setIssueFieldsViaREST (ctx , deps , owner , repo , issueNumber , issueFields )
1097+ }
1098+
10881099 gqlClient , err := deps .GetGQLClient (ctx )
10891100 if err != nil {
10901101 return utils .NewToolResultErrorFromErr ("failed to get GitHub GraphQL client" , err ), nil , nil
@@ -1143,3 +1154,128 @@ func GranularSetIssueFields(t translations.TranslationHelperFunc) inventory.Serv
11431154 st .FeatureFlagEnable = FeatureFlagIssuesGranular
11441155 return st
11451156}
1157+
1158+ // issueFieldValueRESTRequest is the JSON body shape accepted by the REST
1159+ // update-issue endpoint when setting issue field values with rationale or
1160+ // suggestion. The endpoint expects integer database IDs and a single string
1161+ // value per field. Used as a fallback to the GraphQL setIssueFieldValue
1162+ // mutation, which does not (yet) support rationale or suggest.
1163+ type issueFieldValueRESTRequest struct {
1164+ IssueFieldValues []issueFieldValueRESTEntry `json:"issue_field_values"`
1165+ }
1166+
1167+ type issueFieldValueRESTEntry struct {
1168+ FieldID int64 `json:"field_id"`
1169+ Value string `json:"value"`
1170+ Rationale string `json:"rationale,omitempty"`
1171+ Suggest bool `json:"suggest,omitempty"`
1172+ }
1173+
1174+ // setIssueFieldsViaREST dispatches a set_issue_fields request through the
1175+ // REST update-issue endpoint. It resolves each field's integer database ID
1176+ // (and, for single_select fields, each option's name) using the GraphQL
1177+ // issue field definitions, then PATCHes the issue.
1178+ func setIssueFieldsViaREST (
1179+ ctx context.Context ,
1180+ deps ToolDependencies ,
1181+ owner , repo string ,
1182+ issueNumber int ,
1183+ issueFields []IssueFieldCreateOrUpdateInput ,
1184+ ) (* mcp.CallToolResult , any , error ) {
1185+ // Delete is not supported on the REST shape we use here.
1186+ for _ , f := range issueFields {
1187+ if f .Delete != nil && bool (* f .Delete ) {
1188+ return utils .NewToolResultError ("delete is not supported when rationale or is_suggestion is set" ), nil , nil
1189+ }
1190+ }
1191+
1192+ gqlClient , err := deps .GetGQLClient (ctx )
1193+ if err != nil {
1194+ return utils .NewToolResultErrorFromErr ("failed to get GitHub GraphQL client" , err ), nil , nil
1195+ }
1196+
1197+ defs , err := fetchIssueFields (ctx , gqlClient , owner , repo )
1198+ if err != nil {
1199+ return ghErrors .NewGitHubGraphQLErrorResponse (ctx , "failed to look up issue field definitions" , err ), nil , nil
1200+ }
1201+
1202+ fieldByNodeID := make (map [string ]IssueField , len (defs ))
1203+ for _ , f := range defs {
1204+ fieldByNodeID [f .ID ] = f
1205+ }
1206+
1207+ entries := make ([]issueFieldValueRESTEntry , 0 , len (issueFields ))
1208+ for _ , f := range issueFields {
1209+ nodeID := fmt .Sprintf ("%v" , f .FieldID )
1210+ def , ok := fieldByNodeID [nodeID ]
1211+ if ! ok {
1212+ return utils .NewToolResultError (fmt .Sprintf ("unknown field_id %q for %s/%s" , nodeID , owner , repo )), nil , nil
1213+ }
1214+ if def .DatabaseID == 0 {
1215+ return utils .NewToolResultError (fmt .Sprintf ("could not resolve database ID for field %q" , def .Name )), nil , nil
1216+ }
1217+
1218+ var value string
1219+ switch {
1220+ case f .TextValue != nil :
1221+ value = string (* f .TextValue )
1222+ case f .NumberValue != nil :
1223+ value = strconv .FormatFloat (float64 (* f .NumberValue ), 'f' , - 1 , 64 )
1224+ case f .DateValue != nil :
1225+ value = string (* f .DateValue )
1226+ case f .SingleSelectOptionID != nil :
1227+ optID := fmt .Sprintf ("%v" , * f .SingleSelectOptionID )
1228+ optName := ""
1229+ for _ , opt := range def .Options {
1230+ if opt .ID == optID {
1231+ optName = opt .Name
1232+ break
1233+ }
1234+ }
1235+ if optName == "" {
1236+ return utils .NewToolResultError (fmt .Sprintf ("unknown single_select_option_id %q for field %q" , optID , def .Name )), nil , nil
1237+ }
1238+ value = optName
1239+ }
1240+
1241+ entry := issueFieldValueRESTEntry {
1242+ FieldID : def .DatabaseID ,
1243+ Value : value ,
1244+ }
1245+ if f .Rationale != nil {
1246+ entry .Rationale = string (* f .Rationale )
1247+ }
1248+ if f .Suggest != nil {
1249+ entry .Suggest = bool (* f .Suggest )
1250+ }
1251+ entries = append (entries , entry )
1252+ }
1253+
1254+ client , err := deps .GetClient (ctx )
1255+ if err != nil {
1256+ return utils .NewToolResultErrorFromErr ("failed to get GitHub client" , err ), nil , nil
1257+ }
1258+
1259+ body := & issueFieldValueRESTRequest {IssueFieldValues : entries }
1260+ apiURL := fmt .Sprintf ("repos/%s/%s/issues/%d" , owner , repo , issueNumber )
1261+ req , err := client .NewRequest (ctx , "PATCH" , apiURL , body )
1262+ if err != nil {
1263+ return utils .NewToolResultErrorFromErr ("failed to create request" , err ), nil , nil
1264+ }
1265+
1266+ issue := & github.Issue {}
1267+ resp , err := client .Do (req , issue )
1268+ if err != nil {
1269+ return ghErrors .NewGitHubAPIErrorResponse (ctx , "failed to set issue field values" , resp , err ), nil , nil
1270+ }
1271+ defer func () { _ = resp .Body .Close () }()
1272+
1273+ r , err := json .Marshal (MinimalResponse {
1274+ ID : fmt .Sprintf ("%d" , issue .GetID ()),
1275+ URL : issue .GetHTMLURL (),
1276+ })
1277+ if err != nil {
1278+ return utils .NewToolResultErrorFromErr ("failed to marshal response" , err ), nil , nil
1279+ }
1280+ return utils .NewToolResultText (string (r )), nil , nil
1281+ }
0 commit comments