# NousResearch/hermes-agent · MCP server headers, env vars, and auth fixes

## Open now

- 4 issues, 3 PRs open on GitHub.
- #25478: \[Docs\]: MCP user-guide misses required X-Workspace-slug header for Plane MCP — server returns 400 without it
- #30320: \[Bug\]: specific mcp url header can t be added easily
- #53646: fix(mcp): load per-server env_file before CLI probe interpolation
- #61562: \[Bug\]: MCP UI Token
- PR #53647: fix(mcp): load per-server env_file before probe interpolation
- PR #62306: docs(mcp): show provider-specific request headers
- PR #80975: fix(mcp): append token query param for HTTP/SSE and redact URLs in dashboard (#61562)

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.

## TL;DR

- 14 member(s): 4 open issue(s), 1 closed issue(s), 3 open PR(s), 1 merged PR(s), 5 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_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.13`: 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

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

## 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`; implication `superseded`
  - 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 = [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`

    ```diff
    @@ -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`

    ```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
     # ---------------------------------------------------------------------------
    ```
- **#26303: docs(mcp): show multi-header HTTP config**. closed without merge
  - maintainer direction: category `silent_close`; implication `active`
- **#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`
- **#53647: fix(mcp): load per-server env_file before probe interpolation**. open
  - maintainer direction: category `uncategorized`; implication `needs_llm`
  - files: `hermes_cli/mcp_config.py`, `tests/hermes_cli/test_mcp_config.py`
- **#61599: fix(web,mcp): add headers support to MCP server create API and dashboard UI**. closed without merge
  - maintainer direction: category `silent_close`; implication `active`
- **#61602: fix(web,mcp): add headers support to MCP server create API and dashboard UI**. closed without merge
  - maintainer direction: category `stale_abandoned`; implication `active`
- **#62306: docs(mcp): show provider-specific request headers**. open
  - maintainer direction: category `uncategorized`; implication `needs_llm`
  - files: `website/docs/user-guide/features/mcp.md`
- **#74809: fix(mcp): isolate per-server env_file resolution**. closed without merge
  - maintainer direction: category `partial_fix`; implication `needs_llm`; refs `46b8ebefbaaa0542bbe8a7c16a0b43403d8a9559`, `22c5684b983eac6a81ee015ae80296c4b3dbf5bb`, `a688e7d5f`
- **#80975: fix(mcp): append token query param for HTTP/SSE and redact URLs in dashboard (#61562)**. open
  - maintainer direction: category `uncategorized`; implication `needs_llm`; refs `c6bf6e75c`
  - files: `hermes_cli/mcp_config.py`, `hermes_cli/subcommands/mcp.py`, `hermes_cli/web_routers/mcp.py`, `hermes_cli/web_server_mcp.py`, `hermes_cli/web_server_profiles.py`, `tests/hermes_cli/test_dashboard_admin_endpoints.py`, `tests/hermes_cli/test_mcp_config.py`, `tests/hermes_cli/test_web_server.py`, `tests/hermes_cli/test_web_server_mcp_url_redaction.py`, `tests/tools/test_mcp_token_query.py`, `tools/mcp_tool_config.py`, `tools/mcp_tool_errors.py`, `tools/mcp_tool_registration.py`, `tools/mcp_tool_server_run.py`, `tools/mcp_tool_transport.py`, `web/src/lib/api.ts`, `web/src/lib/mcp-server-create.test.ts`, `web/src/lib/mcp-server-create.ts`, `web/src/pages/McpPage.tsx`, `web/src/pages/ProfileBuilderPage.tsx`

## 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: `docs`
