@@ -105,6 +105,152 @@ func getCloseStateReason(stateReason string) IssueClosedStateReason {
105105 }
106106}
107107
108+ // IssueWriteFieldInput is a user-supplied issue field assignment: a field name and a string value.
109+ // The value is forwarded directly to the REST API — for single-select fields it must be the
110+ // option name (e.g. "P1"), not an option ID. Field ID resolution is done internally via GQL.
111+ type IssueWriteFieldInput struct {
112+ FieldName string
113+ Value string
114+ }
115+
116+ // issueFieldWriteMetadataNode queries only the fields needed to resolve a write: the field's
117+ // fullDatabaseId (BigInt scalar, returned as string) plus its name and data type for validation.
118+ // shurcooL/githubv4 cannot use interface-level fragments at union top-level, so we repeat
119+ // fullDatabaseId on each concrete type; all four implement IssueFieldCommon.
120+ type issueFieldWriteMetadataNode struct {
121+ TypeName githubv4.String `graphql:"__typename"`
122+ IssueFieldText struct {
123+ FullDatabaseID githubv4.String `graphql:"fullDatabaseId"`
124+ Name githubv4.String
125+ DataType githubv4.String
126+ } `graphql:"... on IssueFieldText"`
127+ IssueFieldNumber struct {
128+ FullDatabaseID githubv4.String `graphql:"fullDatabaseId"`
129+ Name githubv4.String
130+ DataType githubv4.String
131+ } `graphql:"... on IssueFieldNumber"`
132+ IssueFieldDate struct {
133+ FullDatabaseID githubv4.String `graphql:"fullDatabaseId"`
134+ Name githubv4.String
135+ DataType githubv4.String
136+ } `graphql:"... on IssueFieldDate"`
137+ IssueFieldSingleSelect struct {
138+ FullDatabaseID githubv4.String `graphql:"fullDatabaseId"`
139+ Name githubv4.String
140+ DataType githubv4.String
141+ } `graphql:"... on IssueFieldSingleSelect"`
142+ }
143+
144+ // issueFieldWriteMetadata holds the resolved name, database ID, and data type for a single field.
145+ type issueFieldWriteMetadata struct {
146+ DatabaseID int64
147+ Name string
148+ DataType string
149+ }
150+
151+ type issueFieldWriteMetadataQuery struct {
152+ Repository struct {
153+ IssueFields struct {
154+ Nodes []issueFieldWriteMetadataNode
155+ } `graphql:"issueFields(first: 100)"`
156+ } `graphql:"repository(owner: $owner, name: $repo)"`
157+ }
158+
159+ func optionalIssueWriteFields (args map [string ]any ) ([]IssueWriteFieldInput , error ) {
160+ raw , exists := args ["issue_fields" ]
161+ if ! exists {
162+ return nil , nil
163+ }
164+
165+ var inputMaps []map [string ]any
166+ switch v := raw .(type ) {
167+ case []any :
168+ for _ , item := range v {
169+ m , ok := item .(map [string ]any )
170+ if ! ok {
171+ return nil , fmt .Errorf ("each issue_fields item must be an object" )
172+ }
173+ inputMaps = append (inputMaps , m )
174+ }
175+ case []map [string ]any :
176+ inputMaps = v
177+ default :
178+ return nil , fmt .Errorf ("issue_fields must be an array" )
179+ }
180+
181+ out := make ([]IssueWriteFieldInput , 0 , len (inputMaps ))
182+ for _ , m := range inputMaps {
183+ fieldName , err := RequiredParam [string ](m , "field_name" )
184+ if err != nil || strings .TrimSpace (fieldName ) == "" {
185+ return nil , fmt .Errorf ("field_name is required for each issue_fields item" )
186+ }
187+ value , err := RequiredParam [string ](m , "value" )
188+ if err != nil {
189+ return nil , fmt .Errorf ("issue_fields item %q: value is required" , fieldName )
190+ }
191+ out = append (out , IssueWriteFieldInput {FieldName : fieldName , Value : value })
192+ }
193+ return out , nil
194+ }
195+
196+ func resolveIssueWriteFieldValues (ctx context.Context , gqlClient * githubv4.Client , owner , repo string , inputs []IssueWriteFieldInput ) ([]* github.IssueRequestFieldValue , error ) {
197+ if len (inputs ) == 0 {
198+ return nil , nil
199+ }
200+
201+ ctxWithFeatures := ghcontext .WithGraphQLFeatures (ctx , "issue_fields" , "repo_issue_fields" )
202+ var query issueFieldWriteMetadataQuery
203+ vars := map [string ]any {
204+ "owner" : githubv4 .String (owner ),
205+ "repo" : githubv4 .String (repo ),
206+ }
207+ if err := gqlClient .Query (ctxWithFeatures , & query , vars ); err != nil {
208+ return nil , fmt .Errorf ("failed to query issue field metadata: %w" , err )
209+ }
210+
211+ // Build name → metadata map from the GQL response.
212+ byName := make (map [string ]issueFieldWriteMetadata , len (query .Repository .IssueFields .Nodes ))
213+ for _ , node := range query .Repository .IssueFields .Nodes {
214+ var name , dataType , fullDBID string
215+ switch string (node .TypeName ) {
216+ case "IssueFieldText" :
217+ name , dataType , fullDBID = string (node .IssueFieldText .Name ), string (node .IssueFieldText .DataType ), string (node .IssueFieldText .FullDatabaseID )
218+ case "IssueFieldNumber" :
219+ name , dataType , fullDBID = string (node .IssueFieldNumber .Name ), string (node .IssueFieldNumber .DataType ), string (node .IssueFieldNumber .FullDatabaseID )
220+ case "IssueFieldDate" :
221+ name , dataType , fullDBID = string (node .IssueFieldDate .Name ), string (node .IssueFieldDate .DataType ), string (node .IssueFieldDate .FullDatabaseID )
222+ case "IssueFieldSingleSelect" :
223+ name , dataType , fullDBID = string (node .IssueFieldSingleSelect .Name ), string (node .IssueFieldSingleSelect .DataType ), string (node .IssueFieldSingleSelect .FullDatabaseID )
224+ default :
225+ continue
226+ }
227+ dbID , _ := strconv .ParseInt (fullDBID , 10 , 64 )
228+ byName [strings .ToLower (strings .TrimSpace (name ))] = issueFieldWriteMetadata {
229+ DatabaseID : dbID ,
230+ Name : name ,
231+ DataType : dataType ,
232+ }
233+ }
234+
235+ resolved := make ([]* github.IssueRequestFieldValue , 0 , len (inputs ))
236+ for _ , input := range inputs {
237+ meta , ok := byName [strings .ToLower (strings .TrimSpace (input .FieldName ))]
238+ if ! ok {
239+ return nil , fmt .Errorf ("issue field %q was not found in %s/%s" , input .FieldName , owner , repo )
240+ }
241+ if meta .DatabaseID == 0 {
242+ return nil , fmt .Errorf ("issue field %q is missing fullDatabaseId" , input .FieldName )
243+ }
244+ // For single-select the REST API expects the option name as a string value.
245+ // For all other types, pass the value through as-is.
246+ resolved = append (resolved , & github.IssueRequestFieldValue {
247+ FieldID : meta .DatabaseID ,
248+ Value : input .Value ,
249+ })
250+ }
251+ return resolved , nil
252+ }
253+
108254// IssueFieldRef resolves the name of an issue field across its concrete types.
109255// IssueFields is a union of IssueFieldDate, IssueFieldNumber, IssueFieldSingleSelect, IssueFieldText,
110256// so we have to ask for `name` on each member.
@@ -1509,6 +1655,25 @@ Options are:
15091655 Type : "number" ,
15101656 Description : "Issue number that this issue is a duplicate of. Only used when state_reason is 'duplicate'." ,
15111657 },
1658+ "issue_fields" : {
1659+ Type : "array" ,
1660+ Description : "Custom issue field values to set. Each entry specifies a field by name and its value. For single-select fields, value must be the option name (e.g. \" P1\" ). For date fields, value must be YYYY-MM-DD." ,
1661+ Items : & jsonschema.Schema {
1662+ Type : "object" ,
1663+ AdditionalProperties : & jsonschema.Schema {Not : & jsonschema.Schema {}},
1664+ Properties : map [string ]* jsonschema.Schema {
1665+ "field_name" : {
1666+ Type : "string" ,
1667+ Description : "Name of the custom field (case-insensitive)." ,
1668+ },
1669+ "value" : {
1670+ Type : "string" ,
1671+ Description : "Value to set. For single-select, the option name. For dates, YYYY-MM-DD. For numbers, the numeric value as a string." ,
1672+ },
1673+ },
1674+ Required : []string {"field_name" , "value" },
1675+ },
1676+ },
15121677 },
15131678 Required : []string {"method" , "owner" , "repo" },
15141679 },
@@ -1610,6 +1775,11 @@ Options are:
16101775 return utils .NewToolResultError ("duplicate_of can only be used when state_reason is 'duplicate'" ), nil , nil
16111776 }
16121777
1778+ issueFieldInputs , err := optionalIssueWriteFields (args )
1779+ if err != nil {
1780+ return utils .NewToolResultError (err .Error ()), nil , nil
1781+ }
1782+
16131783 client , err := deps .GetClient (ctx )
16141784 if err != nil {
16151785 return utils .NewToolResultErrorFromErr ("failed to get GitHub client" , err ), nil , nil
@@ -1620,16 +1790,21 @@ Options are:
16201790 return utils .NewToolResultErrorFromErr ("failed to get GraphQL client" , err ), nil , nil
16211791 }
16221792
1793+ issueFieldValues , err := resolveIssueWriteFieldValues (ctx , gqlClient , owner , repo , issueFieldInputs )
1794+ if err != nil {
1795+ return utils .NewToolResultError (fmt .Sprintf ("failed to resolve issue_fields: %v" , err )), nil , nil
1796+ }
1797+
16231798 switch method {
16241799 case "create" :
1625- result , err := CreateIssue (ctx , client , owner , repo , title , body , assignees , labels , milestoneNum , issueType )
1800+ result , err := CreateIssue (ctx , client , owner , repo , title , body , assignees , labels , milestoneNum , issueType , issueFieldValues )
16261801 return result , nil , err
16271802 case "update" :
16281803 issueNumber , err := RequiredInt (args , "issue_number" )
16291804 if err != nil {
16301805 return utils .NewToolResultError (err .Error ()), nil , nil
16311806 }
1632- result , err := UpdateIssue (ctx , client , gqlClient , owner , repo , issueNumber , title , body , assignees , labels , milestoneNum , issueType , state , stateReason , duplicateOf )
1807+ result , err := UpdateIssue (ctx , client , gqlClient , owner , repo , issueNumber , title , body , assignees , labels , milestoneNum , issueType , issueFieldValues , state , stateReason , duplicateOf )
16331808 return result , nil , err
16341809 default :
16351810 return utils .NewToolResultError ("invalid method, must be either 'create' or 'update'" ), nil , nil
@@ -1639,17 +1814,18 @@ Options are:
16391814 return st
16401815}
16411816
1642- 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 ) {
1817+ 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 ) {
16431818 if title == "" {
16441819 return utils .NewToolResultError ("missing required parameter: title" ), nil
16451820 }
16461821
16471822 // Create the issue request
16481823 issueRequest := & github.IssueRequest {
1649- Title : github .Ptr (title ),
1650- Body : github .Ptr (body ),
1651- Assignees : & assignees ,
1652- Labels : & labels ,
1824+ Title : github .Ptr (title ),
1825+ Body : github .Ptr (body ),
1826+ Assignees : & assignees ,
1827+ Labels : & labels ,
1828+ IssueFieldValues : issueFieldValues ,
16531829 }
16541830
16551831 if milestoneNum != 0 {
@@ -1692,7 +1868,7 @@ func CreateIssue(ctx context.Context, client *github.Client, owner string, repo
16921868 return utils .NewToolResultText (string (r )), nil
16931869}
16941870
1695- 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 ) {
1871+ 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 ) {
16961872 // Create the issue request with only provided fields
16971873 issueRequest := & github.IssueRequest {}
16981874
@@ -1721,6 +1897,10 @@ func UpdateIssue(ctx context.Context, client *github.Client, gqlClient *githubv4
17211897 issueRequest .Type = github .Ptr (issueType )
17221898 }
17231899
1900+ if len (issueFieldValues ) > 0 {
1901+ issueRequest .IssueFieldValues = issueFieldValues
1902+ }
1903+
17241904 updatedIssue , resp , err := client .Issues .Edit (ctx , owner , repo , issueNumber , issueRequest )
17251905 if err != nil {
17261906 return ghErrors .NewGitHubAPIErrorResponse (ctx ,
0 commit comments