Fix MCP client double initialize in config flow#171636
Conversation
|
Please take a look at the requested changes, and use the Ready for review button when you are done, thanks 👍 |
|
Hey there @allenporter, mind taking a look at this pull request as it has been labeled with an integration ( Code owner commandsCode owners of
|
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Fixes a regression where MCP servers reject a second initialize() call during config flow by ensuring initialization happens exactly once inside the mcp_client context manager.
Changes:
- Update
mcp_clientto callsession.initialize()internally and yield both the session andInitializeResult. - Refactor coordinator and config flow call sites to unpack
(session, initialize_result)and avoid re-initializing. - Add a regression test asserting
initialize()is only called once during config flow.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
| tests/components/mcp/test_config_flow.py | Adds regression coverage for the double-initialize() config flow bug. |
| homeassistant/components/mcp/coordinator.py | Changes mcp_client to yield (ClientSession, InitializeResult) and updates existing call sites. |
| homeassistant/components/mcp/config_flow.py | Stops calling initialize() directly and uses the initialization result from mcp_client. |
| ) -> AsyncGenerator[tuple[ClientSession, InitializeResult]]: | ||
| """Create an MCP client. | ||
|
|
||
| This is an asynccontext manager that exists to wrap other async context managers |
| url: str, | ||
| token_manager: TokenManager | None = None, | ||
| ) -> AsyncGenerator[ClientSession]: | ||
| ) -> AsyncGenerator[tuple[ClientSession, InitializeResult]]: |
| result = await session.initialize() | ||
| yield session, result |
| async with mcp_client(hass, url, token_manager=token_manager) as ( | ||
| _session, | ||
| response, | ||
| ): | ||
| pass |
| response = Mock() | ||
| response.serverInfo.name = TEST_API_NAME | ||
| mock_mcp_client.return_value.initialize.return_value = response |
|
Thank you for the contribution. Please address or respond to the co-pilot feedback and mark as Ready for review when ready. Thank you. |
Proposed change
mcp_clientis an@asynccontextmanagerthat opens the MCP transport and callssession.initialize()before yielding the session.validate_inputinconfig_flow.pythen calledsession.initialize()a second time on the already-initialized session in order to obtain theInitializeResult(needed forresponse.capabilities.toolsandresponse.serverInfo.name).MCP servers that correctly implement the spec reject the second
initializePOST which carries the existingmcp-session-idheader, with400 -32600 "Invalid Request: Server already initialized". This surfaces to the user as "Cannot connect to MCP server".Minimal curl reproduction (against any compliant MCP server):
Fix: Make mcp_client capture the InitializeResult and yield (session, result) instead of session alone. Update the two non-config-flow callers (ModelContextProtocolTool.async_call and _async_update_data) to unpack (session, _). Update validate_input to unpack (_session, response) from the context manager and drop the second initialize() call. The response object is unchanged - response.capabilities.tools and response.serverInfo.name are still used on the lines that follow.
Type of change
Additional information
Checklist
ruff format homeassistant tests)If user exposed functionality or configuration variables are added/changed:
If the code communicates with devices, web services, or third-party tools:
Updated and included derived files by running:
python3 -m script.hassfest.requirements_all.txt.Updated by running
python3 -m script.gen_requirements_all.To help with the load of incoming pull requests: