# NousResearch/hermes-agent · MCP server header configuration and ${ENV} placeholder handling

## Open now

- 1 issue, 0 PRs open on GitHub.
- #30320: \[Bug\]: specific mcp url header can t be added easily

Members center on configuring Hermes MCP servers with custom headers (e.g., for n8n), including a report that adding a specific URL header is difficult. Two pull requests address resolving ${ENV}/${VAR} placeholders in headers before the discovery probe; their outcomes are not stated here.

## TL;DR

- 4 member(s): 1 open issue(s), 1 closed issue(s), 0 open PR(s), 1 merged PR(s), 1 PR(s) closed without merging.
- A fix was merged to main in PR #38571. Verify it addresses your case.
- **Recommended:** read PR #38571 (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-38571`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_SPANNING`): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
- Pairwise link density `0.56`: 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.

## Open related issues

- #30320: \[Bug\]: specific mcp url header can t be added easily

## Closed / duplicate issues

- #37792: Identified reproducible Hermes MCP issue while configuring n8n MCP server

## Prior & competing PRs

- **#38571: fix(mcp): resolve ${ENV} in discovery probe so header auth works**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`
  - files: `hermes_cli/mcp_config.py`, `scripts/release.py`, `tests/hermes_cli/test_mcp_config.py`
  - fix hunks:
  - `hermes_cli/mcp_config.py`

    ```diff
    @@ -109,6 +109,21 @@ def _env_key_for_server(name: str) -> str:
         return f"MCP_{name.upper().replace('-', '_')}_API_KEY"
     
     
    +def _strip_bearer_prefix(token: str) -> str:
    +    """Strip a leading ``Bearer `` from a pasted token.
    +
    +    The header template stores ``Authorization: Bearer ${MCP_X_API_KEY}``, so
    +    if a user pastes a token that already includes the ``Bearer `` prefix the
    +    server receives ``Bearer Bearer <jwt>`` → 401. Normalize on save. (#37792)
    +    """
    +    if not isinstance(token, str):
    +        return token
    +    stripped = token.strip()
    +    if stripped[:7].lower() == "bearer ":
    +        return stripped[7:].strip()
    +    return stripped
    +
    +
     def _parse_env_assignments(raw_env: Optional[List[str]]) -> Dict[str, str]:
         """Parse ``KEY=VALUE`` strings from CLI args into an env dict."""
         parsed: Dict[str, str] = {}
    ```

    ```diff
    @@ -164,6 +179,27 @@ def _apply_mcp_preset(
     
     # ─── Discovery (temporary connect) ───────────────────────────────────────────
     
    +def _resolve_mcp_server_config(config: dict) -> dict:
    +    """Resolve ``${ENV}`` placeholders in a server config before connecting.
    +
    +    Mirrors ``_load_mcp_config()`` in ``tools/mcp_tool.py``: load
    +    ``~/.hermes/.env`` into ``os.environ`` and recursively interpolate any
    +    ``${VAR}`` placeholders. The CLI builds header templates like
    +    ``Authorization: Bearer ${MCP_X_API_KEY}`` but the probe path never
    +    resolved them, so the discovery probe sent the literal placeholder and
    +    auth-requiring servers (e.g. n8n) returned 401 — while runtime tool
    +    loading worked because it interpolates. (#37792)
    +    """
    +    from tools.mcp_tool import _interpolate_env_vars
    +
    +    try:
    +        from hermes_cli.env_loader import load_hermes_dotenv
    +        load_hermes_dotenv()
    +    except Exception:  # pragma: no cover — defensive
    +        pass
    +    return _interpolate_env_vars(config)
    +
    +
     def _probe_single_server(
         name: str, config: dict, connect_timeout: float = 30
     ) -> List[Tuple[str, str]]:
    ```

    ```diff
    @@ -179,6 +215,8 @@ def _probe_single_server(
             _stop_mcp_loop,
         )
     
    +    config = _resolve_mcp_server_config(config)
    +
         _ensure_mcp_loop()
     
         tools_found: List[Tuple[str, str]] = []
    ```

    ```diff
    @@ -340,6 +378,7 @@ def cmd_mcp_add(args):
                     else:
                         api_key = _prompt("API key / Bearer token", password=True)
                         if api_key:
    +                        api_key = [redacted:secret-assignment]
                             save_env_value(env_key, api_key)
                             _success(f"Saved to {display_hermes_home()}/.env as {env_key}")
     
    ```
  - `scripts/release.py`

    ```diff
    @@ -46,6 +46,7 @@
     # Auto-extracted from noreply emails + manual overrides
     AUTHOR_MAP = {
         "[redacted:email]": "vladkvlchk",
    +    "116314616+ThyFriendlyFox@users.noreply.github.com": "ThyFriendlyFox",
         "[redacted:email]": "truenorth-lj",
         "16943149+nepenth@users.noreply.github.com": "nepenth",
         "[redacted:email]": "benfrank241",
    ```
  - `tests/hermes_cli/test_mcp_config.py`

    ```diff
    @@ -483,6 +483,102 @@ def test_interpolate_non_string(self):
             assert _interpolate_env_vars(None) is None
     
     
    +# ---------------------------------------------------------------------------
    +# Tests: probe-path env resolution (#37792)
    +# ---------------------------------------------------------------------------
    +
    +class TestProbeEnvResolution:
    +    """The probe path must resolve ``${ENV}`` before connecting, so the
    +    discovery probe behaves like runtime tool loading. Regression for #37792
    +    where `hermes mcp add --auth header` sent a literal
    +    ``Authorization: Bearer ${MCP_X_API_KEY}`` and got 401."""
    +
    +    def test_resolve_interpolates_header(self, monkeypatch):
    +        from hermes_cli.mcp_config import _resolve_mcp_server_config
    +
    +        monkeypatch.setenv("MCP_N8N_API_KEY", "jwt-token-xyz")
    +        resolved = _resolve_mcp_server_config({
    +            "url": "http://localhost:5678/mcp-server/http",
    +            "headers": {"Authorization": "Bearer ${MCP_N8N_API_KEY}"},
    +        })
    +        assert resolved["headers"]["Authorization"] == "Bearer jwt-token-xyz"
    +
    +    def test_resolve_leaves_unset_var_literal(self, monkeypatch):
    +        from hermes_cli.mcp_config import _resolve_mcp_server_config
    +
    +        monkeypatch.delenv("MCP_UNSET_API_KEY", raising=False)
    +        resolved = _resolve_mcp_server_config({
    +            "headers": {"Authorization": "Bearer ${MCP_UNSET_API_KEY}"},
    +        })
    +        # Unresolved placeholder stays literal (no crash) — matches
    +        # _interpolate_env_vars semantics.
    +        assert resolved["headers"]["Authorization"] == "Bearer ${MCP_UNSET_API_KEY}"
    +
    +    def test_probe_resolves_before_connect(self, monkeypatch):
    +        """_probe_single_server must pass the RESOLVED config to _connect_server."""
    +        import hermes_cli.mcp_config as mc
    +
    +        monkeypatch.setenv("MCP_N8N_API_KEY", "jwt-token-xyz")
    +
    +        seen = {}
    +
    +        class _FakeTool:
    +            name = "do_thing"
    +            description = "a tool"
    +
    +        class _FakeServer:
    +            _tools = [_FakeTool()]
    +
    +            async def shutdown(self):
    +                return None
    +
    +        async def _fake_connect(name, config):
    +            seen["config"] = config
    +            return _FakeServer()
    +
    +        monkeypatch.setattr("tools.mcp_tool._connect_server", _fake_connect)
    +
    +        tools = mc._probe_single_server("n8n", {
    +            "url": "http://localhost:5678/mcp-server/http",
    +            "headers": {"Authorization": "Bearer ${MCP_N8N_API_KEY}"},
    +        })
    +
    +        assert tools == [("do_thing", "a tool")]
    +        assert seen["config"]["headers"]["Authorization"] == "Bearer jwt-token-xyz"
    +
    +
    +class TestStripBearerPrefix:
    +    """Pasted tokens that already include ``Bearer `` would otherwise produce
    +    ``Bearer Bearer <jwt>`` once the header template adds its own prefix."""
    +
    +    def test_bare_token_unchanged(self):
    +        from hermes_cli.mcp_config import _strip_bearer_prefix
    +
    +        assert _strip_bearer_prefix("eyJabc123") == "eyJabc123"
    +
    +    def test_strips_bearer_prefix(self):
    +        from hermes_cli.mcp_config import _strip_bearer_prefix
    +
    +        assert _strip_bearer_prefix("Bearer eyJabc123") == "eyJabc123"
    +
    +    def test_strips_case_insensitive_and_whitespace(self):
    +        from hermes_cli.mcp_config import _strip_bearer_prefix
    +
    +        assert _strip_bearer_prefix("bearer eyJabc123") == "eyJabc123"
    +        assert _strip_bearer_prefix("  Bearer   eyJabc123  ") == "eyJabc123"
    +
    +    def test_does_not_strip_without_space(self):
    +        from hermes_cli.mcp_config import _strip_bearer_prefix
    +
    +        # "BearerToken" is a token that happens to start with "Bearer", not a prefix.
    +        assert _strip_bearer_prefix("BearerToken") == "BearerToken"
    +
    +    def test_non_string_passthrough(self):
    +        from hermes_cli.mcp_config import _strip_bearer_prefix
    +
    +        assert _strip_bearer_prefix(None) is None  # type: ignore[arg-type]
    +
    +
     # ---------------------------------------------------------------------------
     # Tests: config helpers
     # ---------------------------------------------------------------------------
    ```
- **#37926: fix(mcp): resolve ${VAR} header placeholders before the discovery probe**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `b0a52d74ac44615c228284b43550e768580882ef`, `#38571`, `#37792`

## Fix status

- Merged to main in PR #38571 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-06-04`.
- Subsystem: `mcp`
