From 861ca18c672fb83aaffd5c91d830194ca6f44891 Mon Sep 17 00:00:00 2001 From: JonthanaHanh <92574114+JonthanaHanh@users.noreply.github.com> Date: Mon, 3 Aug 2026 10:07:37 +0530 Subject: [PATCH] fix(catalog): wire api_key auth headers for http MCP servers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When an optional-mcps manifest declares transport.type=http with auth.type=api_key, install_entry() prompts for the key and saves it to .env, but _build_server_config() only handled the oauth case — the api_key case produced a bare url entry with no headers, so every request to the server was unauthenticated (-> 401). Reuse _bearer_auth_headers(entry.name) from mcp_config.py so the catalog path emits the same 'Authorization: Bearer ${MCP_..._API_KEY}' template as the manual 'hermes mcp add --url' path. Salvaged from #70782 (production hunk applied clean; tests re-anchored onto current main). Credit: JonthanaHanh. --- hermes_cli/mcp_catalog.py | 4 +++ tests/hermes_cli/test_mcp_catalog.py | 38 ++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/hermes_cli/mcp_catalog.py b/hermes_cli/mcp_catalog.py index 6f8c9b30c6..3b0e5d25fb 100644 --- a/hermes_cli/mcp_catalog.py +++ b/hermes_cli/mcp_catalog.py @@ -506,6 +506,10 @@ def _build_server_config( cfg["url"] = t.url if entry.auth.type == "oauth": cfg["auth"] = "oauth" + elif entry.auth.type == "api_key": + from hermes_cli.mcp_config import _bearer_auth_headers + + cfg["headers"] = _bearer_auth_headers(entry.name) return cfg diff --git a/tests/hermes_cli/test_mcp_catalog.py b/tests/hermes_cli/test_mcp_catalog.py index a5b465dd80..1e3a78850b 100644 --- a/tests/hermes_cli/test_mcp_catalog.py +++ b/tests/hermes_cli/test_mcp_catalog.py @@ -143,6 +143,21 @@ class TestManifestParsing: assert e.auth.env[1].required is False assert e.auth.env[1].secret is False + def test_http_api_key_builds_bearer_headers_template(self, catalog_dir): + body = _basic_manifest( + transport={"type": "http", "url": "https://mcp.example.com/sse"}, + auth={ + "type": "api_key", + "env": [{"name": "MCP_DEMO_API_KEY", "prompt": "key", "secret": True}], + }, + ) + _write_manifest(catalog_dir, "demo", body) + from hermes_cli.mcp_catalog import _build_server_config + + cfg = _build_server_config(_entry("demo"), None) + assert cfg["url"] == "https://mcp.example.com/sse" + assert cfg["headers"] == {"Authorization": "Bearer ${MCP_DEMO_API_KEY}"} + @@ -193,6 +208,29 @@ class TestInstall: assert get_env_value("DEMO_KEY") == "secret-val" assert "demo" in load_config()["mcp_servers"] + def test_install_http_api_key_writes_bearer_headers(self, catalog_dir, monkeypatch): + body = _basic_manifest( + transport={"type": "http", "url": "https://mcp.example.com/sse"}, + auth={ + "type": "api_key", + "env": [{"name": "MCP_DEMO_API_KEY", "prompt": "key", "secret": True}], + }, + ) + _write_manifest(catalog_dir, "demo", body) + + from hermes_cli import mcp_catalog + + monkeypatch.setattr(mcp_catalog, "_prompt_input", lambda *a, **kw: "secret-val") + + from hermes_cli.mcp_catalog import install_entry + from hermes_cli.config import load_config + + install_entry(_entry("demo"), enable=True) + + server = load_config()["mcp_servers"]["demo"] + assert server["url"] == "https://mcp.example.com/sse" + assert server["headers"] == {"Authorization": "Bearer secret-val"} +