mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
feat(mcp): curated exclude list for cloudflare + glob tool filters + default_excluded manifests
The cloudflare entry's 3,320-endpoint surface is ~43% product families a personal/dev account never touches (Zero Trust org-fleet suite, Magic Transit/WAN, Cloudforce One, Radar analytics, API Shield, legacy migration surfaces). Ship a 34-pattern curated exclude list in the manifest: 3,320 -> 1,905 tools kept, and everything Cloudflare adds later stays enabled by default. Mechanism, two small extensions: - tools/mcp_tool.py: tools.include/exclude entries containing glob metacharacters now match via fnmatch (plain names stay exact-match), so a product family is one pattern instead of hundreds of stale literals. - hermes_cli/mcp_catalog.py: manifests may declare tools.default_excluded (mutually exclusive with default_enabled); install writes it to tools.exclude and skips the probe/checklist — a 3,320-row curses checklist is not a UX. Prior user include selections still win on reinstall. Verified by replaying the real filter functions over the live-probed 3,320-tool list: 1,415 excluded, zero overmatch against a per-product target audit; DNS/Workers/R2/D1/tunnels/Access/AI kept.
This commit is contained in:
parent
ce0defe4d8
commit
58c97b9ddd
6 changed files with 295 additions and 14 deletions
|
|
@ -112,6 +112,14 @@ class ToolsSpec:
|
|||
# pre-checked (or no filter is written when probe fails).
|
||||
default_enabled: Optional[List[str]] = None
|
||||
|
||||
# Exclude-mode counterpart: tool names/glob patterns written to
|
||||
# ``mcp_servers.<name>.tools.exclude`` at install time. Everything NOT
|
||||
# matching stays enabled — including tools the server adds later. Use for
|
||||
# huge auto-generated surfaces (OpenAPI-derived MCPs) where an include
|
||||
# list would be thousands of lines and freeze out new endpoints.
|
||||
# Mutually exclusive with ``default_enabled``.
|
||||
default_excluded: Optional[List[str]] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class CatalogEntry:
|
||||
|
|
@ -241,7 +249,22 @@ def _parse_manifest(path: Path) -> CatalogEntry:
|
|||
raise CatalogError(
|
||||
f"{path}: tools.default_enabled must be a list of strings"
|
||||
)
|
||||
tools_spec = ToolsSpec(default_enabled=default_enabled)
|
||||
default_excluded = tools_raw.get("default_excluded")
|
||||
if default_excluded is not None:
|
||||
if not isinstance(default_excluded, list) or not all(
|
||||
isinstance(t, str) for t in default_excluded
|
||||
):
|
||||
raise CatalogError(
|
||||
f"{path}: tools.default_excluded must be a list of strings"
|
||||
)
|
||||
if default_enabled is not None and default_excluded is not None:
|
||||
raise CatalogError(
|
||||
f"{path}: tools.default_enabled and tools.default_excluded are "
|
||||
"mutually exclusive"
|
||||
)
|
||||
tools_spec = ToolsSpec(
|
||||
default_enabled=default_enabled, default_excluded=default_excluded
|
||||
)
|
||||
|
||||
install: Optional[InstallSpec] = None
|
||||
install_raw = data.get("install")
|
||||
|
|
@ -555,6 +578,22 @@ def _write_tools_include(name: str, include: Optional[List[str]]) -> None:
|
|||
save_config(cfg)
|
||||
|
||||
|
||||
def _write_tools_exclude(name: str, exclude: List[str]) -> None:
|
||||
"""Persist ``mcp_servers.<name>.tools.exclude`` (names or glob patterns)."""
|
||||
cfg = load_config()
|
||||
servers = cfg.setdefault("mcp_servers", {})
|
||||
server_entry = servers.get(name) or {}
|
||||
tools_block = server_entry.get("tools") or {}
|
||||
if not isinstance(tools_block, dict):
|
||||
tools_block = {}
|
||||
tools_block["exclude"] = list(exclude)
|
||||
tools_block.pop("include", None)
|
||||
server_entry["tools"] = tools_block
|
||||
servers[name] = server_entry
|
||||
cfg["mcp_servers"] = servers
|
||||
save_config(cfg)
|
||||
|
||||
|
||||
def _apply_tool_selection(
|
||||
entry: CatalogEntry, *, prior_selection: Optional[List[str]]
|
||||
) -> None:
|
||||
|
|
@ -576,6 +615,23 @@ def _apply_tool_selection(
|
|||
"""
|
||||
print()
|
||||
print(color(f" Probing '{entry.name}' for available tools...", Colors.CYAN))
|
||||
|
||||
# Exclude-mode manifests short-circuit the checklist entirely: the curated
|
||||
# exclude list (names or glob patterns) is written as-is, everything else
|
||||
# stays enabled — including tools the server adds later. A reinstall with
|
||||
# a prior include selection still honours the user's own choice below.
|
||||
if entry.tools.default_excluded and prior_selection is None:
|
||||
_write_tools_exclude(entry.name, entry.tools.default_excluded)
|
||||
print(color(
|
||||
f" Applied manifest exclude list "
|
||||
f"({len(entry.tools.default_excluded)} entries); everything else "
|
||||
f"stays enabled. Edit mcp_servers.{entry.name}.tools.exclude in "
|
||||
"config.yaml or run "
|
||||
f"`hermes mcp configure {entry.name}` to change.",
|
||||
Colors.GREEN,
|
||||
))
|
||||
return
|
||||
|
||||
probed = _probe_tools(entry.name)
|
||||
|
||||
# Probe failure path
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue