diff --git a/homeassistant/components/mcp_server/config_flow.py b/homeassistant/components/mcp_server/config_flow.py index e8df68de5e2..e218691975a 100644 --- a/homeassistant/components/mcp_server/config_flow.py +++ b/homeassistant/components/mcp_server/config_flow.py @@ -32,11 +32,18 @@ class ModelContextServerProtocolConfigFlow(ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: """Handle the initial step.""" + errors: dict[str, str] = {} llm_apis = {api.id: api.name for api in llm.async_get_apis(self.hass)} if user_input is not None: - return self.async_create_entry( - title=llm_apis[user_input[CONF_LLM_HASS_API]], data=user_input - ) + if not user_input[CONF_LLM_HASS_API]: + errors[CONF_LLM_HASS_API] = "llm_api_required" + else: + return self.async_create_entry( + title=", ".join( + llm_apis[api_id] for api_id in user_input[CONF_LLM_HASS_API] + ), + data=user_input, + ) return self.async_show_form( step_id="user", @@ -44,7 +51,7 @@ class ModelContextServerProtocolConfigFlow(ConfigFlow, domain=DOMAIN): { vol.Optional( CONF_LLM_HASS_API, - default=llm.LLM_API_ASSIST, + default=[llm.LLM_API_ASSIST], ): SelectSelector( SelectSelectorConfig( options=[ @@ -53,10 +60,12 @@ class ModelContextServerProtocolConfigFlow(ConfigFlow, domain=DOMAIN): value=llm_api_id, ) for llm_api_id, name in llm_apis.items() - ] + ], + multiple=True, ) ), } ), description_placeholders={"more_info_url": MORE_INFO_URL}, + errors=errors, ) diff --git a/homeassistant/components/mcp_server/server.py b/homeassistant/components/mcp_server/server.py index affa4faecd6..953fc1314da 100644 --- a/homeassistant/components/mcp_server/server.py +++ b/homeassistant/components/mcp_server/server.py @@ -42,7 +42,7 @@ def _format_tool( async def create_server( - hass: HomeAssistant, llm_api_id: str, llm_context: llm.LLMContext + hass: HomeAssistant, llm_api_id: str | list[str], llm_context: llm.LLMContext ) -> Server: """Create a new Model Context Protocol Server. diff --git a/homeassistant/components/mcp_server/strings.json b/homeassistant/components/mcp_server/strings.json index 57f1baf183c..602030475ea 100644 --- a/homeassistant/components/mcp_server/strings.json +++ b/homeassistant/components/mcp_server/strings.json @@ -11,6 +11,9 @@ } } }, + "error": { + "llm_api_required": "At least one LLM API must be configured." + }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } diff --git a/tests/components/mcp_server/conftest.py b/tests/components/mcp_server/conftest.py index b5e25d9fe50..e109a9626d3 100644 --- a/tests/components/mcp_server/conftest.py +++ b/tests/components/mcp_server/conftest.py @@ -23,13 +23,15 @@ def mock_setup_entry() -> Generator[AsyncMock]: @pytest.fixture(name="llm_hass_api") -def llm_hass_api_fixture() -> str: +def llm_hass_api_fixture() -> list[str]: """Fixture for the config entry llm_hass_api.""" - return llm.LLM_API_ASSIST + return [llm.LLM_API_ASSIST] @pytest.fixture(name="config_entry") -def mock_config_entry(hass: HomeAssistant, llm_hass_api: str) -> MockConfigEntry: +def mock_config_entry( + hass: HomeAssistant, llm_hass_api: str | list[str] +) -> MockConfigEntry: """Fixture to load the integration.""" config_entry = MockConfigEntry( domain=DOMAIN, diff --git a/tests/components/mcp_server/test_config_flow.py b/tests/components/mcp_server/test_config_flow.py index 3b9f5bee663..52bbc26873c 100644 --- a/tests/components/mcp_server/test_config_flow.py +++ b/tests/components/mcp_server/test_config_flow.py @@ -16,7 +16,7 @@ from homeassistant.data_entry_flow import FlowResultType "params", [ {}, - {CONF_LLM_HASS_API: "assist"}, + {CONF_LLM_HASS_API: ["assist"]}, ], ) async def test_form( @@ -38,4 +38,33 @@ async def test_form( assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "Assist" assert len(mock_setup_entry.mock_calls) == 1 - assert result["data"] == {CONF_LLM_HASS_API: "assist"} + assert result["data"] == {CONF_LLM_HASS_API: ["assist"]} + + +@pytest.mark.parametrize( + ("params", "errors"), + [ + ({CONF_LLM_HASS_API: []}, {CONF_LLM_HASS_API: "llm_api_required"}), + ], +) +async def test_form_errors( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + params: dict[str, Any], + errors: dict[str, str], +) -> None: + """Test we get the errors on invalid user input.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert not result["errors"] + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + params, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.FORM + assert result["errors"] == errors diff --git a/tests/components/mcp_server/test_http.py b/tests/components/mcp_server/test_http.py index 61cd1a4dd02..e1c8801f51b 100644 --- a/tests/components/mcp_server/test_http.py +++ b/tests/components/mcp_server/test_http.py @@ -194,7 +194,7 @@ async def test_http_sse_multiple_config_entries( """ config_entry = MockConfigEntry( - domain="mcp_server", data={CONF_LLM_HASS_API: "llm-api-id"} + domain="mcp_server", data={CONF_LLM_HASS_API: ["llm-api-id"]} ) config_entry.add_to_hass(hass) await hass.config_entries.async_setup(config_entry.entry_id)