From 61036c0e65005fe7f4eaa93a973f65c4dec13fcb Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Wed, 2 Sep 2026 21:02:18 -0700 Subject: [PATCH] refactor(hermes_cli): mcp_picker routes output through catalog _say; _apply_request_chain returns the result; catalog probe-fail ladder tabled --- hermes_cli/mcp_catalog.py | 44 ++++++++++++++++-------------------- hermes_cli/mcp_picker.py | 47 ++++++++++++++++++--------------------- hermes_cli/middleware.py | 24 +++++++++----------- 3 files changed, 51 insertions(+), 64 deletions(-) diff --git a/hermes_cli/mcp_catalog.py b/hermes_cli/mcp_catalog.py index 99f12776e9..3ba21d7c36 100644 --- a/hermes_cli/mcp_catalog.py +++ b/hermes_cli/mcp_catalog.py @@ -259,16 +259,10 @@ def _parse_manifest(path: Path) -> CatalogEntry: suggest = _parse_suggest(path, data.get("suggest")) install = _parse_install(path, data.get("install")) return CatalogEntry( - name=name, - description=description, - source=str(data.get("source") or "").strip(), - transport=transport, - auth=auth, - tools=tools, - install=install, - post_install=str(data.get("post_install") or ""), - suggest=suggest, - manifest_path=path) + name=name, description=description, source=str(data.get("source") or "").strip(), + transport=transport, auth=auth, tools=tools, install=install, + post_install=str(data.get("post_install") or ""), suggest=suggest, manifest_path=path, + ) # Populated by list_catalog(); inspected by the picker / catalog UIs so the user gets actionable @@ -538,7 +532,8 @@ def _apply_tool_selection( _write_tools_filter(name, "exclude", entry.tools.default_excluded) _say( f" Applied manifest exclude list ({len(entry.tools.default_excluded)} entries); " - f"everything else stays enabled. {edit_hint}") + f"everything else stays enabled. {edit_hint}" + ) return _say(f" Probing '{name}' for available tools...", Colors.CYAN) @@ -550,24 +545,23 @@ def _apply_tool_selection( manifest_default = entry.tools.default_enabled refine_hint = f"Run {configure_hint} after the server is reachable to refine." if prior_selection is not None: - _write_tools_filter(name, "include", prior_selection) - msg = ( - f" Couldn't probe server. Kept your previous tool selection " - f"({len(prior_selection)} tools). {refine_hint}") + mode, values = "include", prior_selection + msg = f"Kept your previous tool selection ({len(prior_selection)} tools). {refine_hint}" elif prior_exclude is not None: - _write_tools_filter(name, "exclude", prior_exclude) - msg = f" Couldn't probe server. Kept your existing exclude list ({len(prior_exclude)} entries)." + mode, values = "exclude", prior_exclude + msg = f"Kept your existing exclude list ({len(prior_exclude)} entries)." elif manifest_default: - _write_tools_filter(name, "include", manifest_default) - msg = ( - f" Couldn't probe server. Applied manifest default " - f"({len(manifest_default)} tools). {refine_hint}") + mode, values = "include", manifest_default + msg = f"Applied manifest default ({len(manifest_default)} tools). {refine_hint}" else: - _write_tools_filter(name, "include", None) + mode, values = "include", None msg = ( - " Couldn't probe server; installed with no tool filter (all tools enabled when " - f"reachable). Run {configure_hint} after first connect to prune.") - _say(msg, Colors.YELLOW) + "installed with no tool filter (all tools enabled when " + f"reachable). Run {configure_hint} after first connect to prune." + ) + _write_tools_filter(name, mode, values) + sep = ";" if values is None else "." + _say(f" Couldn't probe server{sep} {msg}", Colors.YELLOW) return if not probed: diff --git a/hermes_cli/mcp_picker.py b/hermes_cli/mcp_picker.py index 66477ad3d0..ea957a022d 100644 --- a/hermes_cli/mcp_picker.py +++ b/hermes_cli/mcp_picker.py @@ -6,12 +6,12 @@ import sys from dataclasses import dataclass from typing import List, Optional -from hermes_cli.colors import Colors, color +from hermes_cli.colors import Colors from hermes_cli.cli_output import prompt_yes_no from hermes_cli.curses_ui import curses_single_select from hermes_cli.mcp_catalog import ( CatalogEntry, CatalogError, catalog_diagnostics, install_entry, is_enabled, is_installed, - list_catalog, installed_servers, remove_server, server_enabled, uninstall_entry, + list_catalog, installed_servers, remove_server, server_enabled, uninstall_entry, _say, ) from hermes_cli.config import load_config, save_config @@ -71,15 +71,15 @@ def _enable_disable(name: str, *, enable: bool) -> None: servers = cfg.get("mcp_servers") or {} server = servers.get(name) if not server: - print(color(f" '{name}' is not installed.", Colors.RED)) + _say(f" '{name}' is not installed.", Colors.RED) return server["enabled"] = enable cfg["mcp_servers"] = servers save_config(cfg) - print(color( + _say( f" ✓ '{name}' {'enabled' if enable else 'disabled'}. " - "Start a new Hermes session for changes to take effect.", - Colors.GREEN)) + "Start a new Hermes session for changes to take effect." + ) def _configure_tools(name: str) -> None: @@ -93,12 +93,12 @@ def _configure_tools(name: str) -> None: def _remove_custom(name: str) -> None: """Remove a non-catalog MCP entry from config.yaml.""" if not is_installed(name): - print(color(f" '{name}' is not configured.", Colors.RED)) + _say(f" '{name}' is not configured.", Colors.RED) return if not prompt_yes_no(f"Remove '{name}' from mcp_servers?", default=False): return remove_server(name) - print(color(f" ✓ Removed '{name}'", Colors.GREEN)) + _say(f" ✓ Removed '{name}'") def _install(entry: CatalogEntry, verb: str) -> bool: @@ -106,7 +106,7 @@ def _install(entry: CatalogEntry, verb: str) -> bool: try: install_entry(entry, enable=True) except CatalogError as exc: - print(color(f" ✗ {verb} failed: {exc}", Colors.RED)) + _say(f" ✗ {verb} failed: {exc}", Colors.RED) return False return True @@ -115,12 +115,12 @@ def _uninstall(name: str) -> None: if not prompt_yes_no(f"Uninstall '{name}'?", default=False): return if uninstall_entry(name): - print(color( + _say( f" ✓ Uninstalled '{name}'. " - "Credentials in .env preserved — delete manually if no longer needed.", - Colors.GREEN)) + "Credentials in .env preserved — delete manually if no longer needed." + ) else: - print(color(f" '{name}' was not installed", Colors.DIM)) + _say(f" '{name}' was not installed", Colors.DIM) def _run_submenu(title: str, actions: list) -> None: @@ -148,7 +148,7 @@ def _handle_row(row: _Row) -> None: return # Catalog row, installed + enabled print() - print(color(f" '{row.name}' is already enabled.", Colors.DIM)) + _say(f" '{row.name}' is already enabled.", Colors.DIM) _run_submenu(f"Action for '{row.name}'", [ ("Configure tools (probe server + re-pick)", lambda: _configure_tools(row.name)), ("Disable (keep config, stop loading on next session)", @@ -162,27 +162,24 @@ def _print_rows_text(rows: List[_Row]) -> None: """Plain-text catalog dump: `hermes mcp catalog` output and the non-curses fallback.""" print() if not rows: - print(color(" No MCPs in the catalog or configured.", Colors.DIM)) + _say(" No MCPs in the catalog or configured.", Colors.DIM) print() return - print(color(" MCP Catalog + configured servers:", Colors.CYAN + Colors.BOLD)) + _say(" MCP Catalog + configured servers:", Colors.CYAN + Colors.BOLD) print() print(f" {'Name':<18} {'Status':<24} Description") print(f" {'-' * 18} {'-' * 24} {'-' * 11}") for row in rows: print(f" {_format_row(row)}") print() - print(color(" Install: hermes mcp install Picker: hermes mcp", Colors.DIM)) + _say(" Install: hermes mcp install Picker: hermes mcp", Colors.DIM) # Manifest-version warnings: the user's Hermes is too old to install everything listed. future = [d for d in catalog_diagnostics() if d[1] == "future_manifest"] if future: print() for name, _, _msg in future: - print(color( - f" ⚠ '{name}' requires a newer Hermes — run `hermes update` " - "to install this entry.", - Colors.YELLOW)) + _say(f" ⚠ '{name}' requires a newer Hermes — run `hermes update` to install this entry.", Colors.YELLOW) print() print() @@ -215,9 +212,9 @@ def install_by_name(identifier: str) -> int: entry = get_entry(identifier) if entry is None: - print(color( - f" ✗ '{identifier}' is not in the catalog. " - "Run `hermes mcp catalog` to see available entries.", - Colors.RED)) + _say( + f" ✗ '{identifier}' is not in the catalog. Run `hermes mcp catalog` to see available entries.", + Colors.RED, + ) return 1 return 0 if _install(entry, "install") else 1 diff --git a/hermes_cli/middleware.py b/hermes_cli/middleware.py index 5f4e24df61..d0f8a81e1c 100644 --- a/hermes_cli/middleware.py +++ b/hermes_cli/middleware.py @@ -61,8 +61,8 @@ def _safe_copy(payload: Any) -> Any: def _apply_request_chain( - kind: str, payload_key: str, trace: List[Dict[str, Any]], **kwargs: Any -) -> Dict[str, Any]: + kind: str, payload_key: str, trace: List[Dict[str, Any]], original: Any, **kwargs: Any +) -> RequestMiddlewareResult: """Feed ``kwargs[payload_key]`` through every ``kind`` middleware; each may return ``{payload_key: {...}}``.""" from hermes_cli.plugins import invoke_middleware @@ -79,7 +79,7 @@ def _apply_request_chain( for key in ("source", "reason", "name") if isinstance(value := result.get(key), str) and value} trace.append(entry or {"source": "plugin"}) - return current + return RequestMiddlewareResult(payload=current, original_payload=original, changed=bool(trace), trace=trace) def apply_llm_request_middleware(request: Dict[str, Any], **context: Any) -> RequestMiddlewareResult: @@ -90,12 +90,9 @@ def apply_llm_request_middleware(request: Dict[str, Any], **context: Any) -> Req return RequestMiddlewareResult(payload=request, original_payload=request) original_request = _safe_copy(request) - trace: List[Dict[str, Any]] = [] - current_request = _apply_request_chain( - LLM_REQUEST_MIDDLEWARE, "request", trace, - request=_safe_copy(original_request), original_request=original_request, **context) - return RequestMiddlewareResult( - payload=current_request, original_payload=original_request, changed=bool(trace), trace=trace, + return _apply_request_chain( + LLM_REQUEST_MIDDLEWARE, "request", [], original_request, + request=_safe_copy(original_request), original_request=original_request, **context, ) @@ -126,11 +123,10 @@ def apply_tool_request_middleware( payload=args if not trace else current_args, original_payload=args, changed=bool(trace), trace=trace, ) - current_args = _apply_request_chain( - TOOL_REQUEST_MIDDLEWARE, "args", trace, - tool_name=tool_name, args=current_args, original_args=original_args, **context) - return RequestMiddlewareResult( - payload=current_args, original_payload=original_args, changed=bool(trace), trace=trace) + return _apply_request_chain( + TOOL_REQUEST_MIDDLEWARE, "args", trace, original_args, + tool_name=tool_name, args=current_args, original_args=original_args, **context, + ) def run_llm_execution_middleware(