@@ -35,37 +35,57 @@ func (r *Inventory) checkFeatureFlag(ctx context.Context, flagName string) bool
3535 return enabled
3636}
3737
38- // isFeatureFlagAllowed checks if an item passes feature flag filtering.
39- // - If no feature checker is configured, feature flags are not evaluated and
40- // the item passes through. This gives the unfiltered upper bound used by
41- // HTTP mode at startup, before per-request feature checkers exist. The
42- // per-request inventory always installs a checker, so any dual-name
43- // variants are resolved before MCP registration (which can only serve a
44- // given tool name once).
38+ // featureFlagAllowed reports whether an item with the given enable/disable
39+ // flag pair is permitted under the supplied checker. The checker must be
40+ // non-nil — callers that don't want feature filtering should not call this at
41+ // all (this is also the contract for createFeatureFlagFilter, which is only
42+ // installed when WithFeatureChecker received a non-nil checker).
43+ //
4544// - If FeatureFlagEnable is set, the item is only allowed if the flag is enabled.
4645// - If FeatureFlagDisable is set, the item is excluded if the flag is enabled.
47- func (r * Inventory ) isFeatureFlagAllowed (ctx context.Context , enableFlag , disableFlag string ) bool {
48- if r .featureChecker == nil {
49- return true
50- }
51- // Check enable flag - item requires this flag to be on
52- if enableFlag != "" && ! r .checkFeatureFlag (ctx , enableFlag ) {
53- return false
46+ func featureFlagAllowed (ctx context.Context , checker FeatureFlagChecker , enableFlag , disableFlag string ) bool {
47+ if enableFlag != "" {
48+ enabled , err := checker (ctx , enableFlag )
49+ if err != nil {
50+ fmt .Fprintf (os .Stderr , "Feature flag check error for %q: %v\n " , enableFlag , err )
51+ return false
52+ }
53+ if ! enabled {
54+ return false
55+ }
5456 }
55- // Check disable flag - item is excluded if this flag is on
56- if disableFlag != "" && r .checkFeatureFlag (ctx , disableFlag ) {
57- return false
57+ if disableFlag != "" {
58+ enabled , err := checker (ctx , disableFlag )
59+ if err != nil {
60+ fmt .Fprintf (os .Stderr , "Feature flag check error for %q: %v\n " , disableFlag , err )
61+ return false
62+ }
63+ if enabled {
64+ return false
65+ }
5866 }
5967 return true
6068}
6169
70+ // createFeatureFlagFilter returns a ToolFilter that gates tools on their
71+ // FeatureFlagEnable / FeatureFlagDisable annotations using the given checker.
72+ // Builder.Build() installs this filter exactly once when WithFeatureChecker
73+ // has been called with a non-nil checker, so "no feature filtering" is
74+ // expressed structurally — by the absence of the filter — rather than by a
75+ // runtime nil check inside the filter itself.
76+ func createFeatureFlagFilter (checker FeatureFlagChecker ) ToolFilter {
77+ return func (ctx context.Context , tool * ServerTool ) (bool , error ) {
78+ return featureFlagAllowed (ctx , checker , tool .FeatureFlagEnable , tool .FeatureFlagDisable ), nil
79+ }
80+ }
81+
6282// isToolEnabled checks if a specific tool is enabled based on current filters.
6383// Filter evaluation order:
6484// 1. Tool.Enabled (tool self-filtering)
65- // 2. FeatureFlagEnable/FeatureFlagDisable
66- // 3. Read-only filter
67- // 4. Builder filters ( via WithFilter )
68- // 5 . Toolset/additional tools
85+ // 2. Read-only filter
86+ // 3. Builder filters (via WithFilter; the feature-flag filter, when
87+ // installed via WithFeatureChecker, runs as part of this step )
88+ // 4 . Toolset/additional tools
6989func (r * Inventory ) isToolEnabled (ctx context.Context , tool * ServerTool ) bool {
7090 // 1. Check tool's own Enabled function first
7191 if tool .Enabled != nil {
@@ -78,15 +98,11 @@ func (r *Inventory) isToolEnabled(ctx context.Context, tool *ServerTool) bool {
7898 return false
7999 }
80100 }
81- // 2. Check feature flags
82- if ! r .isFeatureFlagAllowed (ctx , tool .FeatureFlagEnable , tool .FeatureFlagDisable ) {
83- return false
84- }
85- // 3. Check read-only filter (applies to all tools)
101+ // 2. Check read-only filter (applies to all tools)
86102 if r .readOnly && ! tool .IsReadOnly () {
87103 return false
88104 }
89- // 4 . Apply builder filters
105+ // 3 . Apply builder filters (includes the feature-flag filter when set)
90106 for _ , filter := range r .filters {
91107 allowed , err := filter (ctx , tool )
92108 if err != nil {
@@ -97,11 +113,11 @@ func (r *Inventory) isToolEnabled(ctx context.Context, tool *ServerTool) bool {
97113 return false
98114 }
99115 }
100- // 5 . Check if tool is in additionalTools (bypasses toolset filter)
116+ // 4 . Check if tool is in additionalTools (bypasses toolset filter)
101117 if r .additionalTools != nil && r .additionalTools [tool .Tool .Name ] {
102118 return true
103119 }
104- // 5 . Check toolset filter
120+ // 4 . Check toolset filter
105121 if ! r .isToolsetEnabled (tool .Toolset .ID ) {
106122 return false
107123 }
@@ -160,8 +176,11 @@ func (r *Inventory) AvailableResourceTemplates(ctx context.Context) []ServerReso
160176 var result []ServerResourceTemplate
161177 for i := range r .resourceTemplates {
162178 res := & r .resourceTemplates [i ]
163- // Check feature flags
164- if ! r .isFeatureFlagAllowed (ctx , res .FeatureFlagEnable , res .FeatureFlagDisable ) {
179+ // Resources have no filter pipeline, so feature gating runs inline.
180+ // The featureChecker != nil guard mirrors the structural "no checker
181+ // = no filtering" contract used for tools (where the absence of a
182+ // pipeline step expresses the same thing).
183+ if r .featureChecker != nil && ! featureFlagAllowed (ctx , r .featureChecker , res .FeatureFlagEnable , res .FeatureFlagDisable ) {
165184 continue
166185 }
167186 if r .isToolsetEnabled (res .Toolset .ID ) {
@@ -189,8 +208,9 @@ func (r *Inventory) AvailablePrompts(ctx context.Context) []ServerPrompt {
189208 var result []ServerPrompt
190209 for i := range r .prompts {
191210 prompt := & r .prompts [i ]
192- // Check feature flags
193- if ! r .isFeatureFlagAllowed (ctx , prompt .FeatureFlagEnable , prompt .FeatureFlagDisable ) {
211+ // Prompts have no filter pipeline; see AvailableResourceTemplates for
212+ // the rationale behind the explicit nil guard.
213+ if r .featureChecker != nil && ! featureFlagAllowed (ctx , r .featureChecker , prompt .FeatureFlagEnable , prompt .FeatureFlagDisable ) {
194214 continue
195215 }
196216 if r .isToolsetEnabled (prompt .Toolset .ID ) {
0 commit comments