Skip to content

Commit 36383c9

Browse files
swissmoclaude
andcommitted
fix(component): keep the pre-rename SDK fallback off env proxies too
The trust_env=False fix only covered the canonical streamable_http_client path. The deprecated streamablehttp_client fallback (older fastmcp/mcp pinned via a pip-spec override) takes no http_client param, but it DOES accept an httpx_client_factory override for the client it builds internally - so it was still trusting HTTP_PROXY/NO_PROXY by default. Pass _loopback_httpx_client_factory there too, which builds the same verify=False/trust_env=False client as the canonical path. Review finding (chatgpt-codex-connector) on #2276. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 3209389 commit 36383c9

2 files changed

Lines changed: 45 additions & 4 deletions

File tree

custom_components/ha_mcp_tools/llm_api.py

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@
7171
)
7272

7373
if TYPE_CHECKING:
74+
import httpx
7475
from homeassistant.config_entries import ConfigEntry
7576
from homeassistant.util.json import JsonObjectType
7677
from mcp import types as mcp_types
@@ -222,6 +223,28 @@ async def async_probe_mcp_sdk(hass: HomeAssistant) -> bool:
222223
return True
223224

224225

226+
def _loopback_httpx_client_factory(
227+
headers: dict[str, str] | None = None,
228+
timeout: httpx.Timeout | None = None,
229+
auth: httpx.Auth | None = None,
230+
) -> httpx.AsyncClient:
231+
"""``httpx_client_factory`` for the pre-rename SDK's ``streamablehttp_client``.
232+
233+
That deprecated entry point takes no ``http_client`` — it always builds
234+
its own via this factory — but the factory itself IS overridable, so the
235+
same ``verify=False`` / ``trust_env=False`` posture as the canonical path
236+
in :func:`_mcp_session` still applies: this fallback is loopback-only
237+
too, so a real SSL context is pure waste and this call must never be
238+
diverted through an environment proxy (which would also leak the URL's
239+
embedded ``secret_path`` to it).
240+
"""
241+
import httpx
242+
243+
return httpx.AsyncClient(
244+
headers=headers, timeout=timeout, auth=auth, verify=False, trust_env=False
245+
)
246+
247+
225248
@asynccontextmanager
226249
async def _mcp_session(
227250
url: str,
@@ -273,11 +296,15 @@ async def _mcp_session(
273296
except ImportError:
274297
# Pre-rename SDK (an older ha-mcp resolved by a pip-spec override
275298
# pins an older fastmcp/mcp): same call shape, deprecated name,
276-
# and no http_client kwarg — it builds its own default client, so
277-
# on those old SDKs the blocking-SSL-setup cost is unavoidable.
299+
# and no http_client kwarg — but it does accept a factory for the
300+
# client it builds internally, so _loopback_httpx_client_factory
301+
# keeps this fallback on the same verify=False/trust_env=False
302+
# posture as the canonical path below.
278303
from mcp.client.streamable_http import streamablehttp_client
279304

280-
transport = streamablehttp_client(url=url)
305+
transport = streamablehttp_client(
306+
url=url, httpx_client_factory=_loopback_httpx_client_factory
307+
)
281308
else:
282309
import httpx
283310

tests/src/unit/test_llm_api.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -770,8 +770,9 @@ async def test_falls_back_to_deprecated_client_name(self, monkeypatch):
770770
opened: dict[str, Any] = {}
771771

772772
@asynccontextmanager
773-
async def _old_name_client(url):
773+
async def _old_name_client(url, httpx_client_factory=None):
774774
opened["url"] = url
775+
opened["httpx_client_factory"] = httpx_client_factory
775776
yield "read-stream", "write-stream", lambda: None
776777

777778
fake_transport = ModuleType("mcp.client.streamable_http")
@@ -806,3 +807,16 @@ async def initialize(self):
806807

807808
assert opened["url"] == "http://127.0.0.1:9584/private_x"
808809
assert opened["streams"] == ("read-stream", "write-stream")
810+
# Regression (review finding on #2276): the deprecated entry point
811+
# takes no http_client, but it DOES accept a factory for the client
812+
# it builds internally — _mcp_session must hand it one that keeps
813+
# this fallback off environment proxies too, not just the canonical
814+
# path.
815+
assert opened["httpx_client_factory"] is llm_api._loopback_httpx_client_factory
816+
817+
async def test_loopback_factory_builds_a_client_that_ignores_env_proxies(self):
818+
client = llm_api._loopback_httpx_client_factory()
819+
try:
820+
assert client.trust_env is False
821+
finally:
822+
await client.aclose()

0 commit comments

Comments
 (0)