Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions pkg/github/feature_flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ const FeatureFlagIFCLabels = "ifc_labels"
// and field_values enrichment in list_issues / search_issues output.
const FeatureFlagIssueFields = "remote_mcp_issue_fields"

// 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 FeatureFlagIssueFieldsUseIntent = "issue_fields_use_intent"
Comment on lines +20 to +24

@reneexeener reneexeener Jun 12, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is intentional because this feature flag shouldn't be user-facing. The feature flag issue_fields_use_intent will be rolled out after the GraphQL mutation is updated.

Comment thread
reneexeener marked this conversation as resolved.

// 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
// is not advertised by default, keeping the tool surface small unless opted in.
Expand Down
105 changes: 105 additions & 0 deletions pkg/github/granular_tools_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 == FeatureFlagIssueFieldsUseIntent, 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)
})
}
41 changes: 39 additions & 2 deletions pkg/github/issues_granular.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 FeatureFlagIssueFieldsUseIntent 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.
Expand Down Expand Up @@ -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, FeatureFlagIssueFieldsUseIntent)
for _, fieldMap := range fieldMaps {
fieldID, err := RequiredParam[string](fieldMap, "field_id")
if err != nil {
Expand Down Expand Up @@ -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 {
Expand All @@ -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))
}
}

Expand All @@ -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")
Expand Down
Loading