@@ -32,6 +32,13 @@ func mockTool(name, toolsetID string, readOnly bool) inventory.ServerTool {
3232 }
3333}
3434
35+ func mockToolWithFeatureFlag (name , toolsetID string , readOnly bool , enableFlag , disableFlag string ) inventory.ServerTool {
36+ tool := mockTool (name , toolsetID , readOnly )
37+ tool .FeatureFlagEnable = enableFlag
38+ tool .FeatureFlagDisable = disableFlag
39+ return tool
40+ }
41+
3542func TestInventoryFiltersForRequest (t * testing.T ) {
3643 tools := []inventory.ServerTool {
3744 mockTool ("get_file_contents" , "repos" , true ),
@@ -115,12 +122,15 @@ func testTools() []inventory.ServerTool {
115122 mockTool ("create_issue" , "issues" , false ),
116123 mockTool ("list_pull_requests" , "pull_requests" , true ),
117124 mockTool ("create_pull_request" , "pull_requests" , false ),
125+ // Feature-flagged tools for testing X-MCP-Features header
126+ mockToolWithFeatureFlag ("needs_holdback" , "repos" , true , "mcp_holdback_consolidated_projects" , "" ),
127+ mockToolWithFeatureFlag ("hidden_by_holdback" , "repos" , true , "" , "mcp_holdback_consolidated_projects" ),
118128 }
119129}
120130
121131// extractToolNames extracts tool names from an inventory
122- func extractToolNames (inv * inventory.Inventory ) []string {
123- available := inv .AvailableTools (context . Background () )
132+ func extractToolNames (inv * inventory.Inventory , ctx context. Context ) []string {
133+ available := inv .AvailableTools (ctx )
124134 names := make ([]string , len (available ))
125135 for i , tool := range available {
126136 names [i ] = tool .Tool .Name
@@ -141,17 +151,17 @@ func TestHTTPHandlerRoutes(t *testing.T) {
141151 {
142152 name : "root path returns all tools" ,
143153 path : "/" ,
144- expectedTools : []string {"get_file_contents" , "create_repository" , "list_issues" , "create_issue" , "list_pull_requests" , "create_pull_request" },
154+ expectedTools : []string {"get_file_contents" , "create_repository" , "list_issues" , "create_issue" , "list_pull_requests" , "create_pull_request" , "hidden_by_holdback" },
145155 },
146156 {
147157 name : "readonly path filters write tools" ,
148158 path : "/readonly" ,
149- expectedTools : []string {"get_file_contents" , "list_issues" , "list_pull_requests" },
159+ expectedTools : []string {"get_file_contents" , "list_issues" , "list_pull_requests" , "hidden_by_holdback" },
150160 },
151161 {
152162 name : "toolset path filters to toolset" ,
153163 path : "/x/repos" ,
154- expectedTools : []string {"get_file_contents" , "create_repository" },
164+ expectedTools : []string {"get_file_contents" , "create_repository" , "hidden_by_holdback" },
155165 },
156166 {
157167 name : "toolset path with issues" ,
@@ -161,7 +171,7 @@ func TestHTTPHandlerRoutes(t *testing.T) {
161171 {
162172 name : "toolset readonly path filters to readonly tools in toolset" ,
163173 path : "/x/repos/readonly" ,
164- expectedTools : []string {"get_file_contents" },
174+ expectedTools : []string {"get_file_contents" , "hidden_by_holdback" },
165175 },
166176 {
167177 name : "toolset readonly path with issues" ,
@@ -198,15 +208,15 @@ func TestHTTPHandlerRoutes(t *testing.T) {
198208 headers : map [string ]string {
199209 headers .MCPReadOnlyHeader : "true" ,
200210 },
201- expectedTools : []string {"get_file_contents" , "list_issues" , "list_pull_requests" },
211+ expectedTools : []string {"get_file_contents" , "list_issues" , "list_pull_requests" , "hidden_by_holdback" },
202212 },
203213 {
204214 name : "X-MCP-Toolsets header filters to toolset" ,
205215 path : "/" ,
206216 headers : map [string ]string {
207217 headers .MCPToolsetsHeader : "repos" ,
208218 },
209- expectedTools : []string {"get_file_contents" , "create_repository" },
219+ expectedTools : []string {"get_file_contents" , "create_repository" , "hidden_by_holdback" },
210220 },
211221 {
212222 name : "URL toolset takes precedence over header toolset" ,
@@ -222,19 +232,41 @@ func TestHTTPHandlerRoutes(t *testing.T) {
222232 headers : map [string ]string {
223233 headers .MCPReadOnlyHeader : "false" ,
224234 },
225- expectedTools : []string {"get_file_contents" , "list_issues" , "list_pull_requests" },
235+ expectedTools : []string {"get_file_contents" , "list_issues" , "list_pull_requests" , "hidden_by_holdback" },
236+ },
237+ {
238+ name : "X-MCP-Features header enables flagged tool" ,
239+ path : "/" ,
240+ headers : map [string ]string {
241+ headers .MCPFeaturesHeader : "mcp_holdback_consolidated_projects" ,
242+ },
243+ expectedTools : []string {"get_file_contents" , "create_repository" , "list_issues" , "create_issue" , "list_pull_requests" , "create_pull_request" , "needs_holdback" },
244+ },
245+ {
246+ name : "X-MCP-Features header with unknown flag is ignored" ,
247+ path : "/" ,
248+ headers : map [string ]string {
249+ headers .MCPFeaturesHeader : "unknown_flag" ,
250+ },
251+ expectedTools : []string {"get_file_contents" , "create_repository" , "list_issues" , "create_issue" , "list_pull_requests" , "create_pull_request" , "hidden_by_holdback" },
226252 },
227253 }
228254
229255 for _ , tt := range tests {
230256 t .Run (tt .name , func (t * testing.T ) {
231257 var capturedInventory * inventory.Inventory
258+ var capturedCtx context.Context
259+
260+ // Create feature checker that reads from context (same as production)
261+ featureChecker := createHTTPFeatureChecker ()
232262
233263 // Create inventory factory that captures the built inventory
234264 inventoryFactory := func (r * http.Request ) (* inventory.Inventory , error ) {
265+ capturedCtx = r .Context ()
235266 builder := inventory .NewBuilder ().
236267 SetTools (tools ).
237- WithToolsets ([]string {"all" })
268+ WithToolsets ([]string {"all" }).
269+ WithFeatureChecker (featureChecker )
238270 builder = InventoryFiltersForRequest (r , builder )
239271 inv , err := builder .Build ()
240272 if err != nil {
@@ -277,7 +309,7 @@ func TestHTTPHandlerRoutes(t *testing.T) {
277309 // Verify the inventory was captured and has the expected tools
278310 require .NotNil (t , capturedInventory , "inventory should have been created" )
279311
280- toolNames := extractToolNames (capturedInventory )
312+ toolNames := extractToolNames (capturedInventory , capturedCtx )
281313 expectedSorted := make ([]string , len (tt .expectedTools ))
282314 copy (expectedSorted , tt .expectedTools )
283315 sort .Strings (expectedSorted )
0 commit comments