From d8e16ce7412efe39dc053b13d2927a162c3fe5a6 Mon Sep 17 00:00:00 2001 From: Renee Xu Date: Fri, 12 Jun 2026 01:47:26 -0700 Subject: [PATCH 1/7] Feature flag setting suggestion fields using the intent model --- pkg/github/feature_flags.go | 8 +++++++ pkg/github/issues_granular.go | 41 +++++++++++++++++++++++++++++++++-- 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/pkg/github/feature_flags.go b/pkg/github/feature_flags.go index 15a78c1f1..4e26105ef 100644 --- a/pkg/github/feature_flags.go +++ b/pkg/github/feature_flags.go @@ -16,6 +16,13 @@ const FeatureFlagIFCLabels = "ifc_labels" // and field_values enrichment in list_issues / search_issues output. const FeatureFlagIssueFields = "remote_mcp_issue_fields" +// FeatureFlagIssueUpdateIntent is the feature flag name for sending the +// rationale and confidence on issue field mutations as a nested `intent` +// object (IssueUpdateIntent) instead of as flat fields. It exists so the +// set_issue_fields tool can switch payload shapes while the backend migrates +// IssueFieldCreateOrUpdateInput from IssueEventRationale to IssueUpdateIntent. +const FeatureFlagIssueUpdateIntent = "issue_update_intent" + // AllowedFeatureFlags is the allowlist of feature flags that can be enabled // by users via --features CLI flag or X-MCP-Features HTTP header. // Only flags in this list are accepted; unknown flags are silently ignored. @@ -25,6 +32,7 @@ var AllowedFeatureFlags = []string{ FeatureFlagCSVOutput, FeatureFlagIFCLabels, FeatureFlagIssueFields, + FeatureFlagIssueUpdateIntent, FeatureFlagIssuesGranular, FeatureFlagPullRequestsGranular, } diff --git a/pkg/github/issues_granular.go b/pkg/github/issues_granular.go index 3ddfd682f..52c2c07c0 100644 --- a/pkg/github/issues_granular.go +++ b/pkg/github/issues_granular.go @@ -917,6 +917,19 @@ type IssueFieldCreateOrUpdateInput struct { Rationale *githubv4.String `json:"rationale,omitempty"` Confidence *string `json:"confidence,omitempty"` Suggest *githubv4.Boolean `json:"suggest,omitempty"` + // Intent bundles rationale and confidence into a single object. It is the + // successor to the flat Rationale/Confidence fields above and is only sent + // when the FeatureFlagIssueUpdateIntent feature flag is enabled. + Intent *IssueUpdateIntentInput `json:"intent,omitempty"` +} + +// IssueUpdateIntentInput is the nested input object that carries the rationale +// and confidence for an issue field mutation. It mirrors the read-path +// IssueUpdateIntent type and replaces the flat rationale/confidence fields on +// IssueFieldCreateOrUpdateInput after the backend migration. +type IssueUpdateIntentInput struct { + Rationale *githubv4.String `json:"rationale,omitempty"` + Confidence *string `json:"confidence,omitempty"` } // GranularSetIssueFields creates a tool to set issue field values on an issue using GraphQL. @@ -1044,6 +1057,12 @@ func GranularSetIssueFields(t translations.TranslationHelperFunc) inventory.Serv } issueFields := make([]IssueFieldCreateOrUpdateInput, 0, len(fieldMaps)) + // The backend is migrating the rationale/confidence pair on + // IssueFieldCreateOrUpdateInput from flat fields to a nested + // `intent` object (IssueUpdateIntent). While that migration is in + // flight, gate the payload shape behind a feature flag so we can + // switch over without breaking the un-migrated schema. + useIntentInput := deps.IsFeatureEnabled(ctx, FeatureFlagIssueUpdateIntent) for _, fieldMap := range fieldMaps { fieldID, err := RequiredParam[string](fieldMap, "field_id") if err != nil { @@ -1093,6 +1112,9 @@ func GranularSetIssueFields(t translations.TranslationHelperFunc) inventory.Serv return utils.NewToolResultError("each field must have exactly one value (text_value, number_value, date_value, single_select_option_id) or delete: true, but multiple were provided"), nil, nil } + var rationalePtr *githubv4.String + var confidencePtr *string + if _, exists := fieldMap["rationale"]; exists { rationale, err := OptionalParam[string](fieldMap, "rationale") if err != nil { @@ -1103,7 +1125,7 @@ func GranularSetIssueFields(t translations.TranslationHelperFunc) inventory.Serv return utils.NewToolResultError("field rationale must be 280 characters or less"), nil, nil } if rationale != "" { - input.Rationale = githubv4.NewString(githubv4.String(rationale)) + rationalePtr = githubv4.NewString(githubv4.String(rationale)) } } @@ -1115,7 +1137,22 @@ func GranularSetIssueFields(t translations.TranslationHelperFunc) inventory.Serv return utils.NewToolResultError("confidence must be one of: low, medium, high"), nil, nil } if confidence != "" { - input.Confidence = &confidence + confidencePtr = &confidence + } + + // Send rationale and confidence either as the nested `intent` + // object (new schema) or as flat fields (old schema), depending + // on the feature flag. + if useIntentInput { + if rationalePtr != nil || confidencePtr != nil { + input.Intent = &IssueUpdateIntentInput{ + Rationale: rationalePtr, + Confidence: confidencePtr, + } + } + } else { + input.Rationale = rationalePtr + input.Confidence = confidencePtr } isSuggestion, err := OptionalParam[bool](fieldMap, "is_suggestion") From aebcbae5c598f5ffdfa7461f781514660bb99c26 Mon Sep 17 00:00:00 2001 From: Renee Xu Date: Fri, 12 Jun 2026 01:47:31 -0700 Subject: [PATCH 2/7] Add test --- pkg/github/granular_tools_test.go | 105 ++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) diff --git a/pkg/github/granular_tools_test.go b/pkg/github/granular_tools_test.go index 27e8079f9..ede7f9a77 100644 --- a/pkg/github/granular_tools_test.go +++ b/pkg/github/granular_tools_test.go @@ -1992,4 +1992,109 @@ func TestGranularSetIssueFields(t *testing.T) { // query does not require the feature flag. assert.Equal(t, "update_issue_suggestions", spy.captured.Get(headers.GraphQLFeaturesHeader)) }) + + t.Run("sends rationale and confidence as intent object when feature flag enabled", func(t *testing.T) { + confidence := "high" + suggestTrue := githubv4.Boolean(true) + matchers := []githubv4mock.Matcher{ + githubv4mock.NewQueryMatcher( + struct { + Repository struct { + Issue struct { + ID githubv4.ID + } `graphql:"issue(number: $issueNumber)"` + } `graphql:"repository(owner: $owner, name: $repo)"` + }{}, + map[string]any{ + "owner": githubv4.String("owner"), + "repo": githubv4.String("repo"), + "issueNumber": githubv4.Int(5), + }, + githubv4mock.DataResponse(map[string]any{ + "repository": map[string]any{ + "issue": map[string]any{"id": "ISSUE_123"}, + }, + }), + ), + githubv4mock.NewMutationMatcher( + struct { + SetIssueFieldValue struct { + Issue struct { + ID githubv4.ID + Number githubv4.Int + URL githubv4.String + } + IssueFieldValues []struct { + TextValue struct { + Value string + } `graphql:"... on IssueFieldTextValue"` + SingleSelectValue struct { + Name string + } `graphql:"... on IssueFieldSingleSelectValue"` + DateValue struct { + Value string + } `graphql:"... on IssueFieldDateValue"` + NumberValue struct { + Value float64 + } `graphql:"... on IssueFieldNumberValue"` + } + } `graphql:"setIssueFieldValue(input: $input)"` + }{}, + SetIssueFieldValueInput{ + IssueID: githubv4.ID("ISSUE_123"), + IssueFields: []IssueFieldCreateOrUpdateInput{ + { + FieldID: githubv4.ID("FIELD_1"), + TextValue: githubv4.NewString(githubv4.String("hello")), + // rationale and confidence are nested under intent, + // while suggest stays a flat field. + Intent: &IssueUpdateIntentInput{ + Rationale: githubv4.NewString(githubv4.String("Reflects the reported severity")), + Confidence: &confidence, + }, + Suggest: &suggestTrue, + }, + }, + }, + nil, + githubv4mock.DataResponse(map[string]any{ + "setIssueFieldValue": map[string]any{ + "issue": map[string]any{ + "id": "ISSUE_123", + "number": 5, + "url": "https://github.com/owner/repo/issues/5", + }, + }, + }), + ), + } + + gqlClient := githubv4.NewClient(githubv4mock.NewMockedHTTPClient(matchers...)) + deps := BaseDeps{ + GQLClient: gqlClient, + featureChecker: func(_ context.Context, flag string) (bool, error) { + return flag == FeatureFlagIssueUpdateIntent, nil + }, + } + serverTool := GranularSetIssueFields(translations.NullTranslationHelper) + handler := serverTool.Handler(deps) + + request := createMCPRequest(map[string]any{ + "owner": "owner", + "repo": "repo", + "issue_number": float64(5), + "fields": []any{ + map[string]any{ + "field_id": "FIELD_1", + "text_value": "hello", + "rationale": "Reflects the reported severity", + "confidence": "high", + "is_suggestion": true, + }, + }, + }) + result, err := handler(ContextWithDeps(context.Background(), deps), &request) + require.NoError(t, err) + assert.False(t, result.IsError, getTextResult(t, result).Text) + }) } From 3e7155c0a2604d5d73e6a24c1d9737694acc3676 Mon Sep 17 00:00:00 2001 From: Renee Xu Date: Fri, 12 Jun 2026 02:02:23 -0700 Subject: [PATCH 3/7] Rename FF --- pkg/github/feature_flags.go | 6 +++--- pkg/github/granular_tools_test.go | 2 +- pkg/github/issues_granular.go | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pkg/github/feature_flags.go b/pkg/github/feature_flags.go index 4e26105ef..40b3c46e0 100644 --- a/pkg/github/feature_flags.go +++ b/pkg/github/feature_flags.go @@ -16,12 +16,12 @@ const FeatureFlagIFCLabels = "ifc_labels" // and field_values enrichment in list_issues / search_issues output. const FeatureFlagIssueFields = "remote_mcp_issue_fields" -// FeatureFlagIssueUpdateIntent is the feature flag name for sending the +// FeatureFlagIssueFieldsUseUpdateIntent is the feature flag name for sending the // rationale and confidence on issue field mutations as a nested `intent` // object (IssueUpdateIntent) instead of as flat fields. It exists so the // set_issue_fields tool can switch payload shapes while the backend migrates // IssueFieldCreateOrUpdateInput from IssueEventRationale to IssueUpdateIntent. -const FeatureFlagIssueUpdateIntent = "issue_update_intent" +const FeatureFlagIssueFieldsUseUpdateIntent = "issue_fields_use_update_intent" // AllowedFeatureFlags is the allowlist of feature flags that can be enabled // by users via --features CLI flag or X-MCP-Features HTTP header. @@ -32,7 +32,7 @@ var AllowedFeatureFlags = []string{ FeatureFlagCSVOutput, FeatureFlagIFCLabels, FeatureFlagIssueFields, - FeatureFlagIssueUpdateIntent, + FeatureFlagIssueFieldsUseUpdateIntent, FeatureFlagIssuesGranular, FeatureFlagPullRequestsGranular, } diff --git a/pkg/github/granular_tools_test.go b/pkg/github/granular_tools_test.go index ede7f9a77..4f849f147 100644 --- a/pkg/github/granular_tools_test.go +++ b/pkg/github/granular_tools_test.go @@ -2073,7 +2073,7 @@ func TestGranularSetIssueFields(t *testing.T) { deps := BaseDeps{ GQLClient: gqlClient, featureChecker: func(_ context.Context, flag string) (bool, error) { - return flag == FeatureFlagIssueUpdateIntent, nil + return flag == FeatureFlagIssueFieldsUseUpdateIntent, nil }, } serverTool := GranularSetIssueFields(translations.NullTranslationHelper) diff --git a/pkg/github/issues_granular.go b/pkg/github/issues_granular.go index 52c2c07c0..207e82260 100644 --- a/pkg/github/issues_granular.go +++ b/pkg/github/issues_granular.go @@ -919,7 +919,7 @@ type IssueFieldCreateOrUpdateInput struct { Suggest *githubv4.Boolean `json:"suggest,omitempty"` // Intent bundles rationale and confidence into a single object. It is the // successor to the flat Rationale/Confidence fields above and is only sent - // when the FeatureFlagIssueUpdateIntent feature flag is enabled. + // when the FeatureFlagIssueFieldsUseUpdateIntent feature flag is enabled. Intent *IssueUpdateIntentInput `json:"intent,omitempty"` } @@ -1062,7 +1062,7 @@ func GranularSetIssueFields(t translations.TranslationHelperFunc) inventory.Serv // `intent` object (IssueUpdateIntent). While that migration is in // flight, gate the payload shape behind a feature flag so we can // switch over without breaking the un-migrated schema. - useIntentInput := deps.IsFeatureEnabled(ctx, FeatureFlagIssueUpdateIntent) + useIntentInput := deps.IsFeatureEnabled(ctx, FeatureFlagIssueFieldsUseUpdateIntent) for _, fieldMap := range fieldMaps { fieldID, err := RequiredParam[string](fieldMap, "field_id") if err != nil { From c64d9a0ae149dc3598889e3b6a4a63057f7c6a1a Mon Sep 17 00:00:00 2001 From: Renee Xu Date: Fri, 12 Jun 2026 02:10:59 -0700 Subject: [PATCH 4/7] Rename field to UpdateIntent to better match model name --- pkg/github/granular_tools_test.go | 2 +- pkg/github/issues_granular.go | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pkg/github/granular_tools_test.go b/pkg/github/granular_tools_test.go index 4f849f147..7e478d58f 100644 --- a/pkg/github/granular_tools_test.go +++ b/pkg/github/granular_tools_test.go @@ -2048,7 +2048,7 @@ func TestGranularSetIssueFields(t *testing.T) { TextValue: githubv4.NewString(githubv4.String("hello")), // rationale and confidence are nested under intent, // while suggest stays a flat field. - Intent: &IssueUpdateIntentInput{ + UpdateIntent: &IssueUpdateIntentInput{ Rationale: githubv4.NewString(githubv4.String("Reflects the reported severity")), Confidence: &confidence, }, diff --git a/pkg/github/issues_granular.go b/pkg/github/issues_granular.go index 207e82260..379b39314 100644 --- a/pkg/github/issues_granular.go +++ b/pkg/github/issues_granular.go @@ -917,10 +917,10 @@ type IssueFieldCreateOrUpdateInput struct { Rationale *githubv4.String `json:"rationale,omitempty"` Confidence *string `json:"confidence,omitempty"` Suggest *githubv4.Boolean `json:"suggest,omitempty"` - // Intent bundles rationale and confidence into a single object. It is the - // successor to the flat Rationale/Confidence fields above and is only sent - // when the FeatureFlagIssueFieldsUseUpdateIntent feature flag is enabled. - Intent *IssueUpdateIntentInput `json:"intent,omitempty"` + // UpdateIntent bundles rationale and confidence into a single object. It is + // the successor to the flat Rationale/Confidence fields above and is only + // sent when the FeatureFlagIssueFieldsUseUpdateIntent feature flag is enabled. + UpdateIntent *IssueUpdateIntentInput `json:"intent,omitempty"` } // IssueUpdateIntentInput is the nested input object that carries the rationale @@ -1145,7 +1145,7 @@ func GranularSetIssueFields(t translations.TranslationHelperFunc) inventory.Serv // on the feature flag. if useIntentInput { if rationalePtr != nil || confidencePtr != nil { - input.Intent = &IssueUpdateIntentInput{ + input.UpdateIntent = &IssueUpdateIntentInput{ Rationale: rationalePtr, Confidence: confidencePtr, } From 1f9cb1f66c681a08840f9621115e3518742e0dd2 Mon Sep 17 00:00:00 2001 From: Renee Xu Date: Fri, 12 Jun 2026 02:28:01 -0700 Subject: [PATCH 5/7] Remove feature flag from enduser FF allowlist --- pkg/github/feature_flags.go | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/github/feature_flags.go b/pkg/github/feature_flags.go index 40b3c46e0..17f1379c8 100644 --- a/pkg/github/feature_flags.go +++ b/pkg/github/feature_flags.go @@ -32,7 +32,6 @@ var AllowedFeatureFlags = []string{ FeatureFlagCSVOutput, FeatureFlagIFCLabels, FeatureFlagIssueFields, - FeatureFlagIssueFieldsUseUpdateIntent, FeatureFlagIssuesGranular, FeatureFlagPullRequestsGranular, } From 8b3dd5b0609910edf34526d80d3ab239289e493e Mon Sep 17 00:00:00 2001 From: Renee Xu Date: Fri, 12 Jun 2026 02:36:59 -0700 Subject: [PATCH 6/7] Fix comment --- pkg/github/feature_flags.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/github/feature_flags.go b/pkg/github/feature_flags.go index 17f1379c8..896bbfbfe 100644 --- a/pkg/github/feature_flags.go +++ b/pkg/github/feature_flags.go @@ -17,7 +17,7 @@ const FeatureFlagIFCLabels = "ifc_labels" const FeatureFlagIssueFields = "remote_mcp_issue_fields" // FeatureFlagIssueFieldsUseUpdateIntent is the feature flag name for sending the -// rationale and confidence on issue field mutations as a nested `intent` +// rationale and confidence on issue field mutations as a nested `update_intent` // object (IssueUpdateIntent) instead of as flat fields. It exists so the // set_issue_fields tool can switch payload shapes while the backend migrates // IssueFieldCreateOrUpdateInput from IssueEventRationale to IssueUpdateIntent. From f559c1913c018a2f6448ebc7b90b6644ba39b7bd Mon Sep 17 00:00:00 2001 From: Renee Xu Date: Fri, 12 Jun 2026 02:47:56 -0700 Subject: [PATCH 7/7] Renaming to match existing convention --- pkg/github/feature_flags.go | 6 +++--- pkg/github/granular_tools_test.go | 4 ++-- pkg/github/issues_granular.go | 10 +++++----- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/pkg/github/feature_flags.go b/pkg/github/feature_flags.go index fb33adb74..b51ee8594 100644 --- a/pkg/github/feature_flags.go +++ b/pkg/github/feature_flags.go @@ -16,12 +16,12 @@ const FeatureFlagIFCLabels = "ifc_labels" // and field_values enrichment in list_issues / search_issues output. const FeatureFlagIssueFields = "remote_mcp_issue_fields" -// FeatureFlagIssueFieldsUseUpdateIntent is the feature flag name for sending the -// rationale and confidence on issue field mutations as a nested `update_intent` +// FeatureFlagIssueFieldsUseIntent is the feature flag name for sending the +// rationale and confidence on issue field mutations as a nested `intent` // object (IssueUpdateIntent) instead of as flat fields. It exists so the // set_issue_fields tool can switch payload shapes while the backend migrates // IssueFieldCreateOrUpdateInput from IssueEventRationale to IssueUpdateIntent. -const FeatureFlagIssueFieldsUseUpdateIntent = "issue_fields_use_update_intent" +const FeatureFlagIssueFieldsUseIntent = "issue_fields_use_intent" // FeatureFlagFileBlame is the feature flag name for the get_file_blame tool, // which exposes git blame information for a file. It is gated so the extra tool diff --git a/pkg/github/granular_tools_test.go b/pkg/github/granular_tools_test.go index 7e478d58f..6b906fb1a 100644 --- a/pkg/github/granular_tools_test.go +++ b/pkg/github/granular_tools_test.go @@ -2048,7 +2048,7 @@ func TestGranularSetIssueFields(t *testing.T) { TextValue: githubv4.NewString(githubv4.String("hello")), // rationale and confidence are nested under intent, // while suggest stays a flat field. - UpdateIntent: &IssueUpdateIntentInput{ + Intent: &IssueUpdateIntentInput{ Rationale: githubv4.NewString(githubv4.String("Reflects the reported severity")), Confidence: &confidence, }, @@ -2073,7 +2073,7 @@ func TestGranularSetIssueFields(t *testing.T) { deps := BaseDeps{ GQLClient: gqlClient, featureChecker: func(_ context.Context, flag string) (bool, error) { - return flag == FeatureFlagIssueFieldsUseUpdateIntent, nil + return flag == FeatureFlagIssueFieldsUseIntent, nil }, } serverTool := GranularSetIssueFields(translations.NullTranslationHelper) diff --git a/pkg/github/issues_granular.go b/pkg/github/issues_granular.go index 379b39314..62da364b7 100644 --- a/pkg/github/issues_granular.go +++ b/pkg/github/issues_granular.go @@ -917,10 +917,10 @@ type IssueFieldCreateOrUpdateInput struct { Rationale *githubv4.String `json:"rationale,omitempty"` Confidence *string `json:"confidence,omitempty"` Suggest *githubv4.Boolean `json:"suggest,omitempty"` - // UpdateIntent bundles rationale and confidence into a single object. It is + // Intent bundles rationale and confidence into a single object. It is // the successor to the flat Rationale/Confidence fields above and is only - // sent when the FeatureFlagIssueFieldsUseUpdateIntent feature flag is enabled. - UpdateIntent *IssueUpdateIntentInput `json:"intent,omitempty"` + // sent when the FeatureFlagIssueFieldsUseIntent feature flag is enabled. + Intent *IssueUpdateIntentInput `json:"intent,omitempty"` } // IssueUpdateIntentInput is the nested input object that carries the rationale @@ -1062,7 +1062,7 @@ func GranularSetIssueFields(t translations.TranslationHelperFunc) inventory.Serv // `intent` object (IssueUpdateIntent). While that migration is in // flight, gate the payload shape behind a feature flag so we can // switch over without breaking the un-migrated schema. - useIntentInput := deps.IsFeatureEnabled(ctx, FeatureFlagIssueFieldsUseUpdateIntent) + useIntentInput := deps.IsFeatureEnabled(ctx, FeatureFlagIssueFieldsUseIntent) for _, fieldMap := range fieldMaps { fieldID, err := RequiredParam[string](fieldMap, "field_id") if err != nil { @@ -1145,7 +1145,7 @@ func GranularSetIssueFields(t translations.TranslationHelperFunc) inventory.Serv // on the feature flag. if useIntentInput { if rationalePtr != nil || confidencePtr != nil { - input.UpdateIntent = &IssueUpdateIntentInput{ + input.Intent = &IssueUpdateIntentInput{ Rationale: rationalePtr, Confidence: confidencePtr, }