# NousResearch/hermes-agent · nested subparser dispatch bug causing chat fallback

## Open now

- Nothing open on GitHub.

A bug where nested subparser commands like 'hermes mcp add' fall through to the chat REPL instead of executing correctly, traced to argparse dest collisions and subparser dispatch ordering. Multiple fixes address the dispatch priority and argument naming conflicts.

## TL;DR

- 7 member(s): 0 open issue(s), 2 closed issue(s), 0 open PR(s), 1 merged PR(s), 4 PR(s) closed without merging.
- A fix was merged to main in PR #21204. Verify it addresses your case.
- **Recommended:** read PR #21204 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `partially_resolved` · deciding PR(s): `pr-21204`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_PARTIAL`): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text.
- Members outside that structural core may belong to a different underlying problem.
- Pairwise link density `0.26`: the share of member pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. `1.00` means every pair is linked.

## Closed / duplicate issues

- #4184: \`hermes mcp add\` falls through to chat REPL due to nested subparser dispatch bug
- #19785: \[Bug\]: \`hermes mcp add\` silently launches chat instead of registering MCP server

## Prior & competing PRs

- **#4189: fix: prioritize nested subparser dispatch over chat fallback**. closed without merge · fix tier `suggested`
  - maintainer direction: category `uncategorized`; implication `needs_llm`; refs `#17294`, `#4184`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#18469: fix: prevent nested subparser commands from falling through to chat REPL**. closed without merge · fix tier `suggested`
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `4f364c4e99d46a0c50d3ea1d5ad179f54348f9f7`, `#4189`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#21204: fix(mcp): give 'mcp add --command' a distinct argparse dest**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `hermes_cli/main.py`, `hermes_cli/mcp_config.py`, `scripts/release.py`, `tests/hermes_cli/test_mcp_add_command_dest.py`, `tests/hermes_cli/test_mcp_config.py`
  - fix hunks:
  - `hermes_cli/main.py`

    ```diff
    @@ -10007,7 +10007,15 @@ def cmd_tools(args):
         )
         mcp_add_p.add_argument("name", help="Server name (used as config key)")
         mcp_add_p.add_argument("--url", help="HTTP/SSE endpoint URL")
    -    mcp_add_p.add_argument("--command", help="Stdio command (e.g. npx)")
    +    # dest="mcp_command" so this flag does not clobber the top-level
    +    # subparser's args.command attribute, which the dispatcher reads to
    +    # route to cmd_mcp.  Without an explicit dest, argparse derives
    +    # dest="command" from the flag name and sets it to None when the
    +    # flag is omitted, causing `hermes mcp add ...` to fall through to
    +    # interactive chat.
    +    mcp_add_p.add_argument(
    +        "--command", dest="mcp_command", help="Stdio command (e.g. npx)"
    +    )
         mcp_add_p.add_argument(
             "--args", nargs="*", default=[], help="Arguments for stdio command"
         )
    ```
  - `hermes_cli/mcp_config.py`

    ```diff
    @@ -221,7 +221,10 @@ def cmd_mcp_add(args):
         """Add a new MCP server with discovery-first tool selection."""
         name = args.name
         url = getattr(args, "url", None)
    -    command = getattr(args, "command", None)
    +    # Read from `mcp_command` (set by --command via explicit dest) — see
    +    # mcp_add_p.add_argument("--command", dest="mcp_command", ...) in
    +    # hermes_cli/main.py for why the dest is renamed.
    +    command = getattr(args, "mcp_command", None)
         cmd_args = getattr(args, "args", None) or []
         auth_type = getattr(args, "auth", None)
         preset_name = getattr(args, "preset", None)
    ```
  - `scripts/release.py`

    ```diff
    @@ -41,6 +41,7 @@
     AUTHOR_MAP = {
         # teknium (multiple emails)
         "[redacted:email]": "teknium1",
    +    "[redacted:email]": "discodirector",
         "[redacted:email]": "mikeyobrien",
         "[redacted:email]": "qiyin-code",
         "[redacted:email]": "olisikh",
    ```
  - `tests/hermes_cli/test_mcp_add_command_dest.py`

    ```diff
    @@ -0,0 +1,87 @@
    +"""Regression test: ``hermes mcp add --command`` must not clobber the
    +top-level ``args.command`` subparser dest.
    +
    +The top-level argparse parser uses ``dest="command"`` for its subparsers
    +(``hermes_cli/_parser.py``).  The dispatcher in ``hermes_cli/main.py``
    +reads ``args.command`` to decide which command to run; if it is ``None``
    +it falls through to interactive chat.
    +
    +The ``mcp add`` subparser exposes a ``--command`` flag (the stdio command
    +for an MCP server, e.g. ``npx``).  Without an explicit ``dest=``, argparse
    +derives the dest from the flag name and writes ``args.command = None``
    +when the flag is omitted, overwriting the top-level ``"mcp"`` value.  As a
    +result, ``hermes mcp add foo --url ...`` silently launches chat instead
    +of registering an MCP server.
    +
    +The fix: declare the flag with ``dest="mcp_command"``.  The CLI flag name
    +is unchanged; only the in-memory attribute moves.
    +
    +We replicate the relevant parser shape here rather than importing the
    +real builder, mirroring ``test_argparse_flag_propagation.py`` and
    +``test_subparser_routing_fallback.py``.
    +"""
    +
    +import argparse
    +
    +
    +def _build_parser():
    +    """Minimal replica of the slice of the hermes parser that exhibits
    +    the bug: top-level subparsers (dest="command") and ``mcp add`` with
    +    its ``--command`` flag.
    +    """
    +    parser = argparse.ArgumentParser(prog="hermes")
    +    subparsers = parser.add_subparsers(dest="command")
    +
    +    subparsers.add_parser("chat")
    +
    +    mcp_p = subparsers.add_parser("mcp")
    +    mcp_sub = mcp_p.add_subparsers(dest="mcp_action")
    +
    +    mcp_add = mcp_sub.add_parser("add")
    +    mcp_add.add_argument("name")
    +    mcp_add.add_argument("--url")
    +    mcp_add.add_argument("--command", dest="mcp_command")
    +
    +    return parser
    +
    +
    +class TestMcpAddCommandDest:
    +    def test_url_invocation_preserves_top_level_command(self):
    +        """`hermes mcp add foo --url ...` must keep args.command == "mcp".
    +
    +        Before the dest fix this was clobbered to None, sending the
    +        dispatcher into the chat fallback.
    +        """
    +        parser = _build_parser()
    +        args = parser.parse_args(
    +            ["mcp", "add", "foo", "--url", "https://example.com/mcp"]
    +        )
    +
    +        assert args.command == "mcp"
    +        assert args.mcp_action == "add"
    +        assert args.name == "foo"
    +        assert args.url == "https://example.com/mcp"
    +        assert args.mcp_command is None
    +
    +    def test_command_flag_writes_to_mcp_command_dest(self):
    +        """`--command npx` must populate args.mcp_command, not args.command."""
    +        parser = _build_parser()
    +        args = parser.parse_args(
    +            ["mcp", "add", "github", "--command", "npx"]
    +        )
    +
    +        assert args.command == "mcp"
    +        assert args.mcp_command == "npx"
    +
    +    def test_bare_mcp_add_does_not_clobber_command(self):
    +        """Even without --url or --command, args.command stays "mcp".
    +
    +        Catches the regression at the parser layer regardless of which
    +        transport flag the user passes.
    +        """
    +        parser = _build_parser()
    +        args = parser.parse_args(["mcp", "add", "foo"])
    +
    +        assert args.command == "mcp"
    +        assert args.mcp_command is None
    +        assert args.url is None
    ```
  - `tests/hermes_cli/test_mcp_config.py`

    ```diff
    @@ -43,7 +43,7 @@ def _make_args(**kwargs):
         defaults = {
             "name": "test-server",
             "url": None,
    -        "command": None,
    +        "mcp_command": None,
             "args": None,
             "auth": None,
             "preset": None,
    ```

    ```diff
    @@ -233,7 +233,7 @@ def mock_probe(name, config, **kw):
     
             cmd_mcp_add(_make_args(
                 name="github",
    -            command="npx",
    +            mcp_command="npx",
                 args=["@mcp/github"],
             ))
             out = capsys.readouterr().out
    ```

    ```diff
    @@ -291,7 +291,7 @@ def mock_probe(name, config, **kw):
     
             cmd_mcp_add(_make_args(
                 name="github",
    -            command="npx",
    +            mcp_command="npx",
                 args=["@mcp/github"],
                 env=["MY_API_KEY=[redacted:secret-assignment]", "DEBUG=true"],
             ))
    ```

    ```diff
    @@ -313,7 +313,7 @@ def test_add_stdio_server_rejects_invalid_env_name(self, capsys):
     
             cmd_mcp_add(_make_args(
                 name="github",
    -            command="npx",
    +            mcp_command="npx",
                 args=["@mcp/github"],
                 env=["BAD-NAME=value"],
             ))
    ```

    ```diff
    @@ -390,7 +390,7 @@ def mock_probe(name, config, **kw):
             cmd_mcp_add(_make_args(
                 name="custom",
                 preset="testmcp",
    -            command="uvx",
    +            mcp_command="uvx",
                 args=["custom-server"],
             ))
             out = capsys.readouterr().out
    ```
- **#19787: fix(mcp): give 'mcp add --command' a distinct argparse dest**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `4f364c4e99d46a0c50d3ea1d5ad179f54348f9f7`, `52d924669bc8`, `#21204`
- **#19827: fix(cli): prevent argparse dest collision shadowing 'mcp add'**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `4f364c4e9`, `#21204`, `#19787`

## Fix status

- Merged to main in PR #21204 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-05-07`.
- Subsystem: `hermes_cli`
