Skip to content
Merged
14 changes: 12 additions & 2 deletions pkg/github/feature_flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,18 @@ type FeatureFlags struct {
}

// ResolveFeatureFlags computes the effective set of enabled feature flags by:
// 1. Taking explicitly enabled features validated against AllowedFeatureFlags
// 2. Adding features enabled by insiders mode from InsidersFeatureFlags
// 1. Taking the user-supplied flags (from --features or X-MCP-Features) and
// keeping only those present in AllowedFeatureFlags. Unknown or unsafe
// flags from request input are silently dropped here.
// 2. If insiders mode is on, unioning in every flag from InsidersFeatureFlags.
// Insiders is a server-controlled meta switch, so its expansion is NOT
// re-validated against AllowedFeatureFlags.
//
// AllowedFeatureFlags and InsidersFeatureFlags are independent sets:
// - A flag in AllowedFeatureFlags but not InsidersFeatureFlags is a regular
// opt-in flag that insiders mode does not turn on automatically.
// - A flag in InsidersFeatureFlags but not AllowedFeatureFlags is reachable
// only through insiders mode and cannot be enabled by user input.
//
// Returns a set (map) for O(1) lookup by the feature checker.
func ResolveFeatureFlags(enabledFeatures []string, insidersMode bool) map[string]bool {
Expand Down
12 changes: 12 additions & 0 deletions pkg/github/feature_flags_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,18 @@ func TestResolveFeatureFlags(t *testing.T) {
expectedFlags: []string{MCPAppsFeatureFlag},
unexpectedFlags: []string{"unknown_flag"},
},
{
name: "user-only flags can be enabled but are not turned on by insiders",
enabledFeatures: []string{FeatureFlagIssuesGranular},
insidersMode: false,
expectedFlags: []string{FeatureFlagIssuesGranular},
},
{
name: "insiders does not enable user-only allowed flags",
enabledFeatures: nil,
insidersMode: true,
unexpectedFlags: []string{FeatureFlagIssuesGranular, FeatureFlagPullRequestsGranular},
},
{
name: "explicit plus insiders deduplicates",
enabledFeatures: []string{MCPAppsFeatureFlag},
Expand Down
57 changes: 37 additions & 20 deletions pkg/inventory/filters.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,15 +103,26 @@ func (r *Inventory) isToolEnabledWithFeatureFlags(ctx context.Context, tool *Ser
return true
}

func sortTools(tools []ServerTool) {
sort.Slice(tools, func(i, j int) bool {
if tools[i].Toolset.ID != tools[j].Toolset.ID {
return tools[i].Toolset.ID < tools[j].Toolset.ID
// sortByToolsetThenName sorts items deterministically by their toolset ID,
// breaking ties by name. The two extractor closures keep this generic helper
// independent of the concrete inventory item shape (tools, resource templates,
// prompts).
func sortByToolsetThenName[T any](items []T, toolsetID func(T) ToolsetID, name func(T) string) {
sort.Slice(items, func(i, j int) bool {
if toolsetID(items[i]) != toolsetID(items[j]) {
return toolsetID(items[i]) < toolsetID(items[j])
}
return tools[i].Tool.Name < tools[j].Tool.Name
return name(items[i]) < name(items[j])
})
Comment on lines +129 to 136
}

func sortTools(tools []ServerTool) {
sortByToolsetThenName(tools,
func(t ServerTool) ToolsetID { return t.Toolset.ID },
func(t ServerTool) string { return t.Tool.Name },
)
}

// AvailableTools returns the tools that pass all current filters,
// sorted deterministically by toolset ID, then tool name.
// The context is used for feature flag evaluation.
Expand All @@ -132,6 +143,12 @@ func (r *Inventory) AvailableTools(ctx context.Context) []ServerTool {

// AvailableToolsWithoutFeatureFiltering returns tools that pass every filter
// except FeatureFlagEnable/FeatureFlagDisable.
//
// HTTP mode uses this to build the static (process-wide) inventory, which acts
// as an upper bound on what any request may see. Per-request feature flags
// (from headers like X-MCP-Features or X-MCP-Insiders) are evaluated later
// when the per-request inventory is derived, so feature-flagged variants must
// be preserved here.
func (r *Inventory) AvailableToolsWithoutFeatureFiltering(ctx context.Context) []ServerTool {
var result []ServerTool
for i := range r.tools {
Expand All @@ -147,12 +164,10 @@ func (r *Inventory) AvailableToolsWithoutFeatureFiltering(ctx context.Context) [
}

func sortResourceTemplates(resourceTemplates []ServerResourceTemplate) {
sort.Slice(resourceTemplates, func(i, j int) bool {
if resourceTemplates[i].Toolset.ID != resourceTemplates[j].Toolset.ID {
return resourceTemplates[i].Toolset.ID < resourceTemplates[j].Toolset.ID
}
return resourceTemplates[i].Template.Name < resourceTemplates[j].Template.Name
})
sortByToolsetThenName(resourceTemplates,
func(r ServerResourceTemplate) ToolsetID { return r.Toolset.ID },
func(r ServerResourceTemplate) string { return r.Template.Name },
)
}

// AvailableResourceTemplates returns resource templates that pass all current filters,
Expand All @@ -178,7 +193,9 @@ func (r *Inventory) AvailableResourceTemplates(ctx context.Context) []ServerReso
}

// AvailableResourceTemplatesWithoutFeatureFiltering returns resource templates
// that pass every filter except FeatureFlagEnable/FeatureFlagDisable.
// that pass every filter except FeatureFlagEnable/FeatureFlagDisable. See
// AvailableToolsWithoutFeatureFiltering for why feature flags are deferred in
// HTTP mode.
func (r *Inventory) AvailableResourceTemplatesWithoutFeatureFiltering(_ context.Context) []ServerResourceTemplate {
var result []ServerResourceTemplate
for i := range r.resourceTemplates {
Expand All @@ -194,12 +211,10 @@ func (r *Inventory) AvailableResourceTemplatesWithoutFeatureFiltering(_ context.
}

func sortPrompts(prompts []ServerPrompt) {
sort.Slice(prompts, func(i, j int) bool {
if prompts[i].Toolset.ID != prompts[j].Toolset.ID {
return prompts[i].Toolset.ID < prompts[j].Toolset.ID
}
return prompts[i].Prompt.Name < prompts[j].Prompt.Name
})
sortByToolsetThenName(prompts,
func(p ServerPrompt) ToolsetID { return p.Toolset.ID },
func(p ServerPrompt) string { return p.Prompt.Name },
)
}

// AvailablePrompts returns prompts that pass all current filters,
Expand All @@ -224,8 +239,10 @@ func (r *Inventory) AvailablePrompts(ctx context.Context) []ServerPrompt {
return result
}

// AvailablePromptsWithoutFeatureFiltering returns prompts that pass every filter
// except FeatureFlagEnable/FeatureFlagDisable.
// AvailablePromptsWithoutFeatureFiltering returns prompts that pass every
// filter except FeatureFlagEnable/FeatureFlagDisable. See
// AvailableToolsWithoutFeatureFiltering for why feature flags are deferred in
// HTTP mode.
func (r *Inventory) AvailablePromptsWithoutFeatureFiltering(_ context.Context) []ServerPrompt {
var result []ServerPrompt
for i := range r.prompts {
Expand Down
Loading