fix(mcp): treat tools.include: [] as an explicit empty whitelist

_normalize_name_filter([]) returns an empty set, which is falsy, so
_should_register fell through to "no filter" and registered every tool
— the exact opposite of what _apply_tool_selection wrote when the user
unchecked everything in the install checklist ("contributes nothing
until reconfigured"). Whitelist mode is now keyed on the include key
holding a valid filter shape (str/list/tuple/set) rather than on set
truthiness, at both the live-discovery and cached-manifest sites.
Invalid include values keep the old warn-and-ignore behaviour.
This commit is contained in:
Jan-Stefan Janetzky
2026-08-25 12:03:15 +02:00
committed by Teknium
parent 164e25a936
commit fb1ec36a4b
2 changed files with 30 additions and 4 deletions

View File

@@ -2394,6 +2394,24 @@ class TestMCPSelectiveToolLoading:
)
assert registered == ["mcp__ink__create_service"]
def test_empty_include_registers_nothing(self):
"""include: [] is an explicit empty whitelist, not "no filter".
The install checklist writes include: [] when the user unchecks
every tool ("contributes nothing until reconfigured") — the next
session must not register the full tool surface.
"""
config = {
"url": "https://mcp.example.com",
"tools": {"include": []},
}
registered, _ = self._run_discover(
"ink",
["create_service", "delete_service", "list_services"],
config,
session=SimpleNamespace(),
)
assert registered == []
def test_enabled_false_skips_connection_attempt(self):
from tools.mcp_tool import discover_mcp_tools

View File

@@ -7125,17 +7125,21 @@ def _register_server_tools(name: str, server: MCPServerTask, config: dict) -> Li
# tools.exclude — blacklist: all tools EXCEPT matching ones are registered
# entries may be exact names or fnmatch globs (e.g. "*_radar_*")
# include takes precedence over exclude
# include: [] → register nothing (an explicit empty whitelist, as
# written by the install checklist's "uncheck everything" path)
# Neither set → register all tools (backward-compatible default)
tools_filter = config.get("tools") or {}
include_raw = tools_filter.get("include")
include_set = _normalize_name_filter(
tools_filter.get("include"), f"mcp_servers.{name}.tools.include"
include_raw, f"mcp_servers.{name}.tools.include"
)
include_active = isinstance(include_raw, (str, list, tuple, set))
exclude_set = _normalize_name_filter(
tools_filter.get("exclude"), f"mcp_servers.{name}.tools.exclude"
)
def _should_register(tool_name: str) -> bool:
if include_set:
if include_active:
return matches_name_filter(tool_name, include_set)
if exclude_set:
return not matches_name_filter(tool_name, exclude_set)
@@ -7389,15 +7393,19 @@ def _register_from_cache_sync(name: str, config: dict, entry: dict) -> List[str]
fingerprint = config_fingerprint(config)
tool_timeout = _resolve_tool_timeout(config)
tools_filter = config.get("tools") or {}
include_raw = tools_filter.get("include")
include_set = _normalize_name_filter(
tools_filter.get("include"), f"mcp_servers.{name}.tools.include"
include_raw, f"mcp_servers.{name}.tools.include"
)
# include: [] is an explicit empty whitelist (register nothing) — see the
# live discovery path above for the full filter rules.
include_active = isinstance(include_raw, (str, list, tuple, set))
exclude_set = _normalize_name_filter(
tools_filter.get("exclude"), f"mcp_servers.{name}.tools.exclude"
)
def _should_register(tool_name: str) -> bool:
if include_set:
if include_active:
return matches_name_filter(tool_name, include_set)
if exclude_set:
return not matches_name_filter(tool_name, exclude_set)