Avoid blocking SSL context creation in the MCP SSE transport (#180367)

This commit is contained in:
Franck Nijhof
2026-08-27 07:49:01 +02:00
committed by GitHub
parent c4ee43f053
commit 8a2a3da34a
2 changed files with 55 additions and 1 deletions
+26 -1
View File
@@ -27,6 +27,7 @@ from homeassistant.helpers import llm
from homeassistant.helpers.httpx_client import create_async_httpx_client
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.util.json import JsonObjectType
from homeassistant.util.ssl import SSL_ALPN_HTTP11, SSLCipherList, client_context
from .auth import AuthenticateHeader
from .const import DOMAIN
@@ -39,6 +40,26 @@ TIMEOUT = 10
type TokenManager = Callable[[], Awaitable[str]]
def _create_sse_httpx_client(
headers: dict[str, str] | None = None,
timeout: httpx.Timeout | None = None,
auth: httpx.Auth | None = None,
) -> httpx.AsyncClient:
"""Create the httpx client used by the SSE transport.
The SSE transport closes the client itself, so it cannot be handed one of
the Home Assistant managed clients. Building it here keeps it off the SDK
default, which reads the CA bundle from disk inside the event loop.
"""
return httpx.AsyncClient(
verify=client_context(SSLCipherList.PYTHON_DEFAULT, SSL_ALPN_HTTP11),
follow_redirects=True,
headers=headers,
timeout=timeout,
auth=auth,
)
@asynccontextmanager
async def mcp_client(
hass: HomeAssistant,
@@ -81,7 +102,11 @@ async def mcp_client(
)
try:
async with (
sse_client(url=url, headers=headers) as streams,
sse_client(
url=url,
headers=headers,
httpx_client_factory=_create_sse_httpx_client,
) as streams,
ClientSession(*streams) as session,
):
await session.initialize()
+29
View File
@@ -1,6 +1,7 @@
"""Tests for the Model Context Protocol component."""
import re
import ssl
from unittest.mock import AsyncMock, Mock, patch
import httpx
@@ -705,3 +706,31 @@ async def test_tool_call_http_error(
),
create_llm_context(),
)
async def test_sse_client_does_not_build_ssl_context(
hass: HomeAssistant,
config_entry: MockConfigEntry,
mock_http_streamable_client: AsyncMock,
mock_sse_client: AsyncMock,
) -> None:
"""Test the SSE transport does not load certificates in the event loop."""
http_405 = httpx.HTTPStatusError(
"Method not allowed", request=None, response=httpx.Response(405)
)
mock_http_streamable_client.side_effect = ExceptionGroup(
"Method not allowed", [http_405]
)
mock_sse_client.side_effect = ExceptionGroup(
"Connection error", [httpx.ConnectError("Connection failed")]
)
await hass.config_entries.async_setup(config_entry.entry_id)
assert config_entry.state is ConfigEntryState.SETUP_RETRY
client_factory = mock_sse_client.call_args.kwargs["httpx_client_factory"]
with patch.object(ssl.SSLContext, "load_verify_locations") as mock_load_certs:
client = client_factory(headers={}, timeout=httpx.Timeout(5))
assert not mock_load_certs.called
await client.aclose()