fix(mcp): tool-selection UIs stay in exclude mode instead of freezing include lists
Closes the two config-UI halves of the exclude-mode review (GottZ findings 5-7 on #94513): - hermes mcp configure: on an exclude-mode server, unchecking a tool now APPENDS a literal exclude and re-checking drops it — glob patterns are preserved so future vendor tools keep getting filtered. Previously one uncheck converted the whole config to a frozen include list (globs silently deleted, new vendor tools invisible). Re-checked tools still shadowed by a kept glob get an explicit warning instead of a silent no-op. - hermes tools MCP checklist: same exclude-mode write-back, plus display now matches excludes via matches_name_filter (fnmatch) — glob excludes previously rendered as if nothing were excluded. - klaviyo manifest: post_install no longer tells users to append a param the URL already pins; now documents how to get the FULL surface. Live-verified through the real cmd_mcp_configure path: exclude-mode server with ['*_secret_*', 'docs'], uncheck beta + re-check docs -> exclude becomes ['*_secret_*', 'beta'], no include written. 171 tests green across test_mcp_catalog/test_mcp_config/test_mcp_tool.
This commit is contained in:
@@ -1074,9 +1074,55 @@ def cmd_mcp_configure(args):
|
||||
config = load_config()
|
||||
server_entry = cfg_get(config, "mcp_servers", name, default={})
|
||||
|
||||
if len(chosen) == total:
|
||||
exclude_mode = bool(exclude) and isinstance(exclude, list) and not include
|
||||
|
||||
if len(chosen) == total and not exclude_mode:
|
||||
# All selected → remove include/exclude (register all)
|
||||
server_entry.pop("tools", None)
|
||||
elif exclude_mode:
|
||||
# Exclude-mode entry (catalog default_excluded or hand-written
|
||||
# tools.exclude): stay in exclude mode instead of demoting the
|
||||
# user's dynamic filter to a frozen include list. Newly-unchecked
|
||||
# tools are appended as literal excludes; re-checked tools have
|
||||
# their literal entries dropped. Glob patterns are preserved —
|
||||
# they keep excluding future vendor tools by design.
|
||||
old_exclude = [str(p) for p in (exclude or [])]
|
||||
glob_entries = [p for p in old_exclude
|
||||
if "*" in p or "?" in p or "[" in p]
|
||||
literal_entries = {p for p in old_exclude if p not in glob_entries}
|
||||
unchecked = {tool_names[i] for i in range(total) if i not in chosen}
|
||||
checked = {tool_names[i] for i in chosen}
|
||||
|
||||
# Literal excludes: drop re-checked, add newly-unchecked.
|
||||
new_literals = (literal_entries - checked) | {
|
||||
tn for tn in unchecked
|
||||
if not matches_name_filter(tn, set(old_exclude))
|
||||
}
|
||||
new_exclude = glob_entries + sorted(new_literals)
|
||||
|
||||
# A re-checked tool still matched by a kept glob can't be enabled
|
||||
# without dropping the glob — surface that instead of silently
|
||||
# ignoring the click or silently freezing the config.
|
||||
glob_shadowed = sorted(
|
||||
tn for tn in checked
|
||||
if glob_entries and matches_name_filter(tn, set(glob_entries))
|
||||
)
|
||||
if glob_shadowed:
|
||||
_warning(
|
||||
f"{len(glob_shadowed)} re-enabled tool(s) still match glob "
|
||||
f"exclude pattern(s) {glob_entries} and stay excluded: "
|
||||
f"{', '.join(glob_shadowed[:5])}"
|
||||
f"{' ...' if len(glob_shadowed) > 5 else ''}. Remove the "
|
||||
f"pattern from mcp_servers.{name}.tools.exclude in "
|
||||
"config.yaml to enable them."
|
||||
)
|
||||
|
||||
if not new_exclude:
|
||||
server_entry.pop("tools", None)
|
||||
else:
|
||||
server_entry.setdefault("tools", {})
|
||||
server_entry["tools"]["exclude"] = new_exclude
|
||||
server_entry["tools"].pop("include", None)
|
||||
else:
|
||||
chosen_names = [tool_names[i] for i in sorted(chosen)]
|
||||
server_entry.setdefault("tools", {})
|
||||
|
||||
Reference in New Issue
Block a user