@@ -34,6 +34,36 @@ type CloseIssueInput struct {
3434// Used to extend the functionality of the githubv4 library to support closing issues as duplicates.
3535type IssueClosedStateReason string
3636
37+ // IssueWriteFieldInput is a user-friendly issue field input for issue_write.
38+ // Field IDs and option IDs are resolved internally before calling the REST API.
39+ type IssueWriteFieldInput struct {
40+ FieldName string
41+ Value any
42+ FieldOptionName string
43+ }
44+
45+ type issueFieldMetadataOption struct {
46+ DatabaseID githubv4.Int `graphql:"databaseId"`
47+ Name githubv4.String
48+ }
49+
50+ type issueFieldMetadataNode struct {
51+ DatabaseID githubv4.Int `graphql:"databaseId"`
52+ Name githubv4.String
53+ DataType githubv4.String
54+ SingleSelectField struct {
55+ Options []issueFieldMetadataOption `graphql:"options"`
56+ } `graphql:"... on IssueFieldSingleSelect"`
57+ }
58+
59+ type issueFieldMetadataQuery struct {
60+ Repository struct {
61+ IssueFields struct {
62+ Nodes []issueFieldMetadataNode
63+ } `graphql:"issueFields(first: 100)"`
64+ } `graphql:"repository(owner: $owner, name: $repo)"`
65+ }
66+
3767const (
3868 IssueClosedStateReasonCompleted IssueClosedStateReason = "COMPLETED"
3969 IssueClosedStateReasonDuplicate IssueClosedStateReason = "DUPLICATE"
@@ -102,6 +132,127 @@ func getCloseStateReason(stateReason string) IssueClosedStateReason {
102132 }
103133}
104134
135+ func optionalIssueWriteFields (args map [string ]any ) ([]IssueWriteFieldInput , error ) {
136+ issueFieldsRaw , exists := args ["issue_fields" ]
137+ if ! exists {
138+ return nil , nil
139+ }
140+
141+ var inputMaps []map [string ]any
142+ switch v := issueFieldsRaw .(type ) {
143+ case []any :
144+ for _ , item := range v {
145+ itemMap , ok := item .(map [string ]any )
146+ if ! ok {
147+ return nil , fmt .Errorf ("each issue_fields item must be an object" )
148+ }
149+ inputMaps = append (inputMaps , itemMap )
150+ }
151+ case []map [string ]any :
152+ inputMaps = v
153+ default :
154+ return nil , fmt .Errorf ("issue_fields must be an array" )
155+ }
156+
157+ issueFields := make ([]IssueWriteFieldInput , 0 , len (inputMaps ))
158+ for _ , itemMap := range inputMaps {
159+ fieldName , err := RequiredParam [string ](itemMap , "field_name" )
160+ if err != nil || strings .TrimSpace (fieldName ) == "" {
161+ return nil , fmt .Errorf ("field_name is required for each issue_fields item" )
162+ }
163+
164+ fieldOptionName , err := OptionalParam [string ](itemMap , "field_option_name" )
165+ if err != nil {
166+ return nil , err
167+ }
168+
169+ value , hasValue := itemMap ["value" ]
170+ if hasValue && value == nil {
171+ return nil , fmt .Errorf ("value cannot be null for field %q" , fieldName )
172+ }
173+
174+ if hasValue && fieldOptionName != "" {
175+ return nil , fmt .Errorf ("issue field %q cannot specify both value and field_option_name" , fieldName )
176+ }
177+
178+ if ! hasValue && fieldOptionName == "" {
179+ return nil , fmt .Errorf ("issue field %q must specify either value or field_option_name" , fieldName )
180+ }
181+
182+ issueFields = append (issueFields , IssueWriteFieldInput {
183+ FieldName : fieldName ,
184+ Value : value ,
185+ FieldOptionName : fieldOptionName ,
186+ })
187+ }
188+
189+ return issueFields , nil
190+ }
191+
192+ func resolveIssueRequestFieldValues (ctx context.Context , gqlClient * githubv4.Client , owner , repo string , issueFields []IssueWriteFieldInput ) ([]* github.IssueRequestFieldValue , error ) {
193+ if len (issueFields ) == 0 {
194+ return nil , nil
195+ }
196+
197+ query := issueFieldMetadataQuery {}
198+ vars := map [string ]any {
199+ "owner" : githubv4 .String (owner ),
200+ "repo" : githubv4 .String (repo ),
201+ }
202+ if err := gqlClient .Query (ctx , & query , vars ); err != nil {
203+ return nil , fmt .Errorf ("failed to query issue fields metadata: %w" , err )
204+ }
205+
206+ fieldByName := make (map [string ]issueFieldMetadataNode , len (query .Repository .IssueFields .Nodes ))
207+ for _ , field := range query .Repository .IssueFields .Nodes {
208+ fieldByName [strings .ToLower (strings .TrimSpace (string (field .Name )))] = field
209+ }
210+
211+ resolved := make ([]* github.IssueRequestFieldValue , 0 , len (issueFields ))
212+ for _ , fieldInput := range issueFields {
213+ field , ok := fieldByName [strings .ToLower (strings .TrimSpace (fieldInput .FieldName ))]
214+ if ! ok {
215+ return nil , fmt .Errorf ("issue field %q was not found in %s/%s" , fieldInput .FieldName , owner , repo )
216+ }
217+
218+ fieldID := int64 (field .DatabaseID )
219+ if fieldID == 0 {
220+ return nil , fmt .Errorf ("issue field %q is missing databaseId" , fieldInput .FieldName )
221+ }
222+
223+ resolvedValue := fieldInput .Value
224+ if fieldInput .FieldOptionName != "" {
225+ if ! strings .EqualFold (string (field .DataType ), "single_select" ) {
226+ return nil , fmt .Errorf ("issue field %q is %q, so field_option_name cannot be used" , fieldInput .FieldName , field .DataType )
227+ }
228+
229+ optionFound := false
230+ for _ , option := range field .SingleSelectField .Options {
231+ if strings .EqualFold (strings .TrimSpace (string (option .Name )), strings .TrimSpace (fieldInput .FieldOptionName )) {
232+ optionID := int64 (option .DatabaseID )
233+ if optionID == 0 {
234+ return nil , fmt .Errorf ("issue field option %q on field %q is missing databaseId" , fieldInput .FieldOptionName , fieldInput .FieldName )
235+ }
236+ resolvedValue = optionID
237+ optionFound = true
238+ break
239+ }
240+ }
241+
242+ if ! optionFound {
243+ return nil , fmt .Errorf ("issue field option %q was not found for field %q" , fieldInput .FieldOptionName , fieldInput .FieldName )
244+ }
245+ }
246+
247+ resolved = append (resolved , & github.IssueRequestFieldValue {
248+ FieldID : fieldID ,
249+ Value : resolvedValue ,
250+ })
251+ }
252+
253+ return resolved , nil
254+ }
255+
105256// IssueFragment represents a fragment of an issue node in the GraphQL API.
106257type IssueFragment struct {
107258 Number githubv4.Int
@@ -1053,6 +1204,27 @@ Options are:
10531204 Type : "number" ,
10541205 Description : "Issue number that this issue is a duplicate of. Only used when state_reason is 'duplicate'." ,
10551206 },
1207+ "issue_fields" : {
1208+ Type : "array" ,
1209+ Description : "Issue field values to set. Each item requires field_name and either value or field_option_name. field_option_name is for single-select fields and is resolved to the corresponding option ID automatically." ,
1210+ Items : & jsonschema.Schema {
1211+ Type : "object" ,
1212+ Properties : map [string ]* jsonschema.Schema {
1213+ "field_name" : {
1214+ Type : "string" ,
1215+ Description : "Issue field name" ,
1216+ },
1217+ "value" : {
1218+ Description : "Value for text/number/date/single-select fields. For single-select, you can use field_option_name instead." ,
1219+ },
1220+ "field_option_name" : {
1221+ Type : "string" ,
1222+ Description : "Single-select option name to resolve and set for the field" ,
1223+ },
1224+ },
1225+ Required : []string {"field_name" },
1226+ },
1227+ },
10561228 },
10571229 Required : []string {"method" , "owner" , "repo" },
10581230 },
@@ -1154,6 +1326,11 @@ Options are:
11541326 return utils .NewToolResultError ("duplicate_of can only be used when state_reason is 'duplicate'" ), nil , nil
11551327 }
11561328
1329+ issueFields , err := optionalIssueWriteFields (args )
1330+ if err != nil {
1331+ return utils .NewToolResultError (err .Error ()), nil , nil
1332+ }
1333+
11571334 client , err := deps .GetClient (ctx )
11581335 if err != nil {
11591336 return utils .NewToolResultErrorFromErr ("failed to get GitHub client" , err ), nil , nil
@@ -1164,16 +1341,21 @@ Options are:
11641341 return utils .NewToolResultErrorFromErr ("failed to get GraphQL client" , err ), nil , nil
11651342 }
11661343
1344+ issueFieldValues , err := resolveIssueRequestFieldValues (ctx , gqlClient , owner , repo , issueFields )
1345+ if err != nil {
1346+ return utils .NewToolResultError (fmt .Sprintf ("failed to resolve issue_fields: %v" , err )), nil , nil
1347+ }
1348+
11671349 switch method {
11681350 case "create" :
1169- result , err := CreateIssue (ctx , client , owner , repo , title , body , assignees , labels , milestoneNum , issueType )
1351+ result , err := CreateIssue (ctx , client , owner , repo , title , body , assignees , labels , milestoneNum , issueType , issueFieldValues )
11701352 return result , nil , err
11711353 case "update" :
11721354 issueNumber , err := RequiredInt (args , "issue_number" )
11731355 if err != nil {
11741356 return utils .NewToolResultError (err .Error ()), nil , nil
11751357 }
1176- result , err := UpdateIssue (ctx , client , gqlClient , owner , repo , issueNumber , title , body , assignees , labels , milestoneNum , issueType , state , stateReason , duplicateOf )
1358+ result , err := UpdateIssue (ctx , client , gqlClient , owner , repo , issueNumber , title , body , assignees , labels , milestoneNum , issueType , issueFieldValues , state , stateReason , duplicateOf )
11771359 return result , nil , err
11781360 default :
11791361 return utils .NewToolResultError ("invalid method, must be either 'create' or 'update'" ), nil , nil
@@ -1183,17 +1365,18 @@ Options are:
11831365 return st
11841366}
11851367
1186- func CreateIssue (ctx context.Context , client * github.Client , owner string , repo string , title string , body string , assignees []string , labels []string , milestoneNum int , issueType string ) (* mcp.CallToolResult , error ) {
1368+ func CreateIssue (ctx context.Context , client * github.Client , owner string , repo string , title string , body string , assignees []string , labels []string , milestoneNum int , issueType string , issueFieldValues [] * github. IssueRequestFieldValue ) (* mcp.CallToolResult , error ) {
11871369 if title == "" {
11881370 return utils .NewToolResultError ("missing required parameter: title" ), nil
11891371 }
11901372
11911373 // Create the issue request
11921374 issueRequest := & github.IssueRequest {
1193- Title : github .Ptr (title ),
1194- Body : github .Ptr (body ),
1195- Assignees : & assignees ,
1196- Labels : & labels ,
1375+ Title : github .Ptr (title ),
1376+ Body : github .Ptr (body ),
1377+ Assignees : & assignees ,
1378+ Labels : & labels ,
1379+ IssueFieldValues : issueFieldValues ,
11971380 }
11981381
11991382 if milestoneNum != 0 {
@@ -1236,7 +1419,7 @@ func CreateIssue(ctx context.Context, client *github.Client, owner string, repo
12361419 return utils .NewToolResultText (string (r )), nil
12371420}
12381421
1239- func UpdateIssue (ctx context.Context , client * github.Client , gqlClient * githubv4.Client , owner string , repo string , issueNumber int , title string , body string , assignees []string , labels []string , milestoneNum int , issueType string , state string , stateReason string , duplicateOf int ) (* mcp.CallToolResult , error ) {
1422+ func UpdateIssue (ctx context.Context , client * github.Client , gqlClient * githubv4.Client , owner string , repo string , issueNumber int , title string , body string , assignees []string , labels []string , milestoneNum int , issueType string , issueFieldValues [] * github. IssueRequestFieldValue , state string , stateReason string , duplicateOf int ) (* mcp.CallToolResult , error ) {
12401423 // Create the issue request with only provided fields
12411424 issueRequest := & github.IssueRequest {}
12421425
@@ -1265,6 +1448,10 @@ func UpdateIssue(ctx context.Context, client *github.Client, gqlClient *githubv4
12651448 issueRequest .Type = github .Ptr (issueType )
12661449 }
12671450
1451+ if len (issueFieldValues ) > 0 {
1452+ issueRequest .IssueFieldValues = issueFieldValues
1453+ }
1454+
12681455 updatedIssue , resp , err := client .Issues .Edit (ctx , owner , repo , issueNumber , issueRequest )
12691456 if err != nil {
12701457 return ghErrors .NewGitHubAPIErrorResponse (ctx ,
0 commit comments