From 2f68b309a02aecbaa541fd1df0b6b2190d73377c Mon Sep 17 00:00:00 2001 From: Boaz Reicher <44614829+boazreicher@users.noreply.github.com> Date: Mon, 8 Jun 2026 05:19:38 +0000 Subject: [PATCH 1/3] Using issues suggestions feature flag --- pkg/github/granular_tools_test.go | 99 +++++++++++++++++++++++++++++++ pkg/github/issues_granular.go | 6 +- 2 files changed, 104 insertions(+), 1 deletion(-) diff --git a/pkg/github/granular_tools_test.go b/pkg/github/granular_tools_test.go index eb688a0b9f..6c6c583a50 100644 --- a/pkg/github/granular_tools_test.go +++ b/pkg/github/granular_tools_test.go @@ -8,6 +8,8 @@ import ( "github.com/github/github-mcp-server/internal/githubv4mock" "github.com/github/github-mcp-server/internal/toolsnaps" + "github.com/github/github-mcp-server/pkg/http/headers" + transportpkg "github.com/github/github-mcp-server/pkg/http/transport" "github.com/github/github-mcp-server/pkg/inventory" "github.com/github/github-mcp-server/pkg/translations" gogithub "github.com/google/go-github/v87/github" @@ -1802,4 +1804,101 @@ func TestGranularSetIssueFields(t *testing.T) { require.NoError(t, err) assert.False(t, result.IsError) }) + + t.Run("sends GraphQL-Features: update_issue_suggestions header on mutation", func(t *testing.T) { + 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")), + }, + }, + }, + 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", + }, + }, + }), + ), + } + + // Build a transport chain matching production: GraphQLFeaturesTransport + // wraps a header-capturing spy, which forwards to the mock's RoundTripper. + // This verifies the mutation request sets the update_issue_suggestions + // feature flag so the rationale/suggest input fields are accepted. + mockClient := githubv4mock.NewMockedHTTPClient(matchers...) + spy := &headerCaptureTransport{inner: mockClient.Transport} + httpClient := &http.Client{ + Transport: &transportpkg.GraphQLFeaturesTransport{Transport: spy}, + } + gqlClient := githubv4.NewClient(httpClient) + deps := BaseDeps{GQLClient: gqlClient} + 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"}, + }, + }) + result, err := handler(ContextWithDeps(context.Background(), deps), &request) + require.NoError(t, err) + require.False(t, result.IsError, getTextResult(t, result).Text) + // The last request captured is the mutation; the preceding issue ID + // query does not require the feature flag. + assert.Equal(t, "update_issue_suggestions", spy.captured.Get(headers.GraphQLFeaturesHeader)) + }) } diff --git a/pkg/github/issues_granular.go b/pkg/github/issues_granular.go index 22d26cc47f..4d6cf12a9d 100644 --- a/pkg/github/issues_granular.go +++ b/pkg/github/issues_granular.go @@ -7,6 +7,7 @@ import ( "maps" "strings" + ghcontext "github.com/github/github-mcp-server/pkg/context" ghErrors "github.com/github/github-mcp-server/pkg/errors" "github.com/github/github-mcp-server/pkg/inventory" "github.com/github/github-mcp-server/pkg/scopes" @@ -1170,7 +1171,10 @@ func GranularSetIssueFields(t translations.TranslationHelperFunc) inventory.Serv IssueFields: issueFields, } - if err := gqlClient.Mutate(ctx, &mutation, mutationInput, nil); err != nil { + // The rationale and suggest input fields on IssueFieldCreateOrUpdateInput + // are gated behind the update_issue_suggestions GraphQL feature flag. + ctxWithFeatures := ghcontext.WithGraphQLFeatures(ctx, "update_issue_suggestions") + if err := gqlClient.Mutate(ctxWithFeatures, &mutation, mutationInput, nil); err != nil { return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "failed to set issue field values", err), nil, nil } From cf0fff7b6ae970cc449512b832dfc713aec34286 Mon Sep 17 00:00:00 2001 From: Boaz Reicher <44614829+boazreicher@users.noreply.github.com> Date: Mon, 8 Jun 2026 09:42:15 +0000 Subject: [PATCH 2/3] Gate set_issue_fields confidence behind update_issue_confidence flag The GitHub GraphQL API does not yet accept the per-field confidence input on setIssueFieldValue mutations. Hide it from the user-facing schema and drop it from the mutation payload unless the new update_issue_confidence feature flag is enabled so users do not try to use it before the API supports it. --- .../__toolsnaps__/set_issue_fields.snap | 11 +- pkg/github/feature_flags.go | 8 ++ pkg/github/granular_tools_test.go | 106 +++++++++++++++++- pkg/github/issues_granular.go | 31 ++--- 4 files changed, 129 insertions(+), 27 deletions(-) diff --git a/pkg/github/__toolsnaps__/set_issue_fields.snap b/pkg/github/__toolsnaps__/set_issue_fields.snap index e46febeeda..88c88fdc65 100644 --- a/pkg/github/__toolsnaps__/set_issue_fields.snap +++ b/pkg/github/__toolsnaps__/set_issue_fields.snap @@ -4,22 +4,13 @@ "openWorldHint": true, "title": "Set Issue Fields" }, - "description": "Set issue field values for an issue. Fields are organization-level custom fields (text, number, date, or single select). Use this to create or update field values on an issue. When setting values, include a confidence level (low, medium, or high) reflecting how certain you are about the choice.", + "description": "Set issue field values for an issue. Fields are organization-level custom fields (text, number, date, or single select). Use this to create or update field values on an issue.", "inputSchema": { "properties": { "fields": { "description": "Array of issue field values to set. Each element must have a 'field_id' (string, the GraphQL node ID of the field) and exactly one value field: 'text_value' for text fields, 'number_value' for number fields, 'date_value' (ISO 8601 date string) for date fields, or 'single_select_option_id' (the GraphQL node ID of the option) for single select fields. Set 'delete' to true to remove a field value.", "items": { "properties": { - "confidence": { - "description": "How confident you are in this choice. Use 'high' for clear signal or explicit user request, 'medium' for reasonable inference with some ambiguity, 'low' for best guess with limited signal.", - "enum": [ - "low", - "medium", - "high" - ], - "type": "string" - }, "date_value": { "description": "The value to set for a date field (ISO 8601 date string)", "type": "string" diff --git a/pkg/github/feature_flags.go b/pkg/github/feature_flags.go index 0f77f6c872..eb6e00ad5f 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" +// FeatureFlagIssueConfidence is the feature flag name for exposing the +// per-field `confidence` input on the set_issue_fields GraphQL mutation. The +// GitHub GraphQL API does not yet accept the confidence input, so the schema +// hides the parameter and the handler drops it from the mutation payload +// unless this flag is enabled. +const FeatureFlagIssueConfidence = "update_issue_confidence" + // 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, + FeatureFlagIssueConfidence, FeatureFlagIssuesGranular, FeatureFlagPullRequestsGranular, } diff --git a/pkg/github/granular_tools_test.go b/pkg/github/granular_tools_test.go index 6c6c583a50..9041ee30e7 100644 --- a/pkg/github/granular_tools_test.go +++ b/pkg/github/granular_tools_test.go @@ -1668,7 +1668,12 @@ func TestGranularSetIssueFields(t *testing.T) { } gqlClient := githubv4.NewClient(githubv4mock.NewMockedHTTPClient(matchers...)) - deps := BaseDeps{GQLClient: gqlClient} + deps := BaseDeps{ + GQLClient: gqlClient, + featureChecker: func(_ context.Context, flag string) (bool, error) { + return flag == FeatureFlagIssueConfidence, nil + }, + } serverTool := GranularSetIssueFields(translations.NullTranslationHelper) handler := serverTool.Handler(deps) @@ -1690,7 +1695,11 @@ func TestGranularSetIssueFields(t *testing.T) { }) t.Run("invalid confidence value returns error", func(t *testing.T) { - deps := BaseDeps{} + deps := BaseDeps{ + featureChecker: func(_ context.Context, flag string) (bool, error) { + return flag == FeatureFlagIssueConfidence, nil + }, + } serverTool := GranularSetIssueFields(translations.NullTranslationHelper) handler := serverTool.Handler(deps) @@ -1712,6 +1721,99 @@ func TestGranularSetIssueFields(t *testing.T) { assert.Contains(t, textContent.Text, "confidence must be one of: low, medium, high") }) + t.Run("confidence is dropped when feature flag is disabled", func(t *testing.T) { + 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"}, + }, + }), + ), + // Expect the mutation input WITHOUT Confidence, proving the handler + // dropped the user-supplied value because the feature flag is off. + 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")), + }, + }, + }, + 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} + 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", + // Confidence is supplied but should be silently dropped + // because FeatureFlagIssueConfidence is off. + "confidence": "high", + }, + }, + }) + result, err := handler(ContextWithDeps(context.Background(), deps), &request) + require.NoError(t, err) + assert.False(t, result.IsError, getTextResult(t, result).Text) + }) + t.Run("successful set with suggest flag", func(t *testing.T) { suggestTrue := githubv4.Boolean(true) matchers := []githubv4mock.Matcher{ diff --git a/pkg/github/issues_granular.go b/pkg/github/issues_granular.go index 4d6cf12a9d..0a7c4a82c5 100644 --- a/pkg/github/issues_granular.go +++ b/pkg/github/issues_granular.go @@ -925,7 +925,7 @@ func GranularSetIssueFields(t translations.TranslationHelperFunc) inventory.Serv ToolsetMetadataIssues, mcp.Tool{ Name: "set_issue_fields", - Description: t("TOOL_SET_ISSUE_FIELDS_DESCRIPTION", "Set issue field values for an issue. Fields are organization-level custom fields (text, number, date, or single select). Use this to create or update field values on an issue. When setting values, include a confidence level (low, medium, or high) reflecting how certain you are about the choice."), + Description: t("TOOL_SET_ISSUE_FIELDS_DESCRIPTION", "Set issue field values for an issue. Fields are organization-level custom fields (text, number, date, or single select). Use this to create or update field values on an issue."), Annotations: &mcp.ToolAnnotations{ Title: t("TOOL_SET_ISSUE_FIELDS_USER_TITLE", "Set Issue Fields"), ReadOnlyHint: false, @@ -985,11 +985,6 @@ func GranularSetIssueFields(t translations.TranslationHelperFunc) inventory.Serv "State the concrete signal (e.g. 'Reports a crash when saving' → high priority).", MaxLength: jsonschema.Ptr(280), }, - "confidence": { - Type: "string", - Description: "How confident you are in this choice. Use 'high' for clear signal or explicit user request, 'medium' for reasonable inference with some ambiguity, 'low' for best guess with limited signal.", - Enum: []any{"low", "medium", "high"}, - }, "is_suggestion": { Type: "boolean", Description: "If true, this field value is sent to the API as a suggestion (suggest:true) rather than an applied value. " + @@ -1107,15 +1102,21 @@ func GranularSetIssueFields(t translations.TranslationHelperFunc) inventory.Serv } } - confidence, err := OptionalParam[string](fieldMap, "confidence") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - if confidence != "" && confidence != "low" && confidence != "medium" && confidence != "high" { - return utils.NewToolResultError("confidence must be one of: low, medium, high"), nil, nil - } - if confidence != "" { - input.Confidence = &confidence + // The `confidence` input is gated behind FeatureFlagIssueConfidence + // because the GitHub GraphQL API does not yet accept it. When the + // flag is off the schema hides the field and the handler drops + // any value supplied by older callers from the mutation payload. + if deps.IsFeatureEnabled(ctx, FeatureFlagIssueConfidence) { + confidence, err := OptionalParam[string](fieldMap, "confidence") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + if confidence != "" && confidence != "low" && confidence != "medium" && confidence != "high" { + return utils.NewToolResultError("confidence must be one of: low, medium, high"), nil, nil + } + if confidence != "" { + input.Confidence = &confidence + } } isSuggestion, err := OptionalParam[bool](fieldMap, "is_suggestion") From ef60245f3d54aa177faf4697bc366f49e206b84f Mon Sep 17 00:00:00 2001 From: Boaz Reicher <44614829+boazreicher@users.noreply.github.com> Date: Tue, 9 Jun 2026 12:50:15 +0000 Subject: [PATCH 3/3] adding back confidence --- .../__toolsnaps__/set_issue_fields.snap | 4 +++ pkg/github/feature_flags.go | 8 ------ pkg/github/granular_tools_test.go | 25 +++++------------ pkg/github/issues_granular.go | 28 +++++++++---------- 4 files changed, 24 insertions(+), 41 deletions(-) diff --git a/pkg/github/__toolsnaps__/set_issue_fields.snap b/pkg/github/__toolsnaps__/set_issue_fields.snap index 88c88fdc65..a29ec51d27 100644 --- a/pkg/github/__toolsnaps__/set_issue_fields.snap +++ b/pkg/github/__toolsnaps__/set_issue_fields.snap @@ -11,6 +11,10 @@ "description": "Array of issue field values to set. Each element must have a 'field_id' (string, the GraphQL node ID of the field) and exactly one value field: 'text_value' for text fields, 'number_value' for number fields, 'date_value' (ISO 8601 date string) for date fields, or 'single_select_option_id' (the GraphQL node ID of the option) for single select fields. Set 'delete' to true to remove a field value.", "items": { "properties": { + "confidence": { + "description": "How confident you are in this field value: low, medium, or high.", + "type": "string" + }, "date_value": { "description": "The value to set for a date field (ISO 8601 date string)", "type": "string" diff --git a/pkg/github/feature_flags.go b/pkg/github/feature_flags.go index eb6e00ad5f..0f77f6c872 100644 --- a/pkg/github/feature_flags.go +++ b/pkg/github/feature_flags.go @@ -16,13 +16,6 @@ const FeatureFlagIFCLabels = "ifc_labels" // and field_values enrichment in list_issues / search_issues output. const FeatureFlagIssueFields = "remote_mcp_issue_fields" -// FeatureFlagIssueConfidence is the feature flag name for exposing the -// per-field `confidence` input on the set_issue_fields GraphQL mutation. The -// GitHub GraphQL API does not yet accept the confidence input, so the schema -// hides the parameter and the handler drops it from the mutation payload -// unless this flag is enabled. -const FeatureFlagIssueConfidence = "update_issue_confidence" - // 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. @@ -32,7 +25,6 @@ var AllowedFeatureFlags = []string{ FeatureFlagCSVOutput, FeatureFlagIFCLabels, FeatureFlagIssueFields, - FeatureFlagIssueConfidence, FeatureFlagIssuesGranular, FeatureFlagPullRequestsGranular, } diff --git a/pkg/github/granular_tools_test.go b/pkg/github/granular_tools_test.go index 9041ee30e7..27e8079f97 100644 --- a/pkg/github/granular_tools_test.go +++ b/pkg/github/granular_tools_test.go @@ -1668,12 +1668,7 @@ func TestGranularSetIssueFields(t *testing.T) { } gqlClient := githubv4.NewClient(githubv4mock.NewMockedHTTPClient(matchers...)) - deps := BaseDeps{ - GQLClient: gqlClient, - featureChecker: func(_ context.Context, flag string) (bool, error) { - return flag == FeatureFlagIssueConfidence, nil - }, - } + deps := BaseDeps{GQLClient: gqlClient} serverTool := GranularSetIssueFields(translations.NullTranslationHelper) handler := serverTool.Handler(deps) @@ -1695,11 +1690,7 @@ func TestGranularSetIssueFields(t *testing.T) { }) t.Run("invalid confidence value returns error", func(t *testing.T) { - deps := BaseDeps{ - featureChecker: func(_ context.Context, flag string) (bool, error) { - return flag == FeatureFlagIssueConfidence, nil - }, - } + deps := BaseDeps{} serverTool := GranularSetIssueFields(translations.NullTranslationHelper) handler := serverTool.Handler(deps) @@ -1721,7 +1712,8 @@ func TestGranularSetIssueFields(t *testing.T) { assert.Contains(t, textContent.Text, "confidence must be one of: low, medium, high") }) - t.Run("confidence is dropped when feature flag is disabled", func(t *testing.T) { + t.Run("confidence is sent when supplied", func(t *testing.T) { + confidence := "high" matchers := []githubv4mock.Matcher{ githubv4mock.NewQueryMatcher( struct { @@ -1742,8 +1734,6 @@ func TestGranularSetIssueFields(t *testing.T) { }, }), ), - // Expect the mutation input WITHOUT Confidence, proving the handler - // dropped the user-supplied value because the feature flag is off. githubv4mock.NewMutationMatcher( struct { SetIssueFieldValue struct { @@ -1772,8 +1762,9 @@ func TestGranularSetIssueFields(t *testing.T) { IssueID: githubv4.ID("ISSUE_123"), IssueFields: []IssueFieldCreateOrUpdateInput{ { - FieldID: githubv4.ID("FIELD_1"), - TextValue: githubv4.NewString(githubv4.String("hello")), + FieldID: githubv4.ID("FIELD_1"), + TextValue: githubv4.NewString(githubv4.String("hello")), + Confidence: &confidence, }, }, }, @@ -1803,8 +1794,6 @@ func TestGranularSetIssueFields(t *testing.T) { map[string]any{ "field_id": "FIELD_1", "text_value": "hello", - // Confidence is supplied but should be silently dropped - // because FeatureFlagIssueConfidence is off. "confidence": "high", }, }, diff --git a/pkg/github/issues_granular.go b/pkg/github/issues_granular.go index 0a7c4a82c5..9813b4ff46 100644 --- a/pkg/github/issues_granular.go +++ b/pkg/github/issues_granular.go @@ -985,6 +985,10 @@ func GranularSetIssueFields(t translations.TranslationHelperFunc) inventory.Serv "State the concrete signal (e.g. 'Reports a crash when saving' → high priority).", MaxLength: jsonschema.Ptr(280), }, + "confidence": { + Type: "string", + Description: "How confident you are in this field value: low, medium, or high.", + }, "is_suggestion": { Type: "boolean", Description: "If true, this field value is sent to the API as a suggestion (suggest:true) rather than an applied value. " + @@ -1102,21 +1106,15 @@ func GranularSetIssueFields(t translations.TranslationHelperFunc) inventory.Serv } } - // The `confidence` input is gated behind FeatureFlagIssueConfidence - // because the GitHub GraphQL API does not yet accept it. When the - // flag is off the schema hides the field and the handler drops - // any value supplied by older callers from the mutation payload. - if deps.IsFeatureEnabled(ctx, FeatureFlagIssueConfidence) { - confidence, err := OptionalParam[string](fieldMap, "confidence") - if err != nil { - return utils.NewToolResultError(err.Error()), nil, nil - } - if confidence != "" && confidence != "low" && confidence != "medium" && confidence != "high" { - return utils.NewToolResultError("confidence must be one of: low, medium, high"), nil, nil - } - if confidence != "" { - input.Confidence = &confidence - } + confidence, err := OptionalParam[string](fieldMap, "confidence") + if err != nil { + return utils.NewToolResultError(err.Error()), nil, nil + } + if confidence != "" && confidence != "low" && confidence != "medium" && confidence != "high" { + return utils.NewToolResultError("confidence must be one of: low, medium, high"), nil, nil + } + if confidence != "" { + input.Confidence = &confidence } isSuggestion, err := OptionalParam[bool](fieldMap, "is_suggestion")