@@ -35,6 +35,36 @@ type CloseIssueInput struct {
3535// Used to extend the functionality of the githubv4 library to support closing issues as duplicates.
3636type IssueClosedStateReason string
3737
38+ // IssueWriteFieldInput is a user-friendly issue field input for issue_write.
39+ // Field IDs and option IDs are resolved internally before calling the REST API.
40+ type IssueWriteFieldInput struct {
41+ FieldName string
42+ Value any
43+ FieldOptionName string
44+ }
45+
46+ type issueFieldMetadataOption struct {
47+ DatabaseID githubv4.Int `graphql:"databaseId"`
48+ Name githubv4.String
49+ }
50+
51+ type issueFieldMetadataNode struct {
52+ DatabaseID githubv4.Int `graphql:"databaseId"`
53+ Name githubv4.String
54+ DataType githubv4.String
55+ SingleSelectField struct {
56+ Options []issueFieldMetadataOption `graphql:"options"`
57+ } `graphql:"... on IssueFieldSingleSelect"`
58+ }
59+
60+ type issueFieldMetadataQuery struct {
61+ Repository struct {
62+ IssueFields struct {
63+ Nodes []issueFieldMetadataNode
64+ } `graphql:"issueFields(first: 100)"`
65+ } `graphql:"repository(owner: $owner, name: $repo)"`
66+ }
67+
3868const (
3969 IssueClosedStateReasonCompleted IssueClosedStateReason = "COMPLETED"
4070 IssueClosedStateReasonDuplicate IssueClosedStateReason = "DUPLICATE"
@@ -103,6 +133,127 @@ func getCloseStateReason(stateReason string) IssueClosedStateReason {
103133 }
104134}
105135
136+ func optionalIssueWriteFields (args map [string ]any ) ([]IssueWriteFieldInput , error ) {
137+ issueFieldsRaw , exists := args ["issue_fields" ]
138+ if ! exists {
139+ return nil , nil
140+ }
141+
142+ var inputMaps []map [string ]any
143+ switch v := issueFieldsRaw .(type ) {
144+ case []any :
145+ for _ , item := range v {
146+ itemMap , ok := item .(map [string ]any )
147+ if ! ok {
148+ return nil , fmt .Errorf ("each issue_fields item must be an object" )
149+ }
150+ inputMaps = append (inputMaps , itemMap )
151+ }
152+ case []map [string ]any :
153+ inputMaps = v
154+ default :
155+ return nil , fmt .Errorf ("issue_fields must be an array" )
156+ }
157+
158+ issueFields := make ([]IssueWriteFieldInput , 0 , len (inputMaps ))
159+ for _ , itemMap := range inputMaps {
160+ fieldName , err := RequiredParam [string ](itemMap , "field_name" )
161+ if err != nil || strings .TrimSpace (fieldName ) == "" {
162+ return nil , fmt .Errorf ("field_name is required for each issue_fields item" )
163+ }
164+
165+ fieldOptionName , err := OptionalParam [string ](itemMap , "field_option_name" )
166+ if err != nil {
167+ return nil , err
168+ }
169+
170+ value , hasValue := itemMap ["value" ]
171+ if hasValue && value == nil {
172+ return nil , fmt .Errorf ("value cannot be null for field %q" , fieldName )
173+ }
174+
175+ if hasValue && fieldOptionName != "" {
176+ return nil , fmt .Errorf ("issue field %q cannot specify both value and field_option_name" , fieldName )
177+ }
178+
179+ if ! hasValue && fieldOptionName == "" {
180+ return nil , fmt .Errorf ("issue field %q must specify either value or field_option_name" , fieldName )
181+ }
182+
183+ issueFields = append (issueFields , IssueWriteFieldInput {
184+ FieldName : fieldName ,
185+ Value : value ,
186+ FieldOptionName : fieldOptionName ,
187+ })
188+ }
189+
190+ return issueFields , nil
191+ }
192+
193+ func resolveIssueRequestFieldValues (ctx context.Context , gqlClient * githubv4.Client , owner , repo string , issueFields []IssueWriteFieldInput ) ([]* github.IssueRequestFieldValue , error ) {
194+ if len (issueFields ) == 0 {
195+ return nil , nil
196+ }
197+
198+ query := issueFieldMetadataQuery {}
199+ vars := map [string ]any {
200+ "owner" : githubv4 .String (owner ),
201+ "repo" : githubv4 .String (repo ),
202+ }
203+ if err := gqlClient .Query (ctx , & query , vars ); err != nil {
204+ return nil , fmt .Errorf ("failed to query issue fields metadata: %w" , err )
205+ }
206+
207+ fieldByName := make (map [string ]issueFieldMetadataNode , len (query .Repository .IssueFields .Nodes ))
208+ for _ , field := range query .Repository .IssueFields .Nodes {
209+ fieldByName [strings .ToLower (strings .TrimSpace (string (field .Name )))] = field
210+ }
211+
212+ resolved := make ([]* github.IssueRequestFieldValue , 0 , len (issueFields ))
213+ for _ , fieldInput := range issueFields {
214+ field , ok := fieldByName [strings .ToLower (strings .TrimSpace (fieldInput .FieldName ))]
215+ if ! ok {
216+ return nil , fmt .Errorf ("issue field %q was not found in %s/%s" , fieldInput .FieldName , owner , repo )
217+ }
218+
219+ fieldID := int64 (field .DatabaseID )
220+ if fieldID == 0 {
221+ return nil , fmt .Errorf ("issue field %q is missing databaseId" , fieldInput .FieldName )
222+ }
223+
224+ resolvedValue := fieldInput .Value
225+ if fieldInput .FieldOptionName != "" {
226+ if ! strings .EqualFold (string (field .DataType ), "single_select" ) {
227+ return nil , fmt .Errorf ("issue field %q is %q, so field_option_name cannot be used" , fieldInput .FieldName , field .DataType )
228+ }
229+
230+ optionFound := false
231+ for _ , option := range field .SingleSelectField .Options {
232+ if strings .EqualFold (strings .TrimSpace (string (option .Name )), strings .TrimSpace (fieldInput .FieldOptionName )) {
233+ optionID := int64 (option .DatabaseID )
234+ if optionID == 0 {
235+ return nil , fmt .Errorf ("issue field option %q on field %q is missing databaseId" , fieldInput .FieldOptionName , fieldInput .FieldName )
236+ }
237+ resolvedValue = optionID
238+ optionFound = true
239+ break
240+ }
241+ }
242+
243+ if ! optionFound {
244+ return nil , fmt .Errorf ("issue field option %q was not found for field %q" , fieldInput .FieldOptionName , fieldInput .FieldName )
245+ }
246+ }
247+
248+ resolved = append (resolved , & github.IssueRequestFieldValue {
249+ FieldID : fieldID ,
250+ Value : resolvedValue ,
251+ })
252+ }
253+
254+ return resolved , nil
255+ }
256+
106257// IssueFragment represents a fragment of an issue node in the GraphQL API.
107258type IssueFragment struct {
108259 Number githubv4.Int
@@ -1171,6 +1322,27 @@ Options are:
11711322 Type : "number" ,
11721323 Description : "Issue number that this issue is a duplicate of. Only used when state_reason is 'duplicate'." ,
11731324 },
1325+ "issue_fields" : {
1326+ Type : "array" ,
1327+ 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." ,
1328+ Items : & jsonschema.Schema {
1329+ Type : "object" ,
1330+ Properties : map [string ]* jsonschema.Schema {
1331+ "field_name" : {
1332+ Type : "string" ,
1333+ Description : "Issue field name" ,
1334+ },
1335+ "value" : {
1336+ Description : "Value for text/number/date/single-select fields. For single-select, you can use field_option_name instead." ,
1337+ },
1338+ "field_option_name" : {
1339+ Type : "string" ,
1340+ Description : "Single-select option name to resolve and set for the field" ,
1341+ },
1342+ },
1343+ Required : []string {"field_name" },
1344+ },
1345+ },
11741346 },
11751347 Required : []string {"method" , "owner" , "repo" },
11761348 },
@@ -1272,6 +1444,11 @@ Options are:
12721444 return utils .NewToolResultError ("duplicate_of can only be used when state_reason is 'duplicate'" ), nil , nil
12731445 }
12741446
1447+ issueFields , err := optionalIssueWriteFields (args )
1448+ if err != nil {
1449+ return utils .NewToolResultError (err .Error ()), nil , nil
1450+ }
1451+
12751452 client , err := deps .GetClient (ctx )
12761453 if err != nil {
12771454 return utils .NewToolResultErrorFromErr ("failed to get GitHub client" , err ), nil , nil
@@ -1282,16 +1459,21 @@ Options are:
12821459 return utils .NewToolResultErrorFromErr ("failed to get GraphQL client" , err ), nil , nil
12831460 }
12841461
1462+ issueFieldValues , err := resolveIssueRequestFieldValues (ctx , gqlClient , owner , repo , issueFields )
1463+ if err != nil {
1464+ return utils .NewToolResultError (fmt .Sprintf ("failed to resolve issue_fields: %v" , err )), nil , nil
1465+ }
1466+
12851467 switch method {
12861468 case "create" :
1287- result , err := CreateIssue (ctx , client , owner , repo , title , body , assignees , labels , milestoneNum , issueType )
1469+ result , err := CreateIssue (ctx , client , owner , repo , title , body , assignees , labels , milestoneNum , issueType , issueFieldValues )
12881470 return result , nil , err
12891471 case "update" :
12901472 issueNumber , err := RequiredInt (args , "issue_number" )
12911473 if err != nil {
12921474 return utils .NewToolResultError (err .Error ()), nil , nil
12931475 }
1294- result , err := UpdateIssue (ctx , client , gqlClient , owner , repo , issueNumber , title , body , assignees , labels , milestoneNum , issueType , state , stateReason , duplicateOf )
1476+ result , err := UpdateIssue (ctx , client , gqlClient , owner , repo , issueNumber , title , body , assignees , labels , milestoneNum , issueType , issueFieldValues , state , stateReason , duplicateOf )
12951477 return result , nil , err
12961478 default :
12971479 return utils .NewToolResultError ("invalid method, must be either 'create' or 'update'" ), nil , nil
@@ -1301,17 +1483,18 @@ Options are:
13011483 return st
13021484}
13031485
1304- 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 ) {
1486+ 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 ) {
13051487 if title == "" {
13061488 return utils .NewToolResultError ("missing required parameter: title" ), nil
13071489 }
13081490
13091491 // Create the issue request
13101492 issueRequest := & github.IssueRequest {
1311- Title : github .Ptr (title ),
1312- Body : github .Ptr (body ),
1313- Assignees : & assignees ,
1314- Labels : & labels ,
1493+ Title : github .Ptr (title ),
1494+ Body : github .Ptr (body ),
1495+ Assignees : & assignees ,
1496+ Labels : & labels ,
1497+ IssueFieldValues : issueFieldValues ,
13151498 }
13161499
13171500 if milestoneNum != 0 {
@@ -1354,7 +1537,7 @@ func CreateIssue(ctx context.Context, client *github.Client, owner string, repo
13541537 return utils .NewToolResultText (string (r )), nil
13551538}
13561539
1357- 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 ) {
1540+ 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 ) {
13581541 // Create the issue request with only provided fields
13591542 issueRequest := & github.IssueRequest {}
13601543
@@ -1383,6 +1566,10 @@ func UpdateIssue(ctx context.Context, client *github.Client, gqlClient *githubv4
13831566 issueRequest .Type = github .Ptr (issueType )
13841567 }
13851568
1569+ if len (issueFieldValues ) > 0 {
1570+ issueRequest .IssueFieldValues = issueFieldValues
1571+ }
1572+
13861573 updatedIssue , resp , err := client .Issues .Edit (ctx , owner , repo , issueNumber , issueRequest )
13871574 if err != nil {
13881575 return ghErrors .NewGitHubAPIErrorResponse (ctx ,
0 commit comments