MCP server headers, env vars, and auth fixes

NousResearch/hermes-agent · cluster 25478 · docs

Data as of

7 open items. Fix merged in #38571. Check it covers your case.

The short answer

4open issues
3open PRs
14items grouped
Known?
Yes. 14 related items grouped here, 1 issue already closed.
Fix event
A fix was merged to main in PR #38571. Check it covers your case. No release data is tracked.
Tracker
The maintainers' tracker marks this partially resolved, deciding PR #38571. That is separate from the fix event.
Still open
4 issues and 3 PRs.

This group covers fixes and documentation around MCP server configuration, specifically HTTP header support, environment variable resolution before discovery probes, per-server env_file isolation, and authenticated HTTP server handling in the dashboard and API.

Open issues4

  • Issue#25478[Docs]: MCP user-guide misses required X-Workspace-slug header for Plane MCP — server returns 400 without it
  • Issue#30320[Bug]: specific mcp url header can t be added easily
  • Issue#53646fix(mcp): load per-server env_file before CLI probe interpolation
  • Issue#61562[Bug]: MCP UI Token

Open PRs3

  • PR#53647fix(mcp): load per-server env_file before probe interpolation
  • PR#62306docs(mcp): show provider-specific request headers
  • PR#80975fix(mcp): append token query param for HTTP/SSE and redact URLs in dashboard (#61562)

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #38571 fix(mcp): resolve ${ENV} in discovery probe so header auth works

    merged to main

    files: hermes_cli/mcp_config.py, scripts/release.py, tests/hermes_cli/test_mcp_config.py

    Show the diff
    • hermes_cli/mcp_config.py
      @@ -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] = {}
      @@ -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]]:
      @@ -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]] = []
      @@ -340,6 +378,7 @@ def cmd_mcp_add(args):
                       else:
                           api_key = [redacted:secret-assignment]"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
      @@ -46,6 +46,7 @@
       # Auto-extracted from noreply emails + manual overrides
       AUTHOR_MAP = {
           "[redacted:email]": "vladkvlchk",
      +    "[redacted:email]": "ThyFriendlyFox",
           "[redacted:email]": "truenorth-lj",
           "[redacted:email]": "nepenth",
           "[redacted:email]": "benfrank241",
    • tests/hermes_cli/test_mcp_config.py
      @@ -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
       # ---------------------------------------------------------------------------

5 PRs closed without merging

From the close comments: closed without comment 2 · done elsewhere 1 · partial fix 1 · stale or abandoned 1

  • #26303 docs(mcp): show multi-header HTTP config · closed without comment
  • #37926 fix(mcp): resolve ${VAR} header placeholders before the discovery probe · done elsewhere → b0a52d7, #38571, #37792
  • #61599 fix(web,mcp): add headers support to MCP server create API and dashboard UI · closed without comment
  • #61602 fix(web,mcp): add headers support to MCP server create API and dashboard UI · stale or abandoned
  • #74809 fix(mcp): isolate per-server env_file resolution · partial fix → 46b8ebe, 22c5684, a688e7d
Closed issues · 1
  • Closed#37792Identified reproducible Hermes MCP issue while configuring n8n MCP server
How this cluster was grouped
  • Link evidence (partly structural links): 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.13: the share of item 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.

Pick up a Lab task

Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.

Open the Lab

At a glance

Subsystem
docs
Merged PRs
#38571
Closed issues
1
Attempts
5 PRs closed without merging
Fix link checked
4 Jun 2026

For your agent

The same facts, typed.