@@ -45,6 +45,9 @@ type issueWriteFieldInput struct {
4545 Value any
4646 FieldOptionName string
4747 Delete bool
48+ // Intent carries optional rationale/confidence/suggestion metadata for a
49+ // value-setting field. It is ignored for delete entries and on create.
50+ Intent valueIntent
4851}
4952
5053const (
@@ -290,19 +293,30 @@ func optionalIssueWriteFields(args map[string]any) ([]issueWriteFieldInput, erro
290293 return nil , fmt .Errorf ("issue field %q must specify either value or field_option_name" , fieldName )
291294 }
292295
296+ intent , _ , err := parseValueIntent (itemMap )
297+ if err != nil {
298+ return nil , err
299+ }
300+
293301 issueFields = append (issueFields , issueWriteFieldInput {
294302 FieldName : fieldName ,
295303 Value : value ,
296304 FieldOptionName : fieldOptionName ,
305+ Intent : intent ,
297306 })
298307 }
299308
300309 return issueFields , nil
301310}
302311
303- func resolveIssueRequestFieldValues (ctx context.Context , gqlClient * githubv4.Client , owner , repo string , issueFields []issueWriteFieldInput ) ([]* github.IssueRequestFieldValue , []int64 , error ) {
312+ // resolveIssueRequestFieldValues resolves user-friendly field inputs into REST
313+ // IssueRequestFieldValue entries (field IDs and option values resolved). It also
314+ // returns the IDs of fields marked for deletion and a map of field ID to intent
315+ // metadata for fields that carried rationale/confidence/suggestion intent, so the
316+ // caller can send those values in object form.
317+ func resolveIssueRequestFieldValues (ctx context.Context , gqlClient * githubv4.Client , owner , repo string , issueFields []issueWriteFieldInput ) ([]* github.IssueRequestFieldValue , []int64 , map [int64 ]valueIntent , error ) {
304318 if len (issueFields ) == 0 {
305- return nil , nil , nil
319+ return nil , nil , nil , nil
306320 }
307321
308322 ctxWithFeatures := ghcontext .WithGraphQLFeatures (ctx , "issue_fields" , "repo_issue_fields" )
@@ -312,7 +326,7 @@ func resolveIssueRequestFieldValues(ctx context.Context, gqlClient *githubv4.Cli
312326 "repo" : githubv4 .String (repo ),
313327 }
314328 if err := gqlClient .Query (ctxWithFeatures , & query , vars ); err != nil {
315- return nil , nil , fmt .Errorf ("failed to query issue fields metadata: %w" , err )
329+ return nil , nil , nil , fmt .Errorf ("failed to query issue fields metadata: %w" , err )
316330 }
317331
318332 // Build name → node map, dispatching on concrete type to extract name.
@@ -336,10 +350,11 @@ func resolveIssueRequestFieldValues(ctx context.Context, gqlClient *githubv4.Cli
336350
337351 resolved := make ([]* github.IssueRequestFieldValue , 0 , len (issueFields ))
338352 var fieldIDsToDelete []int64
353+ var fieldIntents map [int64 ]valueIntent
339354 for _ , fieldInput := range issueFields {
340355 node , ok := fieldByName [strings .ToLower (strings .TrimSpace (fieldInput .FieldName ))]
341356 if ! ok {
342- return nil , nil , fmt .Errorf ("issue field %q was not found in %s/%s" , fieldInput .FieldName , owner , repo )
357+ return nil , nil , nil , fmt .Errorf ("issue field %q was not found in %s/%s" , fieldInput .FieldName , owner , repo )
343358 }
344359
345360 var fullDatabaseIDStr , dataType string
@@ -360,7 +375,7 @@ func resolveIssueRequestFieldValues(ctx context.Context, gqlClient *githubv4.Cli
360375
361376 fieldID := parseFullDatabaseID (fullDatabaseIDStr )
362377 if fieldID == 0 {
363- return nil , nil , fmt .Errorf ("issue field %q is missing fullDatabaseId" , fieldInput .FieldName )
378+ return nil , nil , nil , fmt .Errorf ("issue field %q is missing fullDatabaseId" , fieldInput .FieldName )
364379 }
365380
366381 if fieldInput .Delete {
@@ -371,7 +386,7 @@ func resolveIssueRequestFieldValues(ctx context.Context, gqlClient *githubv4.Cli
371386 resolvedValue := fieldInput .Value
372387 if fieldInput .FieldOptionName != "" {
373388 if ! strings .EqualFold (dataType , "single_select" ) {
374- return nil , nil , fmt .Errorf ("issue field %q is %q, so field_option_name cannot be used" , fieldInput .FieldName , dataType )
389+ return nil , nil , nil , fmt .Errorf ("issue field %q is %q, so field_option_name cannot be used" , fieldInput .FieldName , dataType )
375390 }
376391
377392 optionFound := false
@@ -385,17 +400,24 @@ func resolveIssueRequestFieldValues(ctx context.Context, gqlClient *githubv4.Cli
385400 }
386401
387402 if ! optionFound {
388- return nil , nil , fmt .Errorf ("issue field option %q was not found for field %q" , fieldInput .FieldOptionName , fieldInput .FieldName )
403+ return nil , nil , nil , fmt .Errorf ("issue field option %q was not found for field %q" , fieldInput .FieldOptionName , fieldInput .FieldName )
389404 }
390405 }
391406
407+ if fieldInput .Intent .HasIntent () {
408+ if fieldIntents == nil {
409+ fieldIntents = make (map [int64 ]valueIntent )
410+ }
411+ fieldIntents [fieldID ] = fieldInput .Intent
412+ }
413+
392414 resolved = append (resolved , & github.IssueRequestFieldValue {
393415 FieldID : fieldID ,
394416 Value : resolvedValue ,
395417 })
396418 }
397419
398- return resolved , fieldIDsToDelete , nil
420+ return resolved , fieldIDsToDelete , fieldIntents , nil
399421}
400422
401423// fetchExistingIssueFieldValues retrieves the current field values for an issue
@@ -1922,7 +1944,7 @@ Options are:
19221944 Items : & jsonschema.Schema {
19231945 Type : "object" ,
19241946 AdditionalProperties : & jsonschema.Schema {Not : & jsonschema.Schema {}},
1925- Properties : map [string ]* jsonschema.Schema {
1947+ Properties : withIntentProperties ( map [string ]* jsonschema.Schema {
19261948 "field_name" : {
19271949 Type : "string" ,
19281950 Description : "Issue field name (case-insensitive). Must match a field " +
@@ -1947,7 +1969,7 @@ Options are:
19471969 Description : "Set to true to clear this field's current value on the " +
19481970 "issue. Cannot be combined with 'value' or 'field_option_name'." ,
19491971 },
1950- },
1972+ }) ,
19511973 Required : []string {"field_name" },
19521974 },
19531975 },
@@ -2068,8 +2090,9 @@ Options are:
20682090
20692091 var issueFieldValues []* github.IssueRequestFieldValue
20702092 var fieldIDsToDelete []int64
2093+ var fieldValuesWithIntent map [int64 ]valueIntent
20712094 if len (issueFields ) > 0 {
2072- issueFieldValues , fieldIDsToDelete , err = resolveIssueRequestFieldValues (ctx , gqlClient , owner , repo , issueFields )
2095+ issueFieldValues , fieldIDsToDelete , fieldValuesWithIntent , err = resolveIssueRequestFieldValues (ctx , gqlClient , owner , repo , issueFields )
20732096 if err != nil {
20742097 return utils .NewToolResultError (fmt .Sprintf ("failed to resolve issue_fields: %v" , err )), nil , nil
20752098 }
@@ -2094,6 +2117,9 @@ Options are:
20942117 if issueTypeHasIntent {
20952118 updateOpts .TypeWithIntent = issueTypePayload
20962119 }
2120+ if len (fieldValuesWithIntent ) > 0 {
2121+ updateOpts .FieldValuesWithIntent = fieldValuesWithIntent
2122+ }
20972123 result , err := UpdateIssue (ctx , client , gqlClient , owner , repo , issueNumber , title , body , assignees , labels , milestoneNum , issueType , issueFieldValues , fieldIDsToDelete , state , stateReason , duplicateOf , updateOpts )
20982124 return result , nil , err
20992125 default :
@@ -2412,6 +2438,10 @@ type UpdateIssueOptions struct {
24122438 // intent are preserved. When set, it takes precedence over the issueType
24132439 // string.
24142440 TypeWithIntent any
2441+ // FieldValuesWithIntent, when non-empty, maps a field ID to the intent
2442+ // metadata supplied for it, so those field values are sent in object form
2443+ // (field_id, value, plus rationale/confidence/suggest) via a custom request.
2444+ FieldValuesWithIntent map [int64 ]valueIntent
24152445}
24162446
24172447// issueRequestWithIntentOverrides marshals an IssueRequest into a generic map and
@@ -2431,6 +2461,33 @@ func issueRequestWithIntentOverrides(issueRequest *github.IssueRequest, override
24312461 return payload , nil
24322462}
24332463
2464+ // issueFieldValueWithIntent is the object form of an issue field value: its
2465+ // field ID and value alongside optional intent metadata. It marshals to a single
2466+ // object merging field_id and value with the embedded
2467+ // rationale/confidence/suggest fields, mirroring how labels and types carry
2468+ // intent on the wire.
2469+ type issueFieldValueWithIntent struct {
2470+ FieldID int64
2471+ Value any
2472+ valueIntent
2473+ }
2474+
2475+ // MarshalJSON renders the value as a single object with field_id and value
2476+ // alongside the embedded intent metadata.
2477+ func (v issueFieldValueWithIntent ) MarshalJSON () ([]byte , error ) {
2478+ data , err := json .Marshal (v .valueIntent )
2479+ if err != nil {
2480+ return nil , err
2481+ }
2482+ obj := map [string ]any {}
2483+ if err := json .Unmarshal (data , & obj ); err != nil {
2484+ return nil , err
2485+ }
2486+ obj ["field_id" ] = v .FieldID
2487+ obj ["value" ] = v .Value
2488+ return json .Marshal (obj )
2489+ }
2490+
24342491func 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 , fieldIDsToDelete []int64 , state string , stateReason string , duplicateOf int , opts ... UpdateIssueOptions ) (* mcp.CallToolResult , error ) {
24352492 updateOptions := UpdateIssueOptions {
24362493 AssigneesProvided : len (assignees ) > 0 ,
@@ -2445,6 +2502,9 @@ func UpdateIssue(ctx context.Context, client *github.Client, gqlClient *githubv4
24452502 if opt .TypeWithIntent != nil {
24462503 updateOptions .TypeWithIntent = opt .TypeWithIntent
24472504 }
2505+ if len (opt .FieldValuesWithIntent ) > 0 {
2506+ updateOptions .FieldValuesWithIntent = opt .FieldValuesWithIntent
2507+ }
24482508 }
24492509
24502510 // Create the issue request with only provided fields
@@ -2505,17 +2565,29 @@ func UpdateIssue(ctx context.Context, client *github.Client, gqlClient *githubv4
25052565 var updatedIssue * github.Issue
25062566 var resp * github.Response
25072567 var err error
2508- if len (updateOptions .LabelsWithIntent ) > 0 || updateOptions .TypeWithIntent != nil {
2509- // Send values that carry intent (labels and/or type) in object form so
2510- // their rationale and suggestion intent are preserved. Marshal the standard
2511- // request (those fields omitted), then inject the object-form values.
2568+ if len (updateOptions .LabelsWithIntent ) > 0 || updateOptions .TypeWithIntent != nil || len (updateOptions .FieldValuesWithIntent ) > 0 {
2569+ // Send values that carry intent (labels, type, and/or field values) in
2570+ // object form so their rationale and suggestion intent are preserved.
2571+ // Marshal the standard request (those fields omitted), then inject the
2572+ // object-form values.
25122573 overrides := map [string ]any {}
25132574 if len (updateOptions .LabelsWithIntent ) > 0 {
25142575 overrides ["labels" ] = updateOptions .LabelsWithIntent
25152576 }
25162577 if updateOptions .TypeWithIntent != nil {
25172578 overrides ["type" ] = updateOptions .TypeWithIntent
25182579 }
2580+ if len (updateOptions .FieldValuesWithIntent ) > 0 && len (issueRequest .IssueFieldValues ) > 0 {
2581+ fieldValues := make ([]any , 0 , len (issueRequest .IssueFieldValues ))
2582+ for _ , v := range issueRequest .IssueFieldValues {
2583+ if intent , ok := updateOptions .FieldValuesWithIntent [v .FieldID ]; ok {
2584+ fieldValues = append (fieldValues , issueFieldValueWithIntent {FieldID : v .FieldID , Value : v .Value , valueIntent : intent })
2585+ } else {
2586+ fieldValues = append (fieldValues , map [string ]any {"field_id" : v .FieldID , "value" : v .Value })
2587+ }
2588+ }
2589+ overrides ["issue_field_values" ] = fieldValues
2590+ }
25192591 payload , mErr := issueRequestWithIntentOverrides (issueRequest , overrides )
25202592 if mErr != nil {
25212593 return utils .NewToolResultErrorFromErr ("failed to build issue update request" , mErr ), nil
0 commit comments