Skip to content

Commit 58ed90a

Browse files
committed
add tests for X-MCP-Features header parsing
1 parent 10757ab commit 58ed90a

File tree

1 file changed

+43
-11
lines changed

1 file changed

+43
-11
lines changed

pkg/http/handler_test.go

Lines changed: 43 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
3542
func 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

Comments
 (0)