diff --git a/pkg/tools/registry.go b/pkg/tools/registry.go index f8f0aa3fd..e90d683bb 100644 --- a/pkg/tools/registry.go +++ b/pkg/tools/registry.go @@ -172,6 +172,13 @@ func (r *ToolRegistry) toolAllowedLocked(name string) bool { if r.allowlist == nil { return true } + if isToolDiscoveryToolName(name) { + // Discovery tools are part of the MCP control plane: they must remain + // available whenever configured so deferred MCP tools can still be + // unlocked. Per-agent allowlists still apply to the hidden MCP tools + // themselves during RegisterHidden. + return true + } _, ok := r.allowlist[strings.ToLower(strings.TrimSpace(name))] return ok } diff --git a/pkg/tools/registry_test.go b/pkg/tools/registry_test.go index f75a321f2..ee63586ab 100644 --- a/pkg/tools/registry_test.go +++ b/pkg/tools/registry_test.go @@ -130,6 +130,25 @@ func TestToolRegistry_AllowlistFiltersRegistrations(t *testing.T) { } } +func TestToolRegistry_AllowlistStillAllowsDiscoveryTools(t *testing.T) { + r := NewToolRegistry() + r.SetAllowlist([]string{"mcp_github_search"}) + + r.Register(newMockTool(BM25SearchToolName, "discover hidden tools")) + r.Register(newMockTool(RegexSearchToolName, "discover hidden tools via regex")) + r.Register(newMockTool("blocked_tool", "blocked")) + + if _, ok := r.Get(BM25SearchToolName); !ok { + t.Fatal("expected BM25 discovery tool to bypass allowlist filtering") + } + if _, ok := r.Get(RegexSearchToolName); !ok { + t.Fatal("expected regex discovery tool to bypass allowlist filtering") + } + if _, ok := r.Get("blocked_tool"); ok { + t.Fatal("blocked_tool should not be registered") + } +} + func TestToolRegistry_HasRegisteredIncludesHiddenTools(t *testing.T) { r := NewToolRegistry() r.SetAllowlist([]string{"visible", "hidden"}) diff --git a/pkg/tools/search_tool.go b/pkg/tools/search_tool.go index c5884c9de..511b81a03 100644 --- a/pkg/tools/search_tool.go +++ b/pkg/tools/search_tool.go @@ -14,6 +14,8 @@ import ( const ( MaxRegexPatternLength = 200 + RegexSearchToolName = "tool_search_tool_regex" + BM25SearchToolName = "tool_search_tool_bm25" ) type RegexSearchTool struct { @@ -27,7 +29,7 @@ func NewRegexSearchTool(r *ToolRegistry, ttl int, maxSearchResults int) *RegexSe } func (t *RegexSearchTool) Name() string { - return "tool_search_tool_regex" + return RegexSearchToolName } func (t *RegexSearchTool) Description() string { @@ -96,7 +98,7 @@ func NewBM25SearchTool(r *ToolRegistry, ttl int, maxSearchResults int) *BM25Sear } func (t *BM25SearchTool) Name() string { - return "tool_search_tool_bm25" + return BM25SearchToolName } func (t *BM25SearchTool) Description() string { @@ -294,6 +296,15 @@ func (t *BM25SearchTool) getOrBuildEngine() *bm25CachedEngine { return cached } +func isToolDiscoveryToolName(name string) bool { + switch strings.ToLower(strings.TrimSpace(name)) { + case BM25SearchToolName, RegexSearchToolName: + return true + default: + return false + } +} + // SearchBM25 ranks hidden tools against query using BM25 via utils.BM25Engine. // This non-cached variant rebuilds the engine on every call. Used by tests // and any code that doesn't hold a BM25SearchTool instance.