diff --git a/hermes_cli/banner.py b/hermes_cli/banner.py index f25d03d2a..dec5aecdb 100644 --- a/hermes_cli/banner.py +++ b/hermes_cli/banner.py @@ -626,6 +626,11 @@ def build_welcome_banner(console: "Console", model: str, cwd: str, f"[dim {dim}]{srv['name']}[/] [{text}]({srv['transport']})[/] " f"[dim {dim}]—[/] [{text}]{srv['tools']} tool(s)[/]" ) + elif srv.get("disabled"): + right_lines.append( + f"[dim {dim}]{srv['name']}[/] [dim]({srv['transport']})[/] " + f"[dim {dim}]— disabled[/]" + ) else: right_lines.append( f"[red]{srv['name']}[/] [dim]({srv['transport']})[/] " diff --git a/tests/hermes_cli/test_banner.py b/tests/hermes_cli/test_banner.py index 9945c78c4..f2bcddeb1 100644 --- a/tests/hermes_cli/test_banner.py +++ b/tests/hermes_cli/test_banner.py @@ -133,3 +133,37 @@ def test_build_welcome_banner_title_falls_back_when_no_tag(): raw = buf.getvalue() assert "Hermes Agent v" in raw, "Version label missing from title" assert "\x1b]8;" not in raw, "OSC-8 hyperlink should not be emitted without a tag" + + +def test_build_welcome_banner_disabled_mcp_shows_disabled_not_failed(): + """A disabled MCP server renders '— disabled' (dim), not '— failed' (red).""" + with ( + patch.object(model_tools, "check_tool_availability", return_value=(["web"], [])), + patch.object(banner, "get_available_skills", return_value={}), + patch.object(banner, "get_update_result", return_value=None), + patch.object( + tools.mcp_tool, + "get_mcp_status", + return_value=[ + {"name": "linear", "transport": "http", "tools": 0, + "connected": False, "disabled": True}, + {"name": "broken", "transport": "stdio", "tools": 0, + "connected": False, "disabled": False}, + ], + ), + ): + console = Console(record=True, force_terminal=False, color_system=None, width=160) + banner.build_welcome_banner( + console=console, model="anthropic/test-model", cwd="/tmp/project", + tools=[{"function": {"name": "read_file"}}], + get_toolset_for_tool=lambda n: "file", + ) + + output = console.export_text() + # Disabled server is labeled "disabled", not "failed" + assert "linear" in output + assert "disabled" in output + # A genuinely unreachable server still reads "failed" + assert "broken" in output + assert "failed" in output + diff --git a/tools/mcp_tool.py b/tools/mcp_tool.py index b15fcdaec..ccfe69d55 100644 --- a/tools/mcp_tool.py +++ b/tools/mcp_tool.py @@ -3666,6 +3666,7 @@ def get_mcp_status() -> List[dict]: for name, cfg in configured.items(): transport = cfg.get("transport", "http") if "url" in cfg else "stdio" + enabled = _parse_boolish(cfg.get("enabled", True), default=True) server = active_servers.get(name) if server and server.session is not None: entry = { @@ -3673,16 +3674,21 @@ def get_mcp_status() -> List[dict]: "transport": transport, "tools": len(server._registered_tool_names) if hasattr(server, "_registered_tool_names") else len(server._tools), "connected": True, + "disabled": False, } if server._sampling: entry["sampling"] = dict(server._sampling.metrics) result.append(entry) else: + # A server with enabled: false is intentionally not connected — it is + # disabled, not failed. Surface that distinction so consumers (banner, + # TUI) can render "disabled" rather than an alarming "failed". result.append({ "name": name, "transport": transport, "tools": 0, "connected": False, + "disabled": not enabled, }) return result