Skip to content

Commit 4a53df5

Browse files
committed
hold-bac feature flag
1 parent 6b4f7db commit 4a53df5

File tree

6 files changed

+88
-4
lines changed

6 files changed

+88
-4
lines changed

pkg/github/projects.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,14 @@ const (
2929
// in favor of the consolidated project tools.
3030
const FeatureFlagConsolidatedProjects = "remote_mcp_consolidated_projects"
3131

32+
// FeatureFlagHoldBackLegacyProjects allows users to keep the old individual project tools
33+
// even after FeatureFlagConsolidatedProjects is enabled. This provides a transition period
34+
// for users who need more time to migrate to the consolidated tools.
35+
//
36+
// Deprecated: This flag will be removed in a future release. Users should migrate to
37+
// the consolidated project tools (projects_list, projects_get, projects_write).
38+
const FeatureFlagHoldBackLegacyProjects = "remote_mcp_holdback_legacy_projects"
39+
3240
// Method constants for consolidated project tools
3341
const (
3442
projectsMethodListProjects = "list_projects"
@@ -158,6 +166,7 @@ func ListProjects(t translations.TranslationHelperFunc) inventory.ServerTool {
158166
},
159167
)
160168
tool.FeatureFlagDisable = FeatureFlagConsolidatedProjects
169+
tool.FeatureFlagHoldBack = FeatureFlagHoldBackLegacyProjects
161170
return tool
162171
}
163172

@@ -248,6 +257,7 @@ func GetProject(t translations.TranslationHelperFunc) inventory.ServerTool {
248257
},
249258
)
250259
tool.FeatureFlagDisable = FeatureFlagConsolidatedProjects
260+
tool.FeatureFlagHoldBack = FeatureFlagHoldBackLegacyProjects
251261
return tool
252262
}
253263

@@ -356,6 +366,7 @@ func ListProjectFields(t translations.TranslationHelperFunc) inventory.ServerToo
356366
},
357367
)
358368
tool.FeatureFlagDisable = FeatureFlagConsolidatedProjects
369+
tool.FeatureFlagHoldBack = FeatureFlagHoldBackLegacyProjects
359370
return tool
360371
}
361372

@@ -450,6 +461,7 @@ func GetProjectField(t translations.TranslationHelperFunc) inventory.ServerTool
450461
},
451462
)
452463
tool.FeatureFlagDisable = FeatureFlagConsolidatedProjects
464+
tool.FeatureFlagHoldBack = FeatureFlagHoldBackLegacyProjects
453465
return tool
454466
}
455467

@@ -588,6 +600,7 @@ func ListProjectItems(t translations.TranslationHelperFunc) inventory.ServerTool
588600
},
589601
)
590602
tool.FeatureFlagDisable = FeatureFlagConsolidatedProjects
603+
tool.FeatureFlagHoldBack = FeatureFlagHoldBackLegacyProjects
591604
return tool
592605
}
593606

@@ -696,6 +709,7 @@ func GetProjectItem(t translations.TranslationHelperFunc) inventory.ServerTool {
696709
},
697710
)
698711
tool.FeatureFlagDisable = FeatureFlagConsolidatedProjects
712+
tool.FeatureFlagHoldBack = FeatureFlagHoldBackLegacyProjects
699713
return tool
700714
}
701715

@@ -809,6 +823,7 @@ func AddProjectItem(t translations.TranslationHelperFunc) inventory.ServerTool {
809823
},
810824
)
811825
tool.FeatureFlagDisable = FeatureFlagConsolidatedProjects
826+
tool.FeatureFlagHoldBack = FeatureFlagHoldBackLegacyProjects
812827
return tool
813828
}
814829

@@ -923,6 +938,7 @@ func UpdateProjectItem(t translations.TranslationHelperFunc) inventory.ServerToo
923938
},
924939
)
925940
tool.FeatureFlagDisable = FeatureFlagConsolidatedProjects
941+
tool.FeatureFlagHoldBack = FeatureFlagHoldBackLegacyProjects
926942
return tool
927943
}
928944

@@ -1011,6 +1027,7 @@ func DeleteProjectItem(t translations.TranslationHelperFunc) inventory.ServerToo
10111027
},
10121028
)
10131029
tool.FeatureFlagDisable = FeatureFlagConsolidatedProjects
1030+
tool.FeatureFlagHoldBack = FeatureFlagHoldBackLegacyProjects
10141031
return tool
10151032
}
10161033

pkg/inventory/filters.go

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,18 @@ func (r *Inventory) checkFeatureFlag(ctx context.Context, flagName string) bool
3838
// isFeatureFlagAllowed checks if an item passes feature flag filtering.
3939
// - If FeatureFlagEnable is set, the item is only allowed if the flag is enabled
4040
// - If FeatureFlagDisable is set, the item is excluded if the flag is enabled
41-
func (r *Inventory) isFeatureFlagAllowed(ctx context.Context, enableFlag, disableFlag string) bool {
41+
// - If FeatureFlagHoldBack is set and enabled, it overrides FeatureFlagDisable (keeps tool available)
42+
func (r *Inventory) isFeatureFlagAllowed(ctx context.Context, enableFlag, disableFlag, holdBackFlag string) bool {
4243
// Check enable flag - item requires this flag to be on
4344
if enableFlag != "" && !r.checkFeatureFlag(ctx, enableFlag) {
4445
return false
4546
}
4647
// Check disable flag - item is excluded if this flag is on
4748
if disableFlag != "" && r.checkFeatureFlag(ctx, disableFlag) {
49+
// Check if hold-back flag overrides the disable
50+
if holdBackFlag != "" && r.checkFeatureFlag(ctx, holdBackFlag) {
51+
return true // Hold-back keeps tool enabled during transition
52+
}
4853
return false
4954
}
5055
return true
@@ -70,7 +75,7 @@ func (r *Inventory) isToolEnabled(ctx context.Context, tool *ServerTool) bool {
7075
}
7176
}
7277
// 2. Check feature flags
73-
if !r.isFeatureFlagAllowed(ctx, tool.FeatureFlagEnable, tool.FeatureFlagDisable) {
78+
if !r.isFeatureFlagAllowed(ctx, tool.FeatureFlagEnable, tool.FeatureFlagDisable, tool.FeatureFlagHoldBack) {
7479
return false
7580
}
7681
// 3. Check read-only filter (applies to all tools)
@@ -130,7 +135,7 @@ func (r *Inventory) AvailableResourceTemplates(ctx context.Context) []ServerReso
130135
for i := range r.resourceTemplates {
131136
res := &r.resourceTemplates[i]
132137
// Check feature flags
133-
if !r.isFeatureFlagAllowed(ctx, res.FeatureFlagEnable, res.FeatureFlagDisable) {
138+
if !r.isFeatureFlagAllowed(ctx, res.FeatureFlagEnable, res.FeatureFlagDisable, res.FeatureFlagHoldBack) {
134139
continue
135140
}
136141
if r.isToolsetEnabled(res.Toolset.ID) {
@@ -157,7 +162,7 @@ func (r *Inventory) AvailablePrompts(ctx context.Context) []ServerPrompt {
157162
for i := range r.prompts {
158163
prompt := &r.prompts[i]
159164
// Check feature flags
160-
if !r.isFeatureFlagAllowed(ctx, prompt.FeatureFlagEnable, prompt.FeatureFlagDisable) {
165+
if !r.isFeatureFlagAllowed(ctx, prompt.FeatureFlagEnable, prompt.FeatureFlagDisable, prompt.FeatureFlagHoldBack) {
161166
continue
162167
}
163168
if r.isToolsetEnabled(prompt.Toolset.ID) {

pkg/inventory/prompts.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ type ServerPrompt struct {
1414
// FeatureFlagDisable specifies a feature flag that, when enabled, causes this prompt
1515
// to be omitted. Used to disable prompts when a feature flag is on.
1616
FeatureFlagDisable string
17+
// FeatureFlagHoldBack specifies a feature flag that, when enabled, overrides
18+
// FeatureFlagDisable and keeps the prompt available during a transition period.
19+
FeatureFlagHoldBack string
1720
}
1821

1922
// NewServerPrompt creates a new ServerPrompt with toolset metadata.

pkg/inventory/registry_test.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1077,6 +1077,56 @@ func TestFeatureFlagBoth(t *testing.T) {
10771077
}
10781078
}
10791079

1080+
func TestFeatureFlagHoldBack(t *testing.T) {
1081+
// Tool with disable flag and hold-back flag (simulates legacy tool during consolidation)
1082+
legacyTool := mockToolWithFlags("legacy_tool", "toolset1", true, "", "consolidation_flag")
1083+
legacyTool.FeatureFlagHoldBack = "holdback_flag"
1084+
1085+
tools := []ServerTool{
1086+
mockTool("always_available", "toolset1", true),
1087+
legacyTool,
1088+
}
1089+
1090+
// Consolidation OFF, hold-back OFF -> legacy tool available (normal operation)
1091+
checkerAllOff := func(_ context.Context, _ string) (bool, error) { return false, nil }
1092+
regAllOff := NewBuilder().SetTools(tools).WithToolsets([]string{"all"}).WithFeatureChecker(checkerAllOff).Build()
1093+
availableAllOff := regAllOff.AvailableTools(context.Background())
1094+
if len(availableAllOff) != 2 {
1095+
t.Fatalf("Expected 2 tools when both flags off, got %d", len(availableAllOff))
1096+
}
1097+
1098+
// Consolidation ON, hold-back OFF -> legacy tool excluded (migrated to new tools)
1099+
checkerConsolidationOnly := func(_ context.Context, flag string) (bool, error) {
1100+
return flag == "consolidation_flag", nil
1101+
}
1102+
regConsolidationOnly := NewBuilder().SetTools(tools).WithToolsets([]string{"all"}).WithFeatureChecker(checkerConsolidationOnly).Build()
1103+
availableConsolidationOnly := regConsolidationOnly.AvailableTools(context.Background())
1104+
if len(availableConsolidationOnly) != 1 {
1105+
t.Fatalf("Expected 1 tool when consolidation on but holdback off, got %d", len(availableConsolidationOnly))
1106+
}
1107+
if availableConsolidationOnly[0].Tool.Name != "always_available" {
1108+
t.Errorf("Expected always_available, got %s", availableConsolidationOnly[0].Tool.Name)
1109+
}
1110+
1111+
// Consolidation ON, hold-back ON -> legacy tool available (user opted to hold back)
1112+
checkerBothOn := func(_ context.Context, _ string) (bool, error) { return true, nil }
1113+
regBothOn := NewBuilder().SetTools(tools).WithToolsets([]string{"all"}).WithFeatureChecker(checkerBothOn).Build()
1114+
availableBothOn := regBothOn.AvailableTools(context.Background())
1115+
if len(availableBothOn) != 2 {
1116+
t.Fatalf("Expected 2 tools when both consolidation and holdback on, got %d", len(availableBothOn))
1117+
}
1118+
1119+
// Consolidation OFF, hold-back ON -> legacy tool available (hold-back has no effect when consolidation off)
1120+
checkerHoldbackOnly := func(_ context.Context, flag string) (bool, error) {
1121+
return flag == "holdback_flag", nil
1122+
}
1123+
regHoldbackOnly := NewBuilder().SetTools(tools).WithToolsets([]string{"all"}).WithFeatureChecker(checkerHoldbackOnly).Build()
1124+
availableHoldbackOnly := regHoldbackOnly.AvailableTools(context.Background())
1125+
if len(availableHoldbackOnly) != 2 {
1126+
t.Fatalf("Expected 2 tools when only holdback on, got %d", len(availableHoldbackOnly))
1127+
}
1128+
}
1129+
10801130
func TestFeatureFlagError(t *testing.T) {
10811131
tools := []ServerTool{
10821132
mockToolWithFlags("needs_flag", "toolset1", true, "my_feature", ""),

pkg/inventory/resources.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ type ServerResourceTemplate struct {
2222
// FeatureFlagDisable specifies a feature flag that, when enabled, causes this resource
2323
// to be omitted. Used to disable resources when a feature flag is on.
2424
FeatureFlagDisable string
25+
// FeatureFlagHoldBack specifies a feature flag that, when enabled, overrides
26+
// FeatureFlagDisable and keeps the resource available during a transition period.
27+
FeatureFlagHoldBack string
2528
}
2629

2730
// HasHandler returns true if this resource has a handler function.

pkg/inventory/server_tool.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,12 @@ type ServerTool struct {
6464
// to be omitted. Used to disable tools when a feature flag is on.
6565
FeatureFlagDisable string
6666

67+
// FeatureFlagHoldBack specifies a feature flag that, when enabled, overrides
68+
// FeatureFlagDisable and keeps the tool available. This allows users to "hold back"
69+
// on a deprecation by opting to keep the old tools during a transition period.
70+
// Used during tool consolidation to give users time to migrate.
71+
FeatureFlagHoldBack string
72+
6773
// Enabled is an optional function called at build/filter time to determine
6874
// if this tool should be available. If nil, the tool is considered enabled
6975
// (subject to FeatureFlagEnable/FeatureFlagDisable checks).

0 commit comments

Comments
 (0)