From dd98cd73090a24db2d29a2165dcf38129e8eef78 Mon Sep 17 00:00:00 2001 From: Million <15158090088@163.com> Date: Tue, 11 Aug 2026 14:45:51 +0800 Subject: [PATCH 1/3] fix(config): expand env vars in MCP HTTP/SSE header values Expand ${VAR} placeholders in SSEMCPServerConfig and StreamableHTTPMCPServerConfig header values via os.path.expandvars before transport construction, in both to_transport() and the session pool's _create_transport(). Without this, authenticated MCP servers receiving 'Bearer ${TOKEN}' literals fail with 401. Resolves wolf1069b/wolfharness#365. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/wolfharness/mcp_server/session_pool.py | 5 +++-- src/wolfharness_config/mcp_server.py | 24 ++++++++++++++++++++-- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/src/wolfharness/mcp_server/session_pool.py b/src/wolfharness/mcp_server/session_pool.py index e68fb5730..3b255f899 100644 --- a/src/wolfharness/mcp_server/session_pool.py +++ b/src/wolfharness/mcp_server/session_pool.py @@ -83,6 +83,7 @@ def _create_transport(config: BaseMCPServerConfig) -> ClientTransport: SSEMCPServerConfig, StdioMCPServerConfig, StreamableHTTPMCPServerConfig, + _expand_headers, ) match config: @@ -94,7 +95,7 @@ def _create_transport(config: BaseMCPServerConfig) -> ClientTransport: return SSETransport( url=str(url), - headers=headers, + headers=_expand_headers(headers), httpx_client_factory=make_mcp_httpx_client_factory(read_timeout=config.timeout), ) case StreamableHTTPMCPServerConfig(url=url, headers=headers): @@ -102,7 +103,7 @@ def _create_transport(config: BaseMCPServerConfig) -> ClientTransport: return StreamableHttpTransport( url=str(url), - headers=headers, + headers=_expand_headers(headers), httpx_client_factory=make_mcp_httpx_client_factory(read_timeout=config.timeout), ) case AcpMCPServerConfig(): diff --git a/src/wolfharness_config/mcp_server.py b/src/wolfharness_config/mcp_server.py index 0f0b574b5..3661c61dc 100644 --- a/src/wolfharness_config/mcp_server.py +++ b/src/wolfharness_config/mcp_server.py @@ -22,6 +22,26 @@ _MCP_HTTP_READ_TIMEOUT: float = 60.0 +def _expand_headers(headers: dict[str, str] | None) -> dict[str, str] | None: + """Expand ``${VAR}`` environment variables in all HTTP header values. + + Mirrors the env expansion already performed for skill ``mcp.json`` + companion files (``wolfharness.skills.skill._expand_env_vars_in_value``). + Without this, header values such as ``Bearer ${API_TOKEN}`` are sent to + the MCP server as literals, causing auth failures (e.g. ``401``). + + Args: + headers: Header dict whose values may contain ``${VAR}`` placeholders. + + Returns: + A new dict with every header value passed through + ``os.path.expandvars``, or ``None`` if ``headers`` is ``None``. + """ + if headers is None: + return None + return {key: os.path.expandvars(value) for key, value in headers.items()} + + def make_mcp_httpx_client_factory( read_timeout: float = _MCP_HTTP_READ_TIMEOUT, ) -> Callable[..., httpx.AsyncClient]: @@ -386,7 +406,7 @@ def to_transport(self, force_oauth: bool = False) -> ClientTransport: return SSETransport( url=str(self.url), - headers=self.headers, + headers=_expand_headers(self.headers), httpx_client_factory=make_mcp_httpx_client_factory(read_timeout=self.timeout), ) @@ -456,7 +476,7 @@ def to_transport(self, force_oauth: bool = False) -> ClientTransport: return StreamableHttpTransport( url=str(self.url), - headers=self.headers, + headers=_expand_headers(self.headers), httpx_client_factory=make_mcp_httpx_client_factory(read_timeout=self.timeout), ) From c655de7c4a32a1fe979d94aceb91d873fdbfc2f7 Mon Sep 17 00:00:00 2001 From: Million <15158090088@163.com> Date: Tue, 11 Aug 2026 14:46:17 +0800 Subject: [PATCH 2/3] test(mcp): add env var expansion tests for MCP HTTP/SSE headers (#365) Add reproductions verifying ${VAR} expansion in to_transport() and _create_transport() for both SSE and streamable-http transports. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- tests/config/test_mcp_server_config.py | 30 ++++++++++++++++++++++++ tests/mcp_server/test_session_pool.py | 32 ++++++++++++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/tests/config/test_mcp_server_config.py b/tests/config/test_mcp_server_config.py index 682d64b90..026da4b6a 100644 --- a/tests/config/test_mcp_server_config.py +++ b/tests/config/test_mcp_server_config.py @@ -243,6 +243,36 @@ def test_to_transport_http(): assert transport.headers == {"X-Api-Key": "secret"} +def test_to_transport_sse_expands_env_in_headers(monkeypatch): + """to_transport() expands ${VAR} in SSE header values (repro for #365).""" + from fastmcp.client.transports import SSETransport + + monkeypatch.setenv("VIKING_MCP_API_KEY", "actual-token") + config = SSEMCPServerConfig( + url=HttpUrl("http://localhost:8080/sse"), + headers={"Authorization": "Bearer ${VIKING_MCP_API_KEY}"}, + ) + transport = config.to_transport() + + assert isinstance(transport, SSETransport) + assert transport.headers == {"Authorization": "Bearer actual-token"} + + +def test_to_transport_http_expands_env_in_headers(monkeypatch): + """StreamableHttp to_transport() expands ${VAR} in header values (#365).""" + from fastmcp.client.transports import StreamableHttpTransport + + monkeypatch.setenv("VIKING_MCP_API_KEY", "actual-token") + config = StreamableHTTPMCPServerConfig( + url=HttpUrl("https://api.example.com/mcp"), + headers={"Authorization": "Bearer ${VIKING_MCP_API_KEY}"}, + ) + transport = config.to_transport() + + assert isinstance(transport, StreamableHttpTransport) + assert transport.headers == {"Authorization": "Bearer actual-token"} + + def test_to_transport_acp_raises(): """AcpMCPServerConfig.to_transport() should raise NotImplementedError.""" config = AcpMCPServerConfig(acp_id="my-acp-server") diff --git a/tests/mcp_server/test_session_pool.py b/tests/mcp_server/test_session_pool.py index 46e55cb5e..5d89af1b1 100644 --- a/tests/mcp_server/test_session_pool.py +++ b/tests/mcp_server/test_session_pool.py @@ -378,6 +378,38 @@ def test_create_transport_acp_raises(acp_config: AcpMCPServerConfig) -> None: _create_transport(acp_config) +@pytest.mark.unit +def test_create_transport_sse_expands_env_in_headers(monkeypatch) -> None: + """_create_transport expands ${VAR} in SSE header values (repro for #365).""" + from fastmcp.client.transports import SSETransport + + monkeypatch.setenv("VIKING_MCP_API_KEY", "actual-token") + config = SSEMCPServerConfig( + name="sse-srv", + url=HttpUrl("http://localhost:8080/sse"), + headers={"Authorization": "Bearer ${VIKING_MCP_API_KEY}"}, + ) + transport = _create_transport(config) + assert isinstance(transport, SSETransport) + assert transport.headers == {"Authorization": "Bearer actual-token"} + + +@pytest.mark.unit +def test_create_transport_http_expands_env_in_headers(monkeypatch) -> None: + """_create_transport expands ${VAR} in streamable-http header values (repro for #365).""" + from fastmcp.client.transports import StreamableHttpTransport + + monkeypatch.setenv("VIKING_MCP_API_KEY", "actual-token") + config = StreamableHTTPMCPServerConfig( + name="http-srv", + url=HttpUrl("https://api.example.com/mcp"), + headers={"Authorization": "Bearer ${VIKING_MCP_API_KEY}"}, + ) + transport = _create_transport(config) + assert isinstance(transport, StreamableHttpTransport) + assert transport.headers == {"Authorization": "Bearer actual-token"} + + # --------------------------------------------------------------------------- # _stdio_owner_task # --------------------------------------------------------------------------- From c00ba49cbb9915a721dc9cc50a75f574ff2b7f5f Mon Sep 17 00:00:00 2001 From: Million <15158090088@163.com> Date: Tue, 11 Aug 2026 14:46:24 +0800 Subject: [PATCH 3/3] chore(changelog): add unreleased entry for MCP header env expansion (#365) Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- .../2026-08-11-mcp-header-env-expansion.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 changelog/unreleased/2026-08-11-mcp-header-env-expansion.md diff --git a/changelog/unreleased/2026-08-11-mcp-header-env-expansion.md b/changelog/unreleased/2026-08-11-mcp-header-env-expansion.md new file mode 100644 index 000000000..c48846462 --- /dev/null +++ b/changelog/unreleased/2026-08-11-mcp-header-env-expansion.md @@ -0,0 +1,14 @@ +# Expand environment variables in MCP HTTP/SSE header values + +MCP HTTP/SSE header values now support `${VAR}` environment variable +expansion before the transport is created. Previously, a header such as +`Authorization: Bearer ${API_TOKEN}` was sent to the MCP server as a literal +string, causing `401 Unauthorized` for authenticated servers and forcing users +to hard-code credentials in YAML. + +The expansion is applied in `SSEMCPServerConfig.to_transport()`, +`StreamableHTTPMCPServerConfig.to_transport()`, and the session pool's +`_create_transport()`, mirroring the existing `${VAR}` expansion already +supported for skill `mcp.json` companion files. + +Resolves wolf1069b/wolfharness#365. \ No newline at end of file