From 23f4db708f2e3b88aacbeae7eb0b168ff92aa93d Mon Sep 17 00:00:00 2001 From: teknium1 <127238744+teknium1@users.noreply.github.com> Date: Fri, 18 Sep 2026 00:51:10 -0700 Subject: [PATCH] test(mcp): pin `hermes mcp test` exit codes; document them Two invariant tests: the handler returns 0/1/3 for connected / connection failed / not in config, and the `hermes mcp` CLI dispatcher forwards that code to `main()` (which already exits on an int return). The MCP guide documents the codes so probes and watchdogs can stop parsing the output. --- tests/hermes_cli/test_mcp_config.py | 29 ++++++++++++++++++++++ website/docs/guides/use-mcp-with-hermes.md | 5 ++++ 2 files changed, 34 insertions(+) diff --git a/tests/hermes_cli/test_mcp_config.py b/tests/hermes_cli/test_mcp_config.py index 1673af08ac..3a291d849d 100644 --- a/tests/hermes_cli/test_mcp_config.py +++ b/tests/hermes_cli/test_mcp_config.py @@ -303,6 +303,35 @@ class TestMcpTest: assert "Connected" in out assert "Tools discovered: 2" in out + def test_exit_codes_distinguish_failure_from_unknown_server(self, tmp_path, capsys, monkeypatch): + """0 connected, 1 connection failed, 3 not in config — never argparse's 2, never a silent 0.""" + _seed_config(tmp_path, {"ink": {"url": "https://mcp.ml.ink/mcp"}}) + from hermes_cli.mcp_config import cmd_mcp_test + + monkeypatch.setattr("hermes_cli.mcp_config._probe_single_server", lambda name, cfg, **kw: []) + assert cmd_mcp_test(_make_args(name="ink")) == 0 + + def failing_probe(name, cfg, **kw): + raise RuntimeError("Server returned an error response") + + monkeypatch.setattr("hermes_cli.mcp_config._probe_single_server", failing_probe) + assert cmd_mcp_test(_make_args(name="ink")) == 1 + assert cmd_mcp_test(_make_args(name="doesnotexist")) == 3 + assert "not found in config" in capsys.readouterr().out + + def test_cli_dispatcher_forwards_test_exit_code(self, tmp_path, monkeypatch): + """``hermes mcp test`` reaches ``main()`` with the handler's code (the dispatcher used to drop it).""" + _seed_config(tmp_path, {"ink": {"url": "https://mcp.ml.ink/mcp"}}) + from hermes_cli.main import cmd_mcp + + def failing_probe(name, cfg, **kw): + raise RuntimeError("boom") + + monkeypatch.setattr("hermes_cli.mcp_config._probe_single_server", failing_probe) + assert cmd_mcp(_make_args(name="ink", mcp_action="test")) == 1 + assert cmd_mcp(_make_args(name="doesnotexist", mcp_action="test")) == 3 + assert cmd_mcp(_make_args(mcp_action="list")) is None + def test_probe_uses_configured_connect_timeout(self, monkeypatch): """OAuth-capable probes must not hard-code a short 30s timeout.""" import asyncio diff --git a/website/docs/guides/use-mcp-with-hermes.md b/website/docs/guides/use-mcp-with-hermes.md index 6b8eee4a59..f4aaadcb51 100644 --- a/website/docs/guides/use-mcp-with-hermes.md +++ b/website/docs/guides/use-mcp-with-hermes.md @@ -83,6 +83,11 @@ You can verify MCP in a few ways: - ask Hermes what tools it has available - use `/reload-mcp` after config changes - check logs if the server failed to connect +- run `hermes mcp test ` from a shell — it connects, lists the + discovered tools, and exits `0` on a completed connect, `1` when the + connection fails, and `3` when the server is not in your config (`2` is + argparse's usage error), so health probes and cron watchdogs can branch on + `$?` instead of parsing the output A practical test prompt: