diff --git a/internal/server/allowed_tools_integration_test.go b/internal/server/allowed_tools_integration_test.go index d0d5a8c81..801589959 100644 --- a/internal/server/allowed_tools_integration_test.go +++ b/internal/server/allowed_tools_integration_test.go @@ -463,6 +463,88 @@ func TestIsToolAllowed_Integration(t *testing.T) { assert.True(t, us.isToolAllowed("unknown", "tool")) } +// TestBuildAllowedToolSets_WildcardStar verifies that Tools: ["*"] results in no +// entry in the sets map, meaning all tools are allowed (same as an empty list). +func TestBuildAllowedToolSets_WildcardStar(t *testing.T) { + cfg := &config.Config{ + Servers: map[string]*config.ServerConfig{ + "wildcard": {Tools: []string{"*"}}, + "restricted": {Tools: []string{"a", "b"}}, + "open": {}, + }, + } + sets := buildAllowedToolSets(cfg) + + _, hasWildcardServer := sets["wildcard"] + assert.False(t, hasWildcardServer, "server with wildcard must not be in the set map") + + _, hasRestricted := sets["restricted"] + assert.True(t, hasRestricted, "restricted server should still be in the set map") + + _, hasOpen := sets["open"] + assert.False(t, hasOpen, "open server must not be in the set map") +} + +// TestBuildAllowedToolSets_WildcardMixed verifies that a "*" anywhere in the +// Tools list causes the server to be treated as unrestricted. +func TestBuildAllowedToolSets_WildcardMixed(t *testing.T) { + cfg := &config.Config{ + Servers: map[string]*config.ServerConfig{ + "mixed": {Tools: []string{"tool_a", "*", "tool_b"}}, + }, + } + sets := buildAllowedToolSets(cfg) + + _, hasMixed := sets["mixed"] + assert.False(t, hasMixed, "server with wildcard in mixed list must not be in the set map") +} + +// TestIsToolAllowed_Wildcard verifies that isToolAllowed returns true for any +// tool name when the server is configured with Tools: ["*"]. +func TestIsToolAllowed_Wildcard(t *testing.T) { + cfg := &config.Config{ + Servers: map[string]*config.ServerConfig{ + "wildcard": {Tools: []string{"*"}}, + }, + } + us := &UnifiedServer{allowedToolSets: buildAllowedToolSets(cfg)} + + assert.True(t, us.isToolAllowed("wildcard", "any_tool")) + assert.True(t, us.isToolAllowed("wildcard", "another_tool")) + assert.True(t, us.isToolAllowed("wildcard", "delete_everything")) +} + +// TestRegisterToolsFromBackend_WildcardAllowsAll verifies that when a backend +// is configured with Tools: ["*"], all tools from the backend are registered. +func TestRegisterToolsFromBackend_WildcardAllowsAll(t *testing.T) { + require := require.New(t) + assert := assert.New(t) + + backend := newMockMCPBackendWithTools(t, "elastic-docs", []string{"search_code", "get_file_contents", "delete_repo"}) + defer backend.Close() + + cfg := &config.Config{ + Servers: map[string]*config.ServerConfig{ + "elastic-docs": { + Type: "http", + URL: backend.URL, + Tools: []string{"*"}, // wildcard — all tools allowed + }, + }, + } + + us, err := NewUnified(context.Background(), cfg) + require.NoError(err) + defer us.Close() + + us.toolsMu.RLock() + defer us.toolsMu.RUnlock() + + assert.Contains(us.tools, "elastic-docs___search_code", "search_code should be registered") + assert.Contains(us.tools, "elastic-docs___get_file_contents", "get_file_contents should be registered") + assert.Contains(us.tools, "elastic-docs___delete_repo", "delete_repo should be registered with wildcard") +} + // ----- helpers ----------------------------------------------------------- // toolNameSet converts a []ToolInfo slice into a name -> bool map for easy lookup. diff --git a/internal/server/call_backend_tool_test.go b/internal/server/call_backend_tool_test.go index 78a39cb21..fa4ce82c1 100644 --- a/internal/server/call_backend_tool_test.go +++ b/internal/server/call_backend_tool_test.go @@ -399,6 +399,8 @@ func TestIsToolAllowed(t *testing.T) { {"empty list allows anything", []string{}, "any_tool", true}, {"tool in list", []string{"a", "b"}, "a", true}, {"tool not in list", []string{"a", "b"}, "c", false}, + {"wildcard allows anything", []string{"*"}, "any_tool", true}, + {"wildcard in mixed list allows anything", []string{"a", "*"}, "z", true}, } for _, tc := range tests { diff --git a/internal/server/unified.go b/internal/server/unified.go index ecf30c461..d397f4ee9 100644 --- a/internal/server/unified.go +++ b/internal/server/unified.go @@ -378,7 +378,8 @@ func newErrorCallToolResult(err error) (*sdk.CallToolResult, interface{}, error) // buildAllowedToolSets converts the per-server Tools lists from the config into pre-computed // map[string]bool sets for O(1) lookup. Servers with no Tools list are not added to the map, -// which signals that all tools are permitted. +// which signals that all tools are permitted. If the Tools list contains a "*" entry anywhere, +// the server is treated the same as having no list (all tools allowed). func buildAllowedToolSets(cfg *config.Config) map[string]map[string]bool { sets := make(map[string]map[string]bool) if cfg == nil { @@ -386,6 +387,11 @@ func buildAllowedToolSets(cfg *config.Config) map[string]map[string]bool { } for serverID, serverCfg := range cfg.Servers { if len(serverCfg.Tools) > 0 { + // Treat "*" anywhere in the list as "allow all" — skip adding to the filter map + if hasWildcard(serverCfg.Tools) { + logger.LogInfo("backend", "[allowed-tools] Wildcard \"*\" configured for %s: allowing all tools", serverID) + continue + } set := make(map[string]bool, len(serverCfg.Tools)) for _, t := range serverCfg.Tools { set[t] = true @@ -396,6 +402,16 @@ func buildAllowedToolSets(cfg *config.Config) map[string]map[string]bool { return sets } +// hasWildcard reports whether the tools list contains a "*" entry. +func hasWildcard(tools []string) bool { + for _, t := range tools { + if t == "*" { + return true + } + } + return false +} + // isToolAllowed reports whether toolName is permitted by the server's configured // allowed-tools list. When no list is configured (empty), all tools are allowed. // Uses the pre-computed allowedToolSets map for O(1) lookup.