mirror of
https://github.com/home-assistant/core.git
synced 2026-09-04 20:42:21 +01:00
Avoid blocking SSL context creation in the MCP SSE transport (#180367)
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user