@@ -13,6 +13,24 @@ import (
1313// Returns (enabled, error). If error occurs, the caller should log and treat as false.
1414type FeatureFlagChecker func (ctx context.Context , flagName string ) (bool , error )
1515
16+ // HiddenToolReason is a stable machine-readable reason for tool filtering decisions.
17+ type HiddenToolReason string
18+
19+ const (
20+ HiddenToolReasonEnabledFalse HiddenToolReason = "enabled_false"
21+ HiddenToolReasonFeatureFlag HiddenToolReason = "feature_flag_blocked"
22+ HiddenToolReasonReadOnlyMode HiddenToolReason = "read_only_mode"
23+ HiddenToolReasonBuilderFilterFalse HiddenToolReason = "builder_filter_false"
24+ HiddenToolReasonBuilderFilterError HiddenToolReason = "builder_filter_error"
25+ HiddenToolReasonToolsetDisabled HiddenToolReason = "toolset_disabled"
26+ )
27+
28+ // HiddenTool describes a hidden tool name and its first matching hidden reason.
29+ type HiddenTool struct {
30+ Name string
31+ Reason HiddenToolReason
32+ }
33+
1634// isToolsetEnabled checks if a toolset is enabled based on current filters.
1735func (r * Inventory ) isToolsetEnabled (toolsetID ToolsetID ) bool {
1836 // Check enabled toolsets filter
@@ -51,53 +69,59 @@ func (r *Inventory) isFeatureFlagAllowed(ctx context.Context, enableFlag, disabl
5169 return true
5270}
5371
54- // isToolEnabled checks if a specific tool is enabled based on current filters.
55- // Filter evaluation order:
56- // 1. Tool.Enabled (tool self-filtering)
57- // 2. FeatureFlagEnable/FeatureFlagDisable
58- // 3. Read-only filter
59- // 4. Builder filters (via WithFilter)
60- // 5. Toolset/additional tools
61- func (r * Inventory ) isToolEnabled (ctx context.Context , tool * ServerTool ) bool {
72+ // toolEnabledReason evaluates the tool filter chain in order and returns hidden reason if excluded.
73+ func (r * Inventory ) toolEnabledReason (ctx context.Context , tool * ServerTool ) (bool , HiddenToolReason ) {
6274 // 1. Check tool's own Enabled function first
6375 if tool .Enabled != nil {
6476 enabled , err := tool .Enabled (ctx )
6577 if err != nil {
6678 fmt .Fprintf (os .Stderr , "Tool.Enabled check error for %q: %v\n " , tool .Tool .Name , err )
67- return false
79+ return false , HiddenToolReasonEnabledFalse
6880 }
6981 if ! enabled {
70- return false
82+ return false , HiddenToolReasonEnabledFalse
7183 }
7284 }
7385 // 2. Check feature flags
7486 if ! r .isFeatureFlagAllowed (ctx , tool .FeatureFlagEnable , tool .FeatureFlagDisable ) {
75- return false
87+ return false , HiddenToolReasonFeatureFlag
7688 }
7789 // 3. Check read-only filter (applies to all tools)
7890 if r .readOnly && ! tool .IsReadOnly () {
79- return false
91+ return false , HiddenToolReasonReadOnlyMode
8092 }
8193 // 4. Apply builder filters
8294 for _ , filter := range r .filters {
8395 allowed , err := filter (ctx , tool )
8496 if err != nil {
8597 fmt .Fprintf (os .Stderr , "Builder filter error for tool %q: %v\n " , tool .Tool .Name , err )
86- return false
98+ return false , HiddenToolReasonBuilderFilterError
8799 }
88100 if ! allowed {
89- return false
101+ return false , HiddenToolReasonBuilderFilterFalse
90102 }
91103 }
92104 // 5. Check if tool is in additionalTools (bypasses toolset filter)
93105 if r .additionalTools != nil && r .additionalTools [tool .Tool .Name ] {
94- return true
106+ return true , ""
95107 }
96- // 5 . Check toolset filter
108+ // 6 . Check toolset filter
97109 if ! r .isToolsetEnabled (tool .Toolset .ID ) {
98- return false
110+ return false , HiddenToolReasonToolsetDisabled
99111 }
100- return true
112+ return true , ""
113+ }
114+
115+ // isToolEnabled checks if a specific tool is enabled based on current filters.
116+ // Filter evaluation order:
117+ // 1. Tool.Enabled (tool self-filtering)
118+ // 2. FeatureFlagEnable/FeatureFlagDisable
119+ // 3. Read-only filter
120+ // 4. Builder filters (via WithFilter)
121+ // 5. Toolset/additional tools
122+ func (r * Inventory ) isToolEnabled (ctx context.Context , tool * ServerTool ) bool {
123+ enabled , _ := r .toolEnabledReason (ctx , tool )
124+ return enabled
101125}
102126
103127// AvailableTools returns the tools that pass all current filters,
@@ -284,3 +308,32 @@ func (r *Inventory) EnabledToolsetIDs() []ToolsetID {
284308func (r * Inventory ) FilteredTools (ctx context.Context ) ([]ServerTool , error ) {
285309 return r .AvailableTools (ctx ), nil
286310}
311+
312+ // HiddenTools returns hidden tools and stable reason codes for why they were filtered out.
313+ // The first matching reason in the filter evaluation order is returned per tool name.
314+ func (r * Inventory ) HiddenTools (ctx context.Context ) []HiddenTool {
315+ seen := make (map [string ]bool )
316+ result := make ([]HiddenTool , 0 )
317+ for i := range r .tools {
318+ tool := & r .tools [i ]
319+ enabled , reason := r .toolEnabledReason (ctx , tool )
320+ if enabled {
321+ continue
322+ }
323+ if seen [tool .Tool .Name ] {
324+ continue
325+ }
326+ seen [tool .Tool .Name ] = true
327+ result = append (result , HiddenTool {
328+ Name : tool .Tool .Name ,
329+ Reason : reason ,
330+ })
331+ }
332+ sort .Slice (result , func (i , j int ) bool {
333+ if result [i ].Name != result [j ].Name {
334+ return result [i ].Name < result [j ].Name
335+ }
336+ return result [i ].Reason < result [j ].Reason
337+ })
338+ return result
339+ }
0 commit comments