diff --git a/cmd/github-mcp-server/generate_docs.go b/cmd/github-mcp-server/generate_docs.go index a458c04b6..78fd6c40a 100644 --- a/cmd/github-mcp-server/generate_docs.go +++ b/cmd/github-mcp-server/generate_docs.go @@ -51,7 +51,8 @@ func generateReadmeDocs(readmePath string) error { t, _ := translations.TranslationHelper() // (not available to regular users) while including tools with FeatureFlagDisable. - r := github.NewInventory(t).WithToolsets([]string{"all"}).Build() + // Build() can only fail if WithTools specifies invalid tools - not used here + r, _ := github.NewInventory(t).WithToolsets([]string{"all"}).Build() // Generate toolsets documentation toolsetsDoc := generateToolsetsDoc(r) @@ -341,7 +342,8 @@ func generateRemoteToolsetsDoc() string { t, _ := translations.TranslationHelper() // Build inventory - stateless - r := github.NewInventory(t).Build() + // Build() can only fail if WithTools specifies invalid tools - not used here + r, _ := github.NewInventory(t).Build() // Generate table header (icon is combined with Name column) buf.WriteString("| Name | Description | API URL | 1-Click Install (VS Code) | Read-only Link | 1-Click Read-only Install (VS Code) |\n") diff --git a/cmd/github-mcp-server/list_scopes.go b/cmd/github-mcp-server/list_scopes.go index 2d1817500..d8b8bf392 100644 --- a/cmd/github-mcp-server/list_scopes.go +++ b/cmd/github-mcp-server/list_scopes.go @@ -121,7 +121,10 @@ func runListScopes() error { inventoryBuilder = inventoryBuilder.WithTools(enabledTools) } - inv := inventoryBuilder.Build() + inv, err := inventoryBuilder.Build() + if err != nil { + return fmt.Errorf("failed to build inventory: %w", err) + } // Collect all tools and their scopes output := collectToolScopes(inv, readOnly) diff --git a/internal/ghmcp/server.go b/internal/ghmcp/server.go index 250f6b4cc..b2e82f4e0 100644 --- a/internal/ghmcp/server.go +++ b/internal/ghmcp/server.go @@ -221,7 +221,7 @@ func NewMCPServer(cfg MCPServerConfig) (*mcp.Server, error) { WithDeprecatedAliases(github.DeprecatedToolAliases). WithReadOnly(cfg.ReadOnly). WithToolsets(enabledToolsets). - WithTools(github.CleanTools(cfg.EnabledTools)). + WithTools(cfg.EnabledTools). WithFeatureChecker(createFeatureChecker(cfg.EnabledFeatures)) // Apply token scope filtering if scopes are known (for PAT filtering) @@ -229,7 +229,10 @@ func NewMCPServer(cfg MCPServerConfig) (*mcp.Server, error) { inventoryBuilder = inventoryBuilder.WithFilter(github.CreateToolScopeFilter(cfg.TokenScopes)) } - inventory := inventoryBuilder.Build() + inventory, err := inventoryBuilder.Build() + if err != nil { + return nil, fmt.Errorf("failed to build inventory: %w", err) + } if unrecognized := inventory.UnrecognizedToolsets(); len(unrecognized) > 0 { fmt.Fprintf(os.Stderr, "Warning: unrecognized toolsets ignored: %s\n", strings.Join(unrecognized, ", ")) diff --git a/pkg/github/dynamic_tools_test.go b/pkg/github/dynamic_tools_test.go index 8d12b78c2..260b6b912 100644 --- a/pkg/github/dynamic_tools_test.go +++ b/pkg/github/dynamic_tools_test.go @@ -25,9 +25,10 @@ func createDynamicRequest(args map[string]any) *mcp.CallToolRequest { func TestDynamicTools_ListAvailableToolsets(t *testing.T) { // Build a registry with no toolsets enabled (dynamic mode) - reg := NewInventory(translations.NullTranslationHelper). + reg, err := NewInventory(translations.NullTranslationHelper). WithToolsets([]string{}). Build() + require.NoError(t, err) // Create a mock server server := mcp.NewServer(&mcp.Implementation{Name: "test"}, nil) @@ -73,9 +74,10 @@ func TestDynamicTools_ListAvailableToolsets(t *testing.T) { func TestDynamicTools_GetToolsetTools(t *testing.T) { // Build a registry with no toolsets enabled (dynamic mode) - reg := NewInventory(translations.NullTranslationHelper). + reg, err := NewInventory(translations.NullTranslationHelper). WithToolsets([]string{}). Build() + require.NoError(t, err) // Create a mock server server := mcp.NewServer(&mcp.Implementation{Name: "test"}, nil) @@ -122,9 +124,10 @@ func TestDynamicTools_GetToolsetTools(t *testing.T) { func TestDynamicTools_EnableToolset(t *testing.T) { // Build a registry with no toolsets enabled (dynamic mode) - reg := NewInventory(translations.NullTranslationHelper). + reg, err := NewInventory(translations.NullTranslationHelper). WithToolsets([]string{}). Build() + require.NoError(t, err) // Create a mock server server := mcp.NewServer(&mcp.Implementation{Name: "test"}, nil) @@ -170,9 +173,10 @@ func TestDynamicTools_EnableToolset(t *testing.T) { func TestDynamicTools_EnableToolset_InvalidToolset(t *testing.T) { // Build a registry with no toolsets enabled (dynamic mode) - reg := NewInventory(translations.NullTranslationHelper). + reg, err := NewInventory(translations.NullTranslationHelper). WithToolsets([]string{}). Build() + require.NoError(t, err) // Create a mock server server := mcp.NewServer(&mcp.Implementation{Name: "test"}, nil) @@ -203,7 +207,8 @@ func TestDynamicTools_EnableToolset_InvalidToolset(t *testing.T) { func TestDynamicTools_ToolsetsEnum(t *testing.T) { // Build a registry - reg := NewInventory(translations.NullTranslationHelper).Build() + reg, err := NewInventory(translations.NullTranslationHelper).Build() + require.NoError(t, err) // Get tools to verify they have proper enum values tools := DynamicTools(reg) diff --git a/pkg/github/scope_filter_test.go b/pkg/github/scope_filter_test.go index 451d1a64e..9cdd4db19 100644 --- a/pkg/github/scope_filter_test.go +++ b/pkg/github/scope_filter_test.go @@ -167,11 +167,12 @@ func TestCreateToolScopeFilter_Integration(t *testing.T) { filter := CreateToolScopeFilter([]string{"repo"}) // Build inventory with the filter - inv := inventory.NewBuilder(). + inv, err := inventory.NewBuilder(). SetTools(tools). WithToolsets([]string{"test"}). WithFilter(filter). Build() + require.NoError(t, err) // Get available tools availableTools := inv.AvailableTools(context.Background()) diff --git a/pkg/github/tools.go b/pkg/github/tools.go index b15c4fc9a..4384b730d 100644 --- a/pkg/github/tools.go +++ b/pkg/github/tools.go @@ -309,7 +309,8 @@ func ToStringPtr(s string) *string { // GenerateToolsetsHelp generates the help text for the toolsets flag func GenerateToolsetsHelp() string { // Get toolset group to derive defaults and available toolsets - r := NewInventory(stubTranslator).Build() + // Build() can only fail if WithTools specifies invalid tools - not used here + r, _ := NewInventory(stubTranslator).Build() // Format default tools from metadata using strings.Builder var defaultBuf strings.Builder @@ -391,7 +392,8 @@ func AddDefaultToolset(result []string) []string { result = RemoveToolset(result, string(ToolsetMetadataDefault.ID)) // Get default toolset IDs from the Inventory - r := NewInventory(stubTranslator).Build() + // Build() can only fail if WithTools specifies invalid tools - not used here + r, _ := NewInventory(stubTranslator).Build() for _, id := range r.DefaultToolsetIDs() { if !seen[string(id)] { result = append(result, string(id)) @@ -443,7 +445,8 @@ func CleanTools(toolNames []string) []string { // GetDefaultToolsetIDs returns the IDs of toolsets marked as Default. // This is a convenience function that builds an inventory to determine defaults. func GetDefaultToolsetIDs() []string { - r := NewInventory(stubTranslator).Build() + // Build() can only fail if WithTools specifies invalid tools - not used here + r, _ := NewInventory(stubTranslator).Build() ids := r.DefaultToolsetIDs() result := make([]string, len(ids)) for i, id := range ids { diff --git a/pkg/github/toolset_icons_test.go b/pkg/github/toolset_icons_test.go index fd9cec462..7cfe4bef7 100644 --- a/pkg/github/toolset_icons_test.go +++ b/pkg/github/toolset_icons_test.go @@ -13,7 +13,8 @@ import ( // This prevents broken icon references from being merged. func TestAllToolsetIconsExist(t *testing.T) { // Get all available toolsets from the inventory - inv := NewInventory(stubTranslator).Build() + inv, err := NewInventory(stubTranslator).Build() + require.NoError(t, err) toolsets := inv.AvailableToolsets() // Also test remote-only toolsets @@ -72,7 +73,8 @@ func TestToolsetMetadataHasIcons(t *testing.T) { "default": true, // Meta-toolset } - inv := NewInventory(stubTranslator).Build() + inv, err := NewInventory(stubTranslator).Build() + require.NoError(t, err) toolsets := inv.AvailableToolsets() for _, ts := range toolsets { diff --git a/pkg/inventory/builder.go b/pkg/inventory/builder.go index 0400c2a24..58abb8ad1 100644 --- a/pkg/inventory/builder.go +++ b/pkg/inventory/builder.go @@ -2,6 +2,7 @@ package inventory import ( "context" + "fmt" "sort" "strings" ) @@ -101,6 +102,7 @@ func (b *Builder) WithToolsets(toolsetIDs []string) *Builder { // WithTools specifies additional tools that bypass toolset filtering. // These tools are additive - they will be included even if their toolset is not enabled. // Read-only filtering still applies to these tools. +// Input is cleaned (trimmed, deduplicated) during Build(). // Deprecated tool aliases are automatically resolved to their canonical names during Build(). // Returns self for chaining. func (b *Builder) WithTools(toolNames []string) *Builder { @@ -127,11 +129,33 @@ func (b *Builder) WithFilter(filter ToolFilter) *Builder { return b } +// cleanTools trims whitespace and removes duplicates from tool names. +// Empty strings after trimming are excluded. +func cleanTools(tools []string) []string { + seen := make(map[string]bool) + var cleaned []string + for _, name := range tools { + trimmed := strings.TrimSpace(name) + if trimmed == "" { + continue + } + if !seen[trimmed] { + seen[trimmed] = true + cleaned = append(cleaned, trimmed) + } + } + return cleaned +} + // Build creates the final Inventory with all configuration applied. // This processes toolset filtering, tool name resolution, and sets up // the inventory for use. The returned Inventory is ready for use with // AvailableTools(), RegisterAll(), etc. -func (b *Builder) Build() *Inventory { +// +// Build returns an error if any tools specified via WithTools() are not recognized +// (i.e., they don't exist in the tool set and are not deprecated aliases). +// This ensures invalid tool configurations fail fast at build time. +func (b *Builder) Build() (*Inventory, error) { r := &Inventory{ tools: b.tools, resourceTemplates: b.resourceTemplates, @@ -145,10 +169,19 @@ func (b *Builder) Build() *Inventory { // Process toolsets and pre-compute metadata in a single pass r.enabledToolsets, r.unrecognizedToolsets, r.toolsetIDs, r.toolsetIDSet, r.defaultToolsetIDs, r.toolsetDescriptions = b.processToolsets() - // Process additional tools (resolve aliases) + // Build set of valid tool names for validation + validToolNames := make(map[string]bool, len(b.tools)) + for i := range b.tools { + validToolNames[b.tools[i].Tool.Name] = true + } + + // Process additional tools (clean, resolve aliases, and track unrecognized) if len(b.additionalTools) > 0 { - r.additionalTools = make(map[string]bool, len(b.additionalTools)) - for _, name := range b.additionalTools { + cleanedTools := cleanTools(b.additionalTools) + + r.additionalTools = make(map[string]bool, len(cleanedTools)) + var unrecognizedTools []string + for _, name := range cleanedTools { // Always include the original name - this handles the case where // the tool exists but is controlled by a feature flag that's OFF. r.additionalTools[name] = true @@ -157,11 +190,19 @@ func (b *Builder) Build() *Inventory { // the new consolidated tool is available. if canonical, isAlias := b.deprecatedAliases[name]; isAlias { r.additionalTools[canonical] = true + } else if !validToolNames[name] { + // Not a valid tool and not a deprecated alias - track as unrecognized + unrecognizedTools = append(unrecognizedTools, name) } } + + // Error out if there are unrecognized tools + if len(unrecognizedTools) > 0 { + return nil, fmt.Errorf("unrecognized tools: %s", strings.Join(unrecognizedTools, ", ")) + } } - return r + return r, nil } // processToolsets processes the toolsetIDs configuration and returns: diff --git a/pkg/inventory/registry_test.go b/pkg/inventory/registry_test.go index 136f8e523..bb3337af0 100644 --- a/pkg/inventory/registry_test.go +++ b/pkg/inventory/registry_test.go @@ -7,8 +7,18 @@ import ( "testing" "github.com/modelcontextprotocol/go-sdk/mcp" + "github.com/stretchr/testify/require" ) +// mustBuild is a test helper that calls Build() and fails the test if an error occurs. +// Use this for tests where Build() is not expected to fail. +func mustBuild(t *testing.T, b *Builder) *Inventory { + t.Helper() + inv, err := b.Build() + require.NoError(t, err) + return inv +} + // testToolsetMetadata returns a ToolsetMetadata for testing func testToolsetMetadata(id string) ToolsetMetadata { return ToolsetMetadata{ @@ -65,7 +75,7 @@ func mockTool(name string, toolsetID string, readOnly bool) ServerTool { } func TestNewRegistryEmpty(t *testing.T) { - reg := NewBuilder().Build() + reg := mustBuild(t, NewBuilder()) if len(reg.AvailableTools(context.Background())) != 0 { t.Fatalf("Expected tools to be empty") } @@ -84,7 +94,7 @@ func TestNewRegistryWithTools(t *testing.T) { mockTool("tool3", "toolset2", true), } - reg := NewBuilder().SetTools(tools).Build() + reg := mustBuild(t, NewBuilder().SetTools(tools)) if len(reg.AllTools()) != 3 { t.Errorf("Expected 3 tools, got %d", len(reg.AllTools())) @@ -98,7 +108,7 @@ func TestAvailableTools_NoFilters(t *testing.T) { mockTool("tool_c", "toolset2", true), } - reg := NewBuilder().SetTools(tools).WithToolsets([]string{"all"}).Build() + reg := mustBuild(t, NewBuilder().SetTools(tools).WithToolsets([]string{"all"})) available := reg.AvailableTools(context.Background()) if len(available) != 3 { @@ -121,14 +131,14 @@ func TestWithReadOnly(t *testing.T) { } // Build without read-only - should have both tools - reg := NewBuilder().SetTools(tools).WithToolsets([]string{"all"}).Build() + reg := mustBuild(t, NewBuilder().SetTools(tools).WithToolsets([]string{"all"})) allTools := reg.AvailableTools(context.Background()) if len(allTools) != 2 { t.Fatalf("Expected 2 tools without read-only, got %d", len(allTools)) } // Build with read-only - should filter out write tools - readOnlyReg := NewBuilder().SetTools(tools).WithToolsets([]string{"all"}).WithReadOnly(true).Build() + readOnlyReg := mustBuild(t, NewBuilder().SetTools(tools).WithToolsets([]string{"all"}).WithReadOnly(true)) readOnlyTools := readOnlyReg.AvailableTools(context.Background()) if len(readOnlyTools) != 1 { t.Fatalf("Expected 1 tool in read-only, got %d", len(readOnlyTools)) @@ -146,14 +156,14 @@ func TestWithToolsets(t *testing.T) { } // Build with all toolsets - allReg := NewBuilder().SetTools(tools).WithToolsets([]string{"all"}).Build() + allReg := mustBuild(t, NewBuilder().SetTools(tools).WithToolsets([]string{"all"})) allTools := allReg.AvailableTools(context.Background()) if len(allTools) != 3 { t.Fatalf("Expected 3 tools without filter, got %d", len(allTools)) } // Build with specific toolsets - filteredReg := NewBuilder().SetTools(tools).WithToolsets([]string{"toolset1", "toolset3"}).Build() + filteredReg := mustBuild(t, NewBuilder().SetTools(tools).WithToolsets([]string{"toolset1", "toolset3"})) filteredTools := filteredReg.AvailableTools(context.Background()) if len(filteredTools) != 2 { @@ -177,7 +187,7 @@ func TestWithToolsetsTrimsWhitespace(t *testing.T) { } // Whitespace should be trimmed - filteredReg := NewBuilder().SetTools(tools).WithToolsets([]string{" toolset1 ", " toolset2 "}).Build() + filteredReg := mustBuild(t, NewBuilder().SetTools(tools).WithToolsets([]string{" toolset1 ", " toolset2 "})) filteredTools := filteredReg.AvailableTools(context.Background()) if len(filteredTools) != 2 { @@ -191,7 +201,7 @@ func TestWithToolsetsDeduplicates(t *testing.T) { } // Duplicates should be removed - filteredReg := NewBuilder().SetTools(tools).WithToolsets([]string{"toolset1", "toolset1", " toolset1 "}).Build() + filteredReg := mustBuild(t, NewBuilder().SetTools(tools).WithToolsets([]string{"toolset1", "toolset1", " toolset1 "})) filteredTools := filteredReg.AvailableTools(context.Background()) if len(filteredTools) != 1 { @@ -205,7 +215,7 @@ func TestWithToolsetsIgnoresEmptyStrings(t *testing.T) { } // Empty strings should be ignored - filteredReg := NewBuilder().SetTools(tools).WithToolsets([]string{"", "toolset1", " ", ""}).Build() + filteredReg := mustBuild(t, NewBuilder().SetTools(tools).WithToolsets([]string{"", "toolset1", " ", ""})) filteredTools := filteredReg.AvailableTools(context.Background()) if len(filteredTools) != 1 { @@ -253,7 +263,7 @@ func TestUnrecognizedToolsets(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - filtered := NewBuilder().SetTools(tools).WithToolsets(tt.input).Build() + filtered := mustBuild(t, NewBuilder().SetTools(tools).WithToolsets(tt.input)) unrecognized := filtered.UnrecognizedToolsets() if len(unrecognized) != len(tt.expectedUnrecognized) { @@ -270,6 +280,109 @@ func TestUnrecognizedToolsets(t *testing.T) { } } +func TestBuildErrorsOnUnrecognizedTools(t *testing.T) { + tools := []ServerTool{ + mockTool("tool1", "toolset1", true), + mockTool("tool2", "toolset2", true), + } + + deprecatedAliases := map[string]string{ + "old_tool": "tool1", + } + + tests := []struct { + name string + withTools []string + expectError bool + errorContains string + }{ + { + name: "all valid", + withTools: []string{"tool1", "tool2"}, + expectError: false, + }, + { + name: "one invalid", + withTools: []string{"tool1", "blabla"}, + expectError: true, + errorContains: "blabla", + }, + { + name: "multiple invalid", + withTools: []string{"invalid1", "tool1", "invalid2"}, + expectError: true, + errorContains: "invalid1", + }, + { + name: "deprecated alias is valid", + withTools: []string{"old_tool"}, + expectError: false, + }, + { + name: "mixed valid and deprecated alias", + withTools: []string{"old_tool", "tool2"}, + expectError: false, + }, + { + name: "empty input", + withTools: []string{}, + expectError: false, + }, + { + name: "whitespace trimmed from valid tool", + withTools: []string{" tool1 ", " tool2 "}, + expectError: false, + }, + { + name: "whitespace trimmed from invalid tool", + withTools: []string{" invalid_tool "}, + expectError: true, + errorContains: "invalid_tool", + }, + { + name: "duplicate tools deduplicated", + withTools: []string{"tool1", "tool1"}, + expectError: false, + }, + { + name: "duplicate invalid tools deduplicated", + withTools: []string{"blabla", "blabla"}, + expectError: true, + errorContains: "blabla", + }, + { + name: "mixed whitespace and duplicates", + withTools: []string{" tool1 ", "tool1", " tool1 "}, + expectError: false, + }, + { + name: "empty strings ignored", + withTools: []string{"", "tool1", " ", ""}, + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + inv, err := NewBuilder(). + SetTools(tools). + WithDeprecatedAliases(deprecatedAliases). + WithToolsets([]string{"all"}). + WithTools(tt.withTools). + Build() + + if tt.expectError { + require.Error(t, err, "Expected error for unrecognized tools") + require.Contains(t, err.Error(), tt.errorContains) + require.Nil(t, inv) + } else { + require.NoError(t, err) + require.NotNil(t, inv) + } + }) + } +} + func TestWithTools(t *testing.T) { tools := []ServerTool{ mockTool("tool1", "toolset1", true), @@ -279,7 +392,7 @@ func TestWithTools(t *testing.T) { // WithTools adds additional tools that bypass toolset filtering // When combined with WithToolsets([]), only the additional tools should be available - filteredReg := NewBuilder().SetTools(tools).WithToolsets([]string{}).WithTools([]string{"tool1", "tool3"}).Build() + filteredReg := mustBuild(t, NewBuilder().SetTools(tools).WithToolsets([]string{}).WithTools([]string{"tool1", "tool3"})) filteredTools := filteredReg.AvailableTools(context.Background()) if len(filteredTools) != 2 { @@ -304,7 +417,7 @@ func TestChainedFilters(t *testing.T) { } // Chain read-only and toolset filter - filtered := NewBuilder().SetTools(tools).WithReadOnly(true).WithToolsets([]string{"toolset1"}).Build() + filtered := mustBuild(t, NewBuilder().SetTools(tools).WithReadOnly(true).WithToolsets([]string{"toolset1"})) result := filtered.AvailableTools(context.Background()) if len(result) != 1 { @@ -322,7 +435,7 @@ func TestToolsetIDs(t *testing.T) { mockTool("tool3", "toolset_b", true), // duplicate toolset } - reg := NewBuilder().SetTools(tools).Build() + reg := mustBuild(t, NewBuilder().SetTools(tools)) ids := reg.ToolsetIDs() if len(ids) != 2 { @@ -341,7 +454,7 @@ func TestToolsetDescriptions(t *testing.T) { mockTool("tool2", "toolset2", true), } - reg := NewBuilder().SetTools(tools).Build() + reg := mustBuild(t, NewBuilder().SetTools(tools)) descriptions := reg.ToolsetDescriptions() if len(descriptions) != 2 { @@ -360,7 +473,7 @@ func TestToolsForToolset(t *testing.T) { mockTool("tool3", "toolset2", true), } - reg := NewBuilder().SetTools(tools).Build() + reg := mustBuild(t, NewBuilder().SetTools(tools)) toolset1Tools := reg.ToolsForToolset("toolset1") if len(toolset1Tools) != 2 { @@ -373,10 +486,10 @@ func TestWithDeprecatedAliases(t *testing.T) { mockTool("new_name", "toolset1", true), } - reg := NewBuilder().SetTools(tools).WithDeprecatedAliases(map[string]string{ + reg := mustBuild(t, NewBuilder().SetTools(tools).WithDeprecatedAliases(map[string]string{ "old_name": "new_name", "get_issue": "issue_read", - }).Build() + })) // Test resolving aliases resolved, aliasesUsed := reg.ResolveToolAliases([]string{"old_name"}) @@ -394,10 +507,10 @@ func TestResolveToolAliases(t *testing.T) { mockTool("some_tool", "toolset1", true), } - reg := NewBuilder().SetTools(tools). + reg := mustBuild(t, NewBuilder().SetTools(tools). WithDeprecatedAliases(map[string]string{ "get_issue": "issue_read", - }).Build() + })) // Test resolving a mix of aliases and canonical names input := []string{"get_issue", "some_tool"} @@ -426,7 +539,7 @@ func TestFindToolByName(t *testing.T) { mockTool("issue_read", "toolset1", true), } - reg := NewBuilder().SetTools(tools).Build() + reg := mustBuild(t, NewBuilder().SetTools(tools)) // Find by name tool, toolsetID, err := reg.FindToolByName("issue_read") @@ -456,7 +569,7 @@ func TestWithToolsAdditive(t *testing.T) { // Test WithTools bypasses toolset filtering // Enable only toolset2, but add issue_read as additional tool - filtered := NewBuilder().SetTools(tools).WithToolsets([]string{"toolset2"}).WithTools([]string{"issue_read"}).Build() + filtered := mustBuild(t, NewBuilder().SetTools(tools).WithToolsets([]string{"toolset2"}).WithTools([]string{"issue_read"})) available := filtered.AvailableTools(context.Background()) if len(available) != 2 { @@ -476,7 +589,7 @@ func TestWithToolsAdditive(t *testing.T) { } // Test WithTools respects read-only mode - readOnlyFiltered := NewBuilder().SetTools(tools).WithReadOnly(true).WithTools([]string{"issue_write"}).Build() + readOnlyFiltered := mustBuild(t, NewBuilder().SetTools(tools).WithReadOnly(true).WithTools([]string{"issue_write"})) available = readOnlyFiltered.AvailableTools(context.Background()) // issue_write should be excluded because read-only applies to additional tools too @@ -486,12 +599,10 @@ func TestWithToolsAdditive(t *testing.T) { } } - // Test WithTools with non-existent tool (should not error, just won't match anything) - nonexistent := NewBuilder().SetTools(tools).WithToolsets([]string{}).WithTools([]string{"nonexistent"}).Build() - available = nonexistent.AvailableTools(context.Background()) - if len(available) != 0 { - t.Errorf("expected 0 tools for non-existent additional tool, got %d", len(available)) - } + // Test WithTools with non-existent tool (should error during Build) + _, err := NewBuilder().SetTools(tools).WithToolsets([]string{}).WithTools([]string{"nonexistent"}).Build() + require.Error(t, err, "expected error for non-existent tool") + require.Contains(t, err.Error(), "nonexistent") } func TestWithToolsResolvesAliases(t *testing.T) { @@ -500,13 +611,12 @@ func TestWithToolsResolvesAliases(t *testing.T) { } // Using deprecated alias should resolve to canonical name - filtered := NewBuilder().SetTools(tools). + filtered := mustBuild(t, NewBuilder().SetTools(tools). WithDeprecatedAliases(map[string]string{ "get_issue": "issue_read", }). WithToolsets([]string{}). - WithTools([]string{"get_issue"}). - Build() + WithTools([]string{"get_issue"})) available := filtered.AvailableTools(context.Background()) if len(available) != 1 { @@ -522,7 +632,7 @@ func TestHasToolset(t *testing.T) { mockTool("tool1", "toolset1", true), } - reg := NewBuilder().SetTools(tools).WithToolsets([]string{"all"}).Build() + reg := mustBuild(t, NewBuilder().SetTools(tools).WithToolsets([]string{"all"})) if !reg.HasToolset("toolset1") { t.Error("expected HasToolset to return true for existing toolset") @@ -539,14 +649,14 @@ func TestEnabledToolsetIDs(t *testing.T) { } // Without filter, all toolsets are enabled - reg := NewBuilder().SetTools(tools).WithToolsets([]string{"all"}).Build() + reg := mustBuild(t, NewBuilder().SetTools(tools).WithToolsets([]string{"all"})) ids := reg.EnabledToolsetIDs() if len(ids) != 2 { t.Fatalf("Expected 2 enabled toolset IDs, got %d", len(ids)) } // With filter - filtered := NewBuilder().SetTools(tools).WithToolsets([]string{"toolset1"}).Build() + filtered := mustBuild(t, NewBuilder().SetTools(tools).WithToolsets([]string{"toolset1"})) filteredIDs := filtered.EnabledToolsetIDs() if len(filteredIDs) != 1 { t.Fatalf("Expected 1 enabled toolset ID, got %d", len(filteredIDs)) @@ -563,7 +673,7 @@ func TestAllTools(t *testing.T) { } // Even with read-only filter, AllTools returns everything - readOnlyReg := NewBuilder().SetTools(tools).WithToolsets([]string{"all"}).WithReadOnly(true).Build() + readOnlyReg := mustBuild(t, NewBuilder().SetTools(tools).WithToolsets([]string{"all"}).WithReadOnly(true)) allTools := readOnlyReg.AllTools() if len(allTools) != 2 { @@ -628,7 +738,7 @@ func TestForMCPRequest_Initialize(t *testing.T) { mockPrompt("prompt1", "repos"), } - reg := NewBuilder().SetTools(tools).SetResources(resources).SetPrompts(prompts).WithToolsets([]string{"all"}).Build() + reg := mustBuild(t, NewBuilder().SetTools(tools).SetResources(resources).SetPrompts(prompts).WithToolsets([]string{"all"})) filtered := reg.ForMCPRequest(MCPMethodInitialize, "") // Initialize should return empty - capabilities come from ServerOptions @@ -655,7 +765,7 @@ func TestForMCPRequest_ToolsList(t *testing.T) { mockPrompt("prompt1", "repos"), } - reg := NewBuilder().SetTools(tools).SetResources(resources).SetPrompts(prompts).WithToolsets([]string{"all"}).Build() + reg := mustBuild(t, NewBuilder().SetTools(tools).SetResources(resources).SetPrompts(prompts).WithToolsets([]string{"all"})) filtered := reg.ForMCPRequest(MCPMethodToolsList, "") // tools/list should return all tools, no resources or prompts @@ -677,7 +787,7 @@ func TestForMCPRequest_ToolsCall(t *testing.T) { mockTool("list_repos", "repos", true), } - reg := NewBuilder().SetTools(tools).WithToolsets([]string{"all"}).Build() + reg := mustBuild(t, NewBuilder().SetTools(tools).WithToolsets([]string{"all"})) filtered := reg.ForMCPRequest(MCPMethodToolsCall, "get_me") available := filtered.AvailableTools(context.Background()) @@ -694,7 +804,7 @@ func TestForMCPRequest_ToolsCall_NotFound(t *testing.T) { mockTool("get_me", "context", true), } - reg := NewBuilder().SetTools(tools).WithToolsets([]string{"all"}).Build() + reg := mustBuild(t, NewBuilder().SetTools(tools).WithToolsets([]string{"all"})) filtered := reg.ForMCPRequest(MCPMethodToolsCall, "nonexistent") if len(filtered.AvailableTools(context.Background())) != 0 { @@ -708,11 +818,11 @@ func TestForMCPRequest_ToolsCall_DeprecatedAlias(t *testing.T) { mockTool("list_commits", "repos", true), } - reg := NewBuilder().SetTools(tools). + reg := mustBuild(t, NewBuilder().SetTools(tools). WithToolsets([]string{"all"}). WithDeprecatedAliases(map[string]string{ "old_get_me": "get_me", - }).Build() + })) // Request using the deprecated alias filtered := reg.ForMCPRequest(MCPMethodToolsCall, "old_get_me") @@ -732,7 +842,7 @@ func TestForMCPRequest_ToolsCall_RespectsFilters(t *testing.T) { } // Apply read-only filter at build time, then ForMCPRequest - reg := NewBuilder().SetTools(tools).WithToolsets([]string{"all"}).WithReadOnly(true).Build() + reg := mustBuild(t, NewBuilder().SetTools(tools).WithToolsets([]string{"all"}).WithReadOnly(true)) filtered := reg.ForMCPRequest(MCPMethodToolsCall, "create_issue") // The tool exists in the filtered group, but AvailableTools respects read-only @@ -754,7 +864,7 @@ func TestForMCPRequest_ResourcesList(t *testing.T) { mockPrompt("prompt1", "repos"), } - reg := NewBuilder().SetTools(tools).SetResources(resources).SetPrompts(prompts).WithToolsets([]string{"all"}).Build() + reg := mustBuild(t, NewBuilder().SetTools(tools).SetResources(resources).SetPrompts(prompts).WithToolsets([]string{"all"})) filtered := reg.ForMCPRequest(MCPMethodResourcesList, "") if len(filtered.AvailableTools(context.Background())) != 0 { @@ -774,7 +884,7 @@ func TestForMCPRequest_ResourcesRead(t *testing.T) { mockResource("res2", "repos", "branch://{owner}/{repo}/{branch}"), } - reg := NewBuilder().SetResources(resources).WithToolsets([]string{"all"}).Build() + reg := mustBuild(t, NewBuilder().SetResources(resources).WithToolsets([]string{"all"})) // Pass a concrete URI - all resources remain registered, SDK handles matching filtered := reg.ForMCPRequest(MCPMethodResourcesRead, "repo://owner/repo") @@ -796,7 +906,7 @@ func TestForMCPRequest_PromptsList(t *testing.T) { mockPrompt("prompt2", "issues"), } - reg := NewBuilder().SetTools(tools).SetResources(resources).SetPrompts(prompts).WithToolsets([]string{"all"}).Build() + reg := mustBuild(t, NewBuilder().SetTools(tools).SetResources(resources).SetPrompts(prompts).WithToolsets([]string{"all"})) filtered := reg.ForMCPRequest(MCPMethodPromptsList, "") if len(filtered.AvailableTools(context.Background())) != 0 { @@ -816,7 +926,7 @@ func TestForMCPRequest_PromptsGet(t *testing.T) { mockPrompt("prompt2", "issues"), } - reg := NewBuilder().SetPrompts(prompts).WithToolsets([]string{"all"}).Build() + reg := mustBuild(t, NewBuilder().SetPrompts(prompts).WithToolsets([]string{"all"})) filtered := reg.ForMCPRequest(MCPMethodPromptsGet, "prompt1") available := filtered.AvailablePrompts(context.Background()) @@ -839,7 +949,7 @@ func TestForMCPRequest_UnknownMethod(t *testing.T) { mockPrompt("prompt1", "repos"), } - reg := NewBuilder().SetTools(tools).SetResources(resources).SetPrompts(prompts).WithToolsets([]string{"all"}).Build() + reg := mustBuild(t, NewBuilder().SetTools(tools).SetResources(resources).SetPrompts(prompts).WithToolsets([]string{"all"})) filtered := reg.ForMCPRequest("unknown/method", "") // Unknown methods should return empty @@ -866,7 +976,7 @@ func TestForMCPRequest_DoesNotMutateOriginal(t *testing.T) { mockPrompt("prompt1", "repos"), } - original := NewBuilder().SetTools(tools).SetResources(resources).SetPrompts(prompts).WithToolsets([]string{"all"}).Build() + original := mustBuild(t, NewBuilder().SetTools(tools).SetResources(resources).SetPrompts(prompts).WithToolsets([]string{"all"})) filtered := original.ForMCPRequest(MCPMethodToolsCall, "tool1") // Original should be unchanged @@ -901,10 +1011,9 @@ func TestForMCPRequest_ChainedWithOtherFilters(t *testing.T) { } // Chain: default toolsets -> read-only -> specific method - reg := NewBuilder().SetTools(tools). + reg := mustBuild(t, NewBuilder().SetTools(tools). WithToolsets([]string{"default"}). - WithReadOnly(true). - Build() + WithReadOnly(true)) filtered := reg.ForMCPRequest(MCPMethodToolsList, "") available := filtered.AvailableTools(context.Background()) @@ -942,7 +1051,7 @@ func TestForMCPRequest_ResourcesTemplatesList(t *testing.T) { mockResource("res1", "repos", "repo://{owner}/{repo}"), } - reg := NewBuilder().SetTools(tools).SetResources(resources).WithToolsets([]string{"all"}).Build() + reg := mustBuild(t, NewBuilder().SetTools(tools).SetResources(resources).WithToolsets([]string{"all"})) filtered := reg.ForMCPRequest(MCPMethodResourcesTemplatesList, "") // Same behavior as resources/list @@ -992,7 +1101,7 @@ func TestFeatureFlagEnable(t *testing.T) { } // Without feature checker, tool with FeatureFlagEnable should be excluded - reg := NewBuilder().SetTools(tools).WithToolsets([]string{"all"}).Build() + reg := mustBuild(t, NewBuilder().SetTools(tools).WithToolsets([]string{"all"})) available := reg.AvailableTools(context.Background()) if len(available) != 1 { t.Fatalf("Expected 1 tool without feature checker, got %d", len(available)) @@ -1003,7 +1112,7 @@ func TestFeatureFlagEnable(t *testing.T) { // With feature checker returning false, tool should still be excluded checkerFalse := func(_ context.Context, _ string) (bool, error) { return false, nil } - regFalse := NewBuilder().SetTools(tools).WithToolsets([]string{"all"}).WithFeatureChecker(checkerFalse).Build() + regFalse := mustBuild(t, NewBuilder().SetTools(tools).WithToolsets([]string{"all"}).WithFeatureChecker(checkerFalse)) availableFalse := regFalse.AvailableTools(context.Background()) if len(availableFalse) != 1 { t.Fatalf("Expected 1 tool with false checker, got %d", len(availableFalse)) @@ -1013,7 +1122,7 @@ func TestFeatureFlagEnable(t *testing.T) { checkerTrue := func(_ context.Context, flag string) (bool, error) { return flag == "my_feature", nil } - regTrue := NewBuilder().SetTools(tools).WithToolsets([]string{"all"}).WithFeatureChecker(checkerTrue).Build() + regTrue := mustBuild(t, NewBuilder().SetTools(tools).WithToolsets([]string{"all"}).WithFeatureChecker(checkerTrue)) availableTrue := regTrue.AvailableTools(context.Background()) if len(availableTrue) != 2 { t.Fatalf("Expected 2 tools with true checker, got %d", len(availableTrue)) @@ -1027,7 +1136,7 @@ func TestFeatureFlagDisable(t *testing.T) { } // Without feature checker, tool with FeatureFlagDisable should be included (flag is false) - reg := NewBuilder().SetTools(tools).WithToolsets([]string{"all"}).Build() + reg := mustBuild(t, NewBuilder().SetTools(tools).WithToolsets([]string{"all"})) available := reg.AvailableTools(context.Background()) if len(available) != 2 { t.Fatalf("Expected 2 tools without feature checker, got %d", len(available)) @@ -1037,7 +1146,7 @@ func TestFeatureFlagDisable(t *testing.T) { checkerTrue := func(_ context.Context, flag string) (bool, error) { return flag == "kill_switch", nil } - regFiltered := NewBuilder().SetTools(tools).WithToolsets([]string{"all"}).WithFeatureChecker(checkerTrue).Build() + regFiltered := mustBuild(t, NewBuilder().SetTools(tools).WithToolsets([]string{"all"}).WithFeatureChecker(checkerTrue)) availableFiltered := regFiltered.AvailableTools(context.Background()) if len(availableFiltered) != 1 { t.Fatalf("Expected 1 tool with kill_switch enabled, got %d", len(availableFiltered)) @@ -1055,21 +1164,21 @@ func TestFeatureFlagBoth(t *testing.T) { // Enable flag not set -> excluded checker1 := func(_ context.Context, _ string) (bool, error) { return false, nil } - reg1 := NewBuilder().SetTools(tools).WithToolsets([]string{"all"}).WithFeatureChecker(checker1).Build() + reg1 := mustBuild(t, NewBuilder().SetTools(tools).WithToolsets([]string{"all"}).WithFeatureChecker(checker1)) if len(reg1.AvailableTools(context.Background())) != 0 { t.Error("Tool should be excluded when enable flag is false") } // Enable flag set, disable flag not set -> included checker2 := func(_ context.Context, flag string) (bool, error) { return flag == "new_feature", nil } - reg2 := NewBuilder().SetTools(tools).WithToolsets([]string{"all"}).WithFeatureChecker(checker2).Build() + reg2 := mustBuild(t, NewBuilder().SetTools(tools).WithToolsets([]string{"all"}).WithFeatureChecker(checker2)) if len(reg2.AvailableTools(context.Background())) != 1 { t.Error("Tool should be included when enable flag is true and disable flag is false") } // Enable flag set, disable flag also set -> excluded (disable wins) checker3 := func(_ context.Context, _ string) (bool, error) { return true, nil } - reg3 := NewBuilder().SetTools(tools).WithToolsets([]string{"all"}).WithFeatureChecker(checker3).Build() + reg3 := mustBuild(t, NewBuilder().SetTools(tools).WithToolsets([]string{"all"}).WithFeatureChecker(checker3)) if len(reg3.AvailableTools(context.Background())) != 0 { t.Error("Tool should be excluded when both flags are true (disable wins)") } @@ -1084,7 +1193,7 @@ func TestFeatureFlagError(t *testing.T) { checkerError := func(_ context.Context, _ string) (bool, error) { return false, fmt.Errorf("simulated error") } - reg := NewBuilder().SetTools(tools).WithFeatureChecker(checkerError).Build() + reg := mustBuild(t, NewBuilder().SetTools(tools).WithFeatureChecker(checkerError)) available := reg.AvailableTools(context.Background()) if len(available) != 0 { t.Errorf("Expected 0 tools when checker errors, got %d", len(available)) @@ -1102,7 +1211,7 @@ func TestFeatureFlagResources(t *testing.T) { } // Without checker, resource with enable flag should be excluded - reg := NewBuilder().SetResources(resources).WithToolsets([]string{"all"}).Build() + reg := mustBuild(t, NewBuilder().SetResources(resources).WithToolsets([]string{"all"})) available := reg.AvailableResourceTemplates(context.Background()) if len(available) != 1 { t.Fatalf("Expected 1 resource without checker, got %d", len(available)) @@ -1110,7 +1219,7 @@ func TestFeatureFlagResources(t *testing.T) { // With checker returning true, both should be included checker := func(_ context.Context, _ string) (bool, error) { return true, nil } - regWithChecker := NewBuilder().SetResources(resources).WithToolsets([]string{"all"}).WithFeatureChecker(checker).Build() + regWithChecker := mustBuild(t, NewBuilder().SetResources(resources).WithToolsets([]string{"all"}).WithFeatureChecker(checker)) if len(regWithChecker.AvailableResourceTemplates(context.Background())) != 2 { t.Errorf("Expected 2 resources with checker, got %d", len(regWithChecker.AvailableResourceTemplates(context.Background()))) } @@ -1127,7 +1236,7 @@ func TestFeatureFlagPrompts(t *testing.T) { } // Without checker, prompt with enable flag should be excluded - reg := NewBuilder().SetPrompts(prompts).WithToolsets([]string{"all"}).Build() + reg := mustBuild(t, NewBuilder().SetPrompts(prompts).WithToolsets([]string{"all"})) available := reg.AvailablePrompts(context.Background()) if len(available) != 1 { t.Fatalf("Expected 1 prompt without checker, got %d", len(available)) @@ -1135,7 +1244,7 @@ func TestFeatureFlagPrompts(t *testing.T) { // With checker returning true, both should be included checker := func(_ context.Context, _ string) (bool, error) { return true, nil } - regWithChecker := NewBuilder().SetPrompts(prompts).WithToolsets([]string{"all"}).WithFeatureChecker(checker).Build() + regWithChecker := mustBuild(t, NewBuilder().SetPrompts(prompts).WithToolsets([]string{"all"}).WithFeatureChecker(checker)) if len(regWithChecker.AvailablePrompts(context.Background())) != 2 { t.Errorf("Expected 2 prompts with checker, got %d", len(regWithChecker.AvailablePrompts(context.Background()))) } @@ -1218,7 +1327,7 @@ func TestServerToolEnabled(t *testing.T) { tool := mockTool("test_tool", "toolset1", true) tool.Enabled = tt.enabledFunc - reg := NewBuilder().SetTools([]ServerTool{tool}).WithToolsets([]string{"all"}).Build() + reg := mustBuild(t, NewBuilder().SetTools([]ServerTool{tool}).WithToolsets([]string{"all"})) available := reg.AvailableTools(context.Background()) if len(available) != tt.expectedCount { @@ -1250,7 +1359,7 @@ func TestServerToolEnabledWithContext(t *testing.T) { return user != nil && user.(string) == "authorized", nil } - reg := NewBuilder().SetTools([]ServerTool{tool}).WithToolsets([]string{"all"}).Build() + reg := mustBuild(t, NewBuilder().SetTools([]ServerTool{tool}).WithToolsets([]string{"all"})) // Without user in context - tool should be excluded available := reg.AvailableTools(context.Background()) @@ -1286,11 +1395,10 @@ func TestBuilderWithFilter(t *testing.T) { return tool.Tool.Name != "tool2", nil } - reg := NewBuilder(). + reg := mustBuild(t, NewBuilder(). SetTools(tools). WithToolsets([]string{"all"}). - WithFilter(filter). - Build() + WithFilter(filter)) available := reg.AvailableTools(context.Background()) if len(available) != 2 { @@ -1322,12 +1430,11 @@ func TestBuilderWithMultipleFilters(t *testing.T) { return tool.Tool.Name != "tool3", nil } - reg := NewBuilder(). + reg := mustBuild(t, NewBuilder(). SetTools(tools). WithToolsets([]string{"all"}). WithFilter(filter1). - WithFilter(filter2). - Build() + WithFilter(filter2)) available := reg.AvailableTools(context.Background()) if len(available) != 2 { @@ -1357,11 +1464,10 @@ func TestBuilderFilterError(t *testing.T) { return false, fmt.Errorf("filter error") } - reg := NewBuilder(). + reg := mustBuild(t, NewBuilder(). SetTools(tools). WithToolsets([]string{"all"}). - WithFilter(filter). - Build() + WithFilter(filter)) available := reg.AvailableTools(context.Background()) if len(available) != 0 { @@ -1387,11 +1493,10 @@ func TestBuilderFilterWithContext(t *testing.T) { return true, nil } - reg := NewBuilder(). + reg := mustBuild(t, NewBuilder(). SetTools(tools). WithToolsets([]string{"all"}). - WithFilter(filter). - Build() + WithFilter(filter)) // With public scope - private_tool should be excluded ctxPublic := context.WithValue(context.Background(), scopeKey, "public") @@ -1420,10 +1525,9 @@ func TestEnabledAndFeatureFlagInteraction(t *testing.T) { } // Feature flag not enabled - tool should be excluded despite Enabled returning true - reg1 := NewBuilder(). + reg1 := mustBuild(t, NewBuilder(). SetTools([]ServerTool{tool}). - WithToolsets([]string{"all"}). - Build() + WithToolsets([]string{"all"})) available1 := reg1.AvailableTools(context.Background()) if len(available1) != 0 { t.Error("Tool should be excluded when feature flag is not enabled") @@ -1433,11 +1537,10 @@ func TestEnabledAndFeatureFlagInteraction(t *testing.T) { checker := func(_ context.Context, flag string) (bool, error) { return flag == "my_feature", nil } - reg2 := NewBuilder(). + reg2 := mustBuild(t, NewBuilder(). SetTools([]ServerTool{tool}). WithToolsets([]string{"all"}). - WithFeatureChecker(checker). - Build() + WithFeatureChecker(checker)) available2 := reg2.AvailableTools(context.Background()) if len(available2) != 1 { t.Error("Tool should be included when both Enabled and feature flag pass") @@ -1447,11 +1550,10 @@ func TestEnabledAndFeatureFlagInteraction(t *testing.T) { tool.Enabled = func(_ context.Context) (bool, error) { return false, nil } - reg3 := NewBuilder(). + reg3 := mustBuild(t, NewBuilder(). SetTools([]ServerTool{tool}). WithToolsets([]string{"all"}). - WithFeatureChecker(checker). - Build() + WithFeatureChecker(checker)) available3 := reg3.AvailableTools(context.Background()) if len(available3) != 0 { t.Error("Tool should be excluded when Enabled returns false") @@ -1469,11 +1571,10 @@ func TestEnabledAndBuilderFilterInteraction(t *testing.T) { return false, nil } - reg := NewBuilder(). + reg := mustBuild(t, NewBuilder(). SetTools([]ServerTool{tool}). WithToolsets([]string{"all"}). - WithFilter(filter). - Build() + WithFilter(filter)) available := reg.AvailableTools(context.Background()) if len(available) != 0 { @@ -1497,12 +1598,11 @@ func TestAllFiltersInteraction(t *testing.T) { } // All conditions pass - tool should be included - reg := NewBuilder(). + reg := mustBuild(t, NewBuilder(). SetTools([]ServerTool{tool}). WithToolsets([]string{"all"}). WithFeatureChecker(checker). - WithFilter(filter). - Build() + WithFilter(filter)) available := reg.AvailableTools(context.Background()) if len(available) != 1 { @@ -1514,12 +1614,11 @@ func TestAllFiltersInteraction(t *testing.T) { return false, nil } - reg2 := NewBuilder(). + reg2 := mustBuild(t, NewBuilder(). SetTools([]ServerTool{tool}). WithToolsets([]string{"all"}). WithFeatureChecker(checker). - WithFilter(filterFalse). - Build() + WithFilter(filterFalse)) available2 := reg2.AvailableTools(context.Background()) if len(available2) != 0 { @@ -1538,11 +1637,10 @@ func TestFilteredTools(t *testing.T) { return tool.Tool.Name == "tool1", nil } - reg := NewBuilder(). + reg := mustBuild(t, NewBuilder(). SetTools(tools). WithToolsets([]string{"all"}). - WithFilter(filter). - Build() + WithFilter(filter)) filtered, err := reg.FilteredTools(context.Background()) if err != nil { @@ -1565,11 +1663,10 @@ func TestFilteredToolsMatchesAvailableTools(t *testing.T) { mockTool("tool3", "toolset2", true), } - reg := NewBuilder(). + reg := mustBuild(t, NewBuilder(). SetTools(tools). WithToolsets([]string{"toolset1"}). - WithReadOnly(true). - Build() + WithReadOnly(true)) ctx := context.Background() filtered, err := reg.FilteredTools(ctx) @@ -1619,13 +1716,12 @@ func TestFilteringOrder(t *testing.T) { return true, nil } - reg := NewBuilder(). + reg := mustBuild(t, NewBuilder(). SetTools([]ServerTool{tool}). WithToolsets([]string{"all"}). WithReadOnly(true). // This will exclude the tool (it's not read-only) WithFeatureChecker(checker). - WithFilter(filter). - Build() + WithFilter(filter)) _ = reg.AvailableTools(context.Background()) @@ -1653,10 +1749,9 @@ func TestForMCPRequest_ToolsCall_FeatureFlaggedVariants(t *testing.T) { } // Test 1: Flag is OFF - first tool variant should be available - regFlagOff := NewBuilder(). + regFlagOff := mustBuild(t, NewBuilder(). SetTools(tools). - WithToolsets([]string{"all"}). - Build() + WithToolsets([]string{"all"})) filteredOff := regFlagOff.ForMCPRequest(MCPMethodToolsCall, "get_job_logs") availableOff := filteredOff.AvailableTools(context.Background()) if len(availableOff) != 1 { @@ -1671,11 +1766,10 @@ func TestForMCPRequest_ToolsCall_FeatureFlaggedVariants(t *testing.T) { checker := func(_ context.Context, flag string) (bool, error) { return flag == "consolidated_flag", nil } - regFlagOn := NewBuilder(). + regFlagOn := mustBuild(t, NewBuilder(). SetTools(tools). WithToolsets([]string{"all"}). - WithFeatureChecker(checker). - Build() + WithFeatureChecker(checker)) filteredOn := regFlagOn.ForMCPRequest(MCPMethodToolsCall, "get_job_logs") availableOn := filteredOn.AvailableTools(context.Background()) if len(availableOn) != 1 { @@ -1707,12 +1801,11 @@ func TestWithTools_DeprecatedAliasAndFeatureFlag(t *testing.T) { // Test 1: Flag OFF - old_tool should be available via direct name match // (not via alias resolution to new_tool, since old_tool still exists) - regFlagOff := NewBuilder(). + regFlagOff := mustBuild(t, NewBuilder(). SetTools(tools). WithDeprecatedAliases(deprecatedAliases). WithToolsets([]string{}). // No toolsets enabled - WithTools([]string{"old_tool"}). // Explicitly request old tool - Build() + WithTools([]string{"old_tool"})) // Explicitly request old tool availableOff := regFlagOff.AvailableTools(context.Background()) if len(availableOff) != 1 { t.Fatalf("Flag OFF: Expected 1 tool, got %d", len(availableOff)) @@ -1725,13 +1818,12 @@ func TestWithTools_DeprecatedAliasAndFeatureFlag(t *testing.T) { checker := func(_ context.Context, flag string) (bool, error) { return flag == "my_flag", nil } - regFlagOn := NewBuilder(). + regFlagOn := mustBuild(t, NewBuilder(). SetTools(tools). WithDeprecatedAliases(deprecatedAliases). WithToolsets([]string{}). // No toolsets enabled WithTools([]string{"old_tool"}). // Request old tool name - WithFeatureChecker(checker). - Build() + WithFeatureChecker(checker)) availableOn := regFlagOn.AvailableTools(context.Background()) if len(availableOn) != 1 { t.Fatalf("Flag ON: Expected 1 tool, got %d", len(availableOn))