Skip to content

Commit f59fce9

Browse files
Simplify feature-flag handling: collapse CSV dual-variant + skip filtering when no checker (#2516)
* refactor: generic toolset+name sort, clarify feature flag intent Address review feedback on #2450: - Collapse the three near-identical sort helpers in pkg/inventory/filters.go into a generic sortByToolsetThenName so adding new inventory item types doesn't require copying the comparator. - Expand the doc comments on the three *WithoutFeatureFiltering helpers to spell out why they exist: HTTP mode builds a static (process-wide) inventory as an upper bound, but per-request feature flags from headers (X-MCP-Features, X-MCP-Insiders) are evaluated later, so feature-flagged variants must be preserved here. - Strengthen the doc comment on ResolveFeatureFlags to make the contract explicit: user-supplied flags are validated against AllowedFeatureFlags, but insiders expansion deliberately is not — InsidersFeatureFlags may include server-controlled flags that are not user-toggleable. CORS comments are intentionally left for the PR author. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs(feature-flags): clarify allowed and insiders sets are independent Also add tests covering: - a user-toggleable flag (FeatureFlagIssuesGranular) that insiders does not turn on automatically - insiders mode not turning on user-only allowed flags Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(inventory): collapse three *WithoutFeatureFiltering helpers into StaticUpperBound The three parallel methods (AvailableToolsWithoutFeatureFiltering, AvailableResourceTemplatesWithoutFeatureFiltering, AvailablePromptsWithoutFeatureFiltering) were always called as a triple in exactly two places: HTTP buildStaticInventory and its test mirror. They exist because the dual-variant pattern (sibling tools with mirrored FeatureFlagEnable / FeatureFlagDisable on the same name, e.g. CSV output) makes feature filtering at static-build time impossible — both variants must be kept and resolved per-request. Replace the three with one method, Inventory.StaticUpperBound(ctx), that returns (tools, resources, prompts) and carries the rationale in its doc comment. Reduces API surface, eliminates the triplication, and makes the single "skip feature filtering" concept obvious to readers. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor: simplify feature-flag handling Two related simplifications, both about treating insiders as a meta flag that expands once at startup and then stops mattering: - Collapse CSV's dual-variant pattern into a single tool whose handler performs a runtime feature-flag check via deps.IsFeatureEnabled. CSV is a pure response-format toggle, not a schema change, so it does not need the dual-name pattern that genuine schema variants (granular issues/PRs) still use. - When no feature checker is installed, skip feature-flag filtering and return the full upper bound. The static HTTP inventory now uses plain AvailableTools/Resources/Prompts; the per-request inventory always installs a checker, so MCP registration (which serves a tool name once) always sees a deduplicated set. The bespoke StaticUpperBound helper and the isToolEnabledWithFeatureFlags split go away. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * ci(mcp-diff): add insiders + per-feature configs The mcp-diff matrix now includes: - --insiders (and --insiders --read-only) - one config per github.AllowedFeatureFlags entry, generated by script/print-mcp-diff-configs so new user-controllable flags get diffed automatically without editing the workflow Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs(insiders): explain feature-flag resolution for contributors Adds a 'How feature flags are resolved' section covering: - Insiders is a meta flag, like 'all'/'default' for toolsets - User input -> allowlist filter -> insiders expansion -> server-side fallback (remote only) - AllowedFeatureFlags vs InsidersFeatureFlags are independent - How to add a new feature flag, including the TestGitHubPackageDoesNotReadInsidersMode guard Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(inventory): make feature-flag gating a regular ToolFilter Move tool feature-flag evaluation out of isToolEnabled and into a ToolFilter installed at the head of the pipeline by Build() when WithFeatureChecker received a non-nil checker. The 'no checker = no filtering' contract is now expressed structurally (the filter isn't installed) instead of by a runtime nil check inside the helper. Resources and prompts have no filter pipeline, so they call the now-pure featureFlagAllowed helper behind an explicit r.featureChecker != nil guard at the iteration site. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * perf(inventory): cache extracted toolset IDs in sort comparator Avoid evaluating the extractor closures up to three times per comparison. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent bfa6fee commit f59fce9

13 files changed

Lines changed: 357 additions & 233 deletions

File tree

.github/workflows/mcp-diff.yml

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -19,32 +19,35 @@ jobs:
1919
with:
2020
fetch-depth: 0
2121

22+
- name: Set up Go
23+
uses: actions/setup-go@v5
24+
with:
25+
go-version-file: go.mod
26+
2227
- name: Build UI
2328
uses: ./.github/actions/build-ui
2429

30+
- name: Generate diff configurations
31+
id: configs
32+
# The generator imports pkg/github so any new entry in
33+
# AllowedFeatureFlags is automatically diffed without touching this
34+
# workflow. See script/print-mcp-diff-configs/main.go.
35+
run: |
36+
{
37+
echo 'configurations<<MCP_DIFF_EOF'
38+
go run ./script/print-mcp-diff-configs
39+
echo 'MCP_DIFF_EOF'
40+
} >> "$GITHUB_OUTPUT"
41+
2542
- name: Run MCP Server Diff
2643
uses: SamMorrowDrums/mcp-server-diff@v2.3.5
2744
with:
28-
setup_go: "true"
45+
setup_go: "false"
2946
install_command: go mod download
3047
start_command: go run ./cmd/github-mcp-server stdio
3148
env_vars: |
3249
GITHUB_PERSONAL_ACCESS_TOKEN=test-token
33-
configurations: |
34-
[
35-
{"name": "default", "args": ""},
36-
{"name": "read-only", "args": "--read-only"},
37-
{"name": "toolsets-repos", "args": "--toolsets=repos"},
38-
{"name": "toolsets-issues", "args": "--toolsets=issues"},
39-
{"name": "toolsets-context", "args": "--toolsets=context"},
40-
{"name": "toolsets-pull_requests", "args": "--toolsets=pull_requests"},
41-
{"name": "toolsets-repos,issues", "args": "--toolsets=repos,issues"},
42-
{"name": "toolsets-issues,context", "args": "--toolsets=issues,context"},
43-
{"name": "toolsets-all", "args": "--toolsets=all"},
44-
{"name": "tools-get_me", "args": "--tools=get_me"},
45-
{"name": "tools-get_me,list_issues", "args": "--tools=get_me,list_issues"},
46-
{"name": "toolsets-repos+read-only", "args": "--toolsets=repos --read-only"}
47-
]
50+
configurations: ${{ steps.configs.outputs.configurations }}
4851

4952
- name: Add interpretation note
5053
if: always()

docs/insiders-features.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,3 +67,35 @@ github-mcp-server stdio --features csv_output
6767
```
6868

6969
Because this changes list tool response shape, clients that require JSON list responses should avoid enabling this feature.
70+
71+
---
72+
73+
## How feature flags are resolved
74+
75+
> [!NOTE]
76+
> This section is for contributors. End users only need the table at the top of this page.
77+
78+
Insiders is a **meta feature flag** — the same shape as `default` or `all` for toolsets. It expands once at startup into a curated set of individual feature flags, and from that point on every code path keys off concrete flags, never `InsidersMode` directly. New experimental work should always get its own flag and then be added to the insiders expansion list, never folded into `insiders` as a catch-all.
79+
80+
### Resolution order
81+
82+
1. **User input.** Users may opt into specific features:
83+
- Local server: `--features=<flag>,<flag>` CLI flag (or `GITHUB_FEATURES` env var).
84+
- Self-hosted HTTP server: `X-MCP-Features: <flag>,<flag>` request header.
85+
2. **Allowlist filter.** User-supplied flags are filtered against [`AllowedFeatureFlags`](../pkg/github/feature_flags.go). Anything not on the allowlist is silently dropped — flags missing from the allowlist can only be turned on by remote-server feature management, not by end users.
86+
3. **Insiders expansion.** If insiders mode is on (`--insiders`, `/insiders` route, or `X-MCP-Insiders: true`), every flag in [`InsidersFeatureFlags`](../pkg/github/feature_flags.go) is unioned in. The insiders expansion is **not** re-validated against the allowlist — insiders is a server-controlled switch that can reach internal-only flags.
87+
4. **Server-side fallback (remote server only).** Any flag not yet decided falls back to the remote server's feature manager, which can roll a feature out independently of user input or insiders membership.
88+
89+
`AllowedFeatureFlags` and `InsidersFeatureFlags` are deliberately independent sets:
90+
91+
- A flag in **`AllowedFeatureFlags` only** is a regular opt-in: users can turn it on, but insiders does not auto-enable it. Granular issues/PRs flags work this way.
92+
- A flag in **`InsidersFeatureFlags` only** is reachable through insiders (and remote-server rollouts), but cannot be enabled by user input. Internal-only experiments work this way.
93+
- A flag in **both** is opt-in for end users *and* automatically on under insiders.
94+
95+
### Adding a new feature flag
96+
97+
1. Add a constant in `pkg/github/feature_flags.go`.
98+
2. Add it to `AllowedFeatureFlags` if end users should be able to opt in via `--features` / `X-MCP-Features`.
99+
3. Add it to `InsidersFeatureFlags` if insiders mode should turn it on automatically.
100+
4. Gate the behavior on the concrete flag (`deps.IsFeatureEnabled(ctx, FeatureFlagX)`), never on `cfg.InsidersMode`. There is a `TestGitHubPackageDoesNotReadInsidersMode` guard test that fails if `pkg/github` reads `InsidersMode` directly.
101+
5. The MCP-diff CI workflow picks up new entries in `AllowedFeatureFlags` automatically — see `.github/workflows/mcp-diff.yml`.

pkg/github/csv_output.go

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -42,24 +42,18 @@ type csvOutputDocument struct {
4242
rows []map[string]string
4343
}
4444

45-
func withCSVOutputVariants(tools []inventory.ServerTool) []inventory.ServerTool {
46-
result := make([]inventory.ServerTool, 0, len(tools))
47-
for _, tool := range tools {
48-
if !isCSVOutputTool(tool) {
49-
result = append(result, tool)
45+
// withCSVOutput wraps the handler of every default-toolset list_* tool so that,
46+
// at request time, it checks the csv_output feature flag and converts the JSON
47+
// text response to CSV when enabled. The tool's schema, name, and scope are
48+
// unchanged — only the response payload format differs.
49+
func withCSVOutput(tools []inventory.ServerTool) []inventory.ServerTool {
50+
for i := range tools {
51+
if !isCSVOutputTool(tools[i]) {
5052
continue
5153
}
52-
53-
jsonOnly := tool
54-
jsonOnly.FeatureFlagDisable = FeatureFlagCSVOutput
55-
result = append(result, jsonOnly)
56-
57-
csvCapable := tool
58-
csvCapable.FeatureFlagEnable = FeatureFlagCSVOutput
59-
csvCapable.HandlerFunc = wrapHandlerWithCSVOutput(tool.HandlerFunc)
60-
result = append(result, csvCapable)
54+
tools[i].HandlerFunc = wrapHandlerWithCSVOutput(tools[i].HandlerFunc)
6155
}
62-
return result
56+
return tools
6357
}
6458

6559
func isCSVOutputTool(tool inventory.ServerTool) bool {
@@ -75,12 +69,15 @@ func isCSVOutputTool(tool inventory.ServerTool) bool {
7569
func wrapHandlerWithCSVOutput(next inventory.HandlerFunc) inventory.HandlerFunc {
7670
return func(deps any) mcp.ToolHandler {
7771
handler := next(deps)
72+
csvDeps, _ := deps.(ToolDependencies)
7873
return func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
7974
result, err := handler(ctx, req)
8075
if err != nil || result == nil || result.IsError {
8176
return result, err
8277
}
83-
78+
if csvDeps == nil || !csvDeps.IsFeatureEnabled(ctx, FeatureFlagCSVOutput) {
79+
return result, nil
80+
}
8481
return convertJSONTextResultToCSV(result), nil
8582
}
8683
}

pkg/github/csv_output_test.go

Lines changed: 63 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -14,55 +14,55 @@ import (
1414
"github.com/stretchr/testify/require"
1515
)
1616

17-
func TestCSVOutputVariantsAreFeatureGated(t *testing.T) {
17+
func TestCSVOutputAppliedToDefaultListTools(t *testing.T) {
1818
listTool := testCSVOutputTool("list_things", `[{"number":1}]`)
1919
getTool := testCSVOutputTool("get_thing", `{"number":1}`)
2020

21-
tools := withCSVOutputVariants([]inventory.ServerTool{listTool, getTool})
22-
require.Len(t, tools, 3)
21+
tools := withCSVOutput([]inventory.ServerTool{listTool, getTool})
22+
require.Len(t, tools, 2)
2323

24-
inv := buildCSVOutputInventory(t, tools, false)
25-
available := inv.AvailableTools(context.Background())
26-
require.Len(t, available, 2)
24+
// CSV mode does not introduce variants or change tool gating; both tools
25+
// remain visible regardless of feature flag state.
26+
for _, csvOutputEnabled := range []bool{false, true} {
27+
inv := buildCSVOutputInventory(t, tools, csvOutputEnabled)
28+
available := inv.AvailableTools(context.Background())
29+
require.Len(t, available, 2)
2730

28-
jsonOnly := requireToolByName(t, available, "list_things")
29-
assert.Empty(t, jsonOnly.FeatureFlagEnable)
30-
assert.Equal(t, FeatureFlagCSVOutput, jsonOnly.FeatureFlagDisable)
31+
listing := requireToolByName(t, available, "list_things")
32+
assert.Empty(t, listing.FeatureFlagEnable)
33+
assert.Empty(t, listing.FeatureFlagDisable)
3134

32-
getThing := requireToolByName(t, available, "get_thing")
33-
assert.Empty(t, getThing.FeatureFlagEnable)
34-
assert.Empty(t, getThing.FeatureFlagDisable)
35-
36-
inv = buildCSVOutputInventory(t, tools, true)
37-
available = inv.AvailableTools(context.Background())
38-
require.Len(t, available, 2)
39-
40-
csvCapable := requireToolByName(t, available, "list_things")
41-
assert.Equal(t, FeatureFlagCSVOutput, csvCapable.FeatureFlagEnable)
42-
assert.Empty(t, csvCapable.FeatureFlagDisable)
35+
getting := requireToolByName(t, available, "get_thing")
36+
assert.Empty(t, getting.FeatureFlagEnable)
37+
assert.Empty(t, getting.FeatureFlagDisable)
38+
}
4339
}
4440

45-
func TestCSVOutputVariantsOnlyApplyToDefaultToolsets(t *testing.T) {
41+
func TestCSVOutputOnlyAppliesToDefaultToolsets(t *testing.T) {
4642
nonDefaultListTool := testCSVOutputToolWithToolset("list_discussions", `[{"number":1}]`, ToolsetMetadataDiscussions)
4743

48-
tools := withCSVOutputVariants([]inventory.ServerTool{nonDefaultListTool})
44+
tools := withCSVOutput([]inventory.ServerTool{nonDefaultListTool})
4945
require.Len(t, tools, 1)
5046

51-
assert.Empty(t, tools[0].FeatureFlagEnable)
52-
assert.Empty(t, tools[0].FeatureFlagDisable)
47+
// Non-default toolset list tools are not wrapped: even with the flag on,
48+
// the response stays in JSON form.
49+
deps := newCSVOutputTestDeps(true)
50+
result, err := tools[0].Handler(deps)(ContextWithDeps(context.Background(), deps), testCSVOutputRequest())
51+
require.NoError(t, err)
52+
assert.JSONEq(t, `[{"number":1}]`, textResult(t, result))
5353
}
5454

55-
func TestCSVOutputVariantDoesNotExposeFormatParameter(t *testing.T) {
56-
tools := withCSVOutputVariants([]inventory.ServerTool{testCSVOutputTool("list_things", `[{"number":1}]`)})
57-
csvCapable := requireCSVOutputVariant(t, tools)
55+
func TestCSVOutputDoesNotExposeFormatParameter(t *testing.T) {
56+
tools := withCSVOutput([]inventory.ServerTool{testCSVOutputTool("list_things", `[{"number":1}]`)})
57+
require.Len(t, tools, 1)
5858

59-
schema, ok := csvCapable.Tool.InputSchema.(*jsonschema.Schema)
59+
schema, ok := tools[0].Tool.InputSchema.(*jsonschema.Schema)
6060
require.True(t, ok)
6161
assert.NotContains(t, schema.Properties, "output_format")
6262
}
6363

64-
func TestCSVOutputVariantConvertsJSONTextToCSV(t *testing.T) {
65-
tools := withCSVOutputVariants([]inventory.ServerTool{
64+
func TestCSVOutputConvertsJSONTextToCSVWhenFlagOn(t *testing.T) {
65+
tools := withCSVOutput([]inventory.ServerTool{
6666
testCSVOutputTool("list_things", `[
6767
{
6868
"number": 1,
@@ -72,10 +72,10 @@ func TestCSVOutputVariantConvertsJSONTextToCSV(t *testing.T) {
7272
}
7373
]`),
7474
})
75-
inv := buildCSVOutputInventory(t, tools, true)
76-
csvCapable := requireToolByName(t, inv.AvailableTools(context.Background()), "list_things")
75+
require.Len(t, tools, 1)
7776

78-
result, err := csvCapable.Handler(nil)(context.Background(), testCSVOutputRequest())
77+
deps := newCSVOutputTestDeps(true)
78+
result, err := tools[0].Handler(deps)(ContextWithDeps(context.Background(), deps), testCSVOutputRequest())
7979
require.NoError(t, err)
8080
require.NotNil(t, result)
8181
require.False(t, result.IsError)
@@ -92,6 +92,22 @@ func TestCSVOutputVariantConvertsJSONTextToCSV(t *testing.T) {
9292
assert.Equal(t, "octocat", row["user.login"])
9393
}
9494

95+
func TestCSVOutputPreservesOriginalJSONWhenFlagOff(t *testing.T) {
96+
const jsonResponse = `[{"number":1,"user":{"login":"octocat"}}]`
97+
tools := withCSVOutput([]inventory.ServerTool{testCSVOutputTool("list_things", jsonResponse)})
98+
require.Len(t, tools, 1)
99+
100+
deps := newCSVOutputTestDeps(false)
101+
result, err := tools[0].Handler(deps)(ContextWithDeps(context.Background(), deps), testCSVOutputRequest())
102+
require.NoError(t, err)
103+
require.NotNil(t, result)
104+
105+
require.Len(t, result.Content, 1)
106+
text, ok := result.Content[0].(*mcp.TextContent)
107+
require.True(t, ok)
108+
assert.JSONEq(t, jsonResponse, text.Text)
109+
}
110+
95111
func TestCSVOutputVariantMovesMetadataToPreamble(t *testing.T) {
96112
csvText, err := jsonTextToCSV(`{
97113
"issues": [
@@ -118,22 +134,6 @@ func TestCSVOutputVariantMovesMetadataToPreamble(t *testing.T) {
118134
assert.NotContains(t, row, "totalCount")
119135
}
120136

121-
func TestJSONOnlyVariantPreservesOriginalJSONText(t *testing.T) {
122-
const jsonResponse = `[{"number":1,"user":{"login":"octocat"}}]`
123-
tools := withCSVOutputVariants([]inventory.ServerTool{testCSVOutputTool("list_things", jsonResponse)})
124-
inv := buildCSVOutputInventory(t, tools, false)
125-
jsonOnly := requireToolByName(t, inv.AvailableTools(context.Background()), "list_things")
126-
127-
result, err := jsonOnly.Handler(nil)(context.Background(), testCSVOutputRequest())
128-
require.NoError(t, err)
129-
require.NotNil(t, result)
130-
131-
require.Len(t, result.Content, 1)
132-
text, ok := result.Content[0].(*mcp.TextContent)
133-
require.True(t, ok)
134-
assert.JSONEq(t, jsonResponse, text.Text)
135-
}
136-
137137
func TestJSONTextToCSVFlattensPrimaryRows(t *testing.T) {
138138
csvText, err := jsonTextToCSV(`{
139139
"discussions": [
@@ -329,40 +329,38 @@ func testCSVOutputToolWithToolset(name string, response string, toolset inventor
329329
}
330330
}
331331

332-
func buildCSVOutputInventory(t *testing.T, tools []inventory.ServerTool, csvOutputEnabled bool) *inventory.Inventory {
332+
func buildCSVOutputInventory(t *testing.T, tools []inventory.ServerTool, _ bool) *inventory.Inventory {
333333
t.Helper()
334334

335335
inv, err := inventory.NewBuilder().
336336
SetTools(tools).
337-
WithFeatureChecker(func(_ context.Context, flagName string) (bool, error) {
338-
return flagName == FeatureFlagCSVOutput && csvOutputEnabled, nil
339-
}).
340337
Build()
341338
require.NoError(t, err)
342339
return inv
343340
}
344341

345-
func requireToolByName(t *testing.T, tools []inventory.ServerTool, name string) inventory.ServerTool {
346-
t.Helper()
342+
func newCSVOutputTestDeps(csvOutputEnabled bool) ToolDependencies {
343+
return csvOutputTestDeps{stubDeps: stubDeps{obsv: stubExporters()}, csvOn: csvOutputEnabled}
344+
}
347345

348-
for _, tool := range tools {
349-
if tool.Tool.Name == name {
350-
return tool
351-
}
352-
}
353-
require.Failf(t, "tool not found", "tool %q not found", name)
354-
return inventory.ServerTool{}
346+
type csvOutputTestDeps struct {
347+
stubDeps
348+
csvOn bool
355349
}
356350

357-
func requireCSVOutputVariant(t *testing.T, tools []inventory.ServerTool) inventory.ServerTool {
351+
func (d csvOutputTestDeps) IsFeatureEnabled(_ context.Context, flag string) bool {
352+
return flag == FeatureFlagCSVOutput && d.csvOn
353+
}
354+
355+
func requireToolByName(t *testing.T, tools []inventory.ServerTool, name string) inventory.ServerTool {
358356
t.Helper()
359357

360358
for _, tool := range tools {
361-
if tool.FeatureFlagEnable == FeatureFlagCSVOutput {
359+
if tool.Tool.Name == name {
362360
return tool
363361
}
364362
}
365-
require.Fail(t, "CSV output variant not found")
363+
require.Failf(t, "tool not found", "tool %q not found", name)
366364
return inventory.ServerTool{}
367365
}
368366

pkg/github/feature_flags.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,18 @@ type FeatureFlags struct {
3838
}
3939

4040
// ResolveFeatureFlags computes the effective set of enabled feature flags by:
41-
// 1. Taking explicitly enabled features validated against AllowedFeatureFlags
42-
// 2. Adding features enabled by insiders mode from InsidersFeatureFlags
41+
// 1. Taking the user-supplied flags (from --features or X-MCP-Features) and
42+
// keeping only those present in AllowedFeatureFlags. Unknown or unsafe
43+
// flags from request input are silently dropped here.
44+
// 2. If insiders mode is on, unioning in every flag from InsidersFeatureFlags.
45+
// Insiders is a server-controlled meta switch, so its expansion is NOT
46+
// re-validated against AllowedFeatureFlags.
47+
//
48+
// AllowedFeatureFlags and InsidersFeatureFlags are independent sets:
49+
// - A flag in AllowedFeatureFlags but not InsidersFeatureFlags is a regular
50+
// opt-in flag that insiders mode does not turn on automatically.
51+
// - A flag in InsidersFeatureFlags but not AllowedFeatureFlags is reachable
52+
// only through insiders mode and cannot be enabled by user input.
4353
//
4454
// Returns a set (map) for O(1) lookup by the feature checker.
4555
func ResolveFeatureFlags(enabledFeatures []string, insidersMode bool) map[string]bool {

pkg/github/feature_flags_test.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,18 @@ func TestResolveFeatureFlags(t *testing.T) {
184184
expectedFlags: []string{MCPAppsFeatureFlag},
185185
unexpectedFlags: []string{"unknown_flag"},
186186
},
187+
{
188+
name: "user-only flags can be enabled but are not turned on by insiders",
189+
enabledFeatures: []string{FeatureFlagIssuesGranular},
190+
insidersMode: false,
191+
expectedFlags: []string{FeatureFlagIssuesGranular},
192+
},
193+
{
194+
name: "insiders does not enable user-only allowed flags",
195+
enabledFeatures: nil,
196+
insidersMode: true,
197+
unexpectedFlags: []string{FeatureFlagIssuesGranular, FeatureFlagPullRequestsGranular},
198+
},
187199
{
188200
name: "explicit plus insiders deduplicates",
189201
enabledFeatures: []string{MCPAppsFeatureFlag},

pkg/github/tools.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ var (
167167
// AllTools returns all tools with their embedded toolset metadata.
168168
// Tool functions return ServerTool directly with toolset info.
169169
func AllTools(t translations.TranslationHelperFunc) []inventory.ServerTool {
170-
return withCSVOutputVariants([]inventory.ServerTool{
170+
return withCSVOutput([]inventory.ServerTool{
171171
// Context tools
172172
GetMe(t),
173173
GetTeams(t),

0 commit comments

Comments
 (0)