refactor(hermes_cli): mcp_picker routes output through catalog _say; _apply_request_chain returns the result; catalog probe-fail ladder tabled

This commit is contained in:
Teknium
2026-09-02 21:02:18 -07:00
parent 5e595c0121
commit 61036c0e65
3 changed files with 51 additions and 64 deletions

View File

@@ -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:

View File

@@ -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 <name> Picker: hermes mcp", Colors.DIM))
_say(" Install: hermes mcp install <name> 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

View File

@@ -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(