-
Notifications
You must be signed in to change notification settings - Fork 4.5k
Python: Fix MCP plugin hang when initialization fails #13414 #13437
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -269,15 +269,15 @@ async def __aexit__( | |||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| async def connect(self) -> None: | ||||||||||||||||||||||||||||||||||||||||||||||
| """Connect to the MCP server.""" | ||||||||||||||||||||||||||||||||||||||||||||||
| ready_event = asyncio.Event() | ||||||||||||||||||||||||||||||||||||||||||||||
| loop = asyncio.get_running_loop() | ||||||||||||||||||||||||||||||||||||||||||||||
| ready_future: asyncio.Future[None] = loop.create_future() | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||
| self._current_task = asyncio.create_task(self._inner_connect(ready_event)) | ||||||||||||||||||||||||||||||||||||||||||||||
| await ready_event.wait() | ||||||||||||||||||||||||||||||||||||||||||||||
| self._current_task = asyncio.create_task(self._inner_connect(ready_future)) | ||||||||||||||||||||||||||||||||||||||||||||||
| await ready_future | ||||||||||||||||||||||||||||||||||||||||||||||
| except KernelPluginInvalidConfigurationError: | ||||||||||||||||||||||||||||||||||||||||||||||
| ready_event.clear() | ||||||||||||||||||||||||||||||||||||||||||||||
| raise | ||||||||||||||||||||||||||||||||||||||||||||||
| except Exception as ex: | ||||||||||||||||||||||||||||||||||||||||||||||
| ready_event.clear() | ||||||||||||||||||||||||||||||||||||||||||||||
| await self.close() | ||||||||||||||||||||||||||||||||||||||||||||||
| raise FunctionExecutionException("Failed to enter context manager.") from ex | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -293,16 +293,20 @@ async def close(self) -> None: | |||||||||||||||||||||||||||||||||||||||||||||
| self._current_task = None | ||||||||||||||||||||||||||||||||||||||||||||||
| self.session = None | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| async def _inner_connect(self, ready_event: asyncio.Event) -> None: | ||||||||||||||||||||||||||||||||||||||||||||||
| async def _inner_connect(self, ready_future: asyncio.Future) -> None: | ||||||||||||||||||||||||||||||||||||||||||||||
| if not self.session: | ||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||
| transport = await self._exit_stack.enter_async_context(self.get_mcp_client()) | ||||||||||||||||||||||||||||||||||||||||||||||
| except Exception as ex: | ||||||||||||||||||||||||||||||||||||||||||||||
| await self._exit_stack.aclose() | ||||||||||||||||||||||||||||||||||||||||||||||
| ready_event.set() | ||||||||||||||||||||||||||||||||||||||||||||||
| raise KernelPluginInvalidConfigurationError( | ||||||||||||||||||||||||||||||||||||||||||||||
| "Failed to connect to the MCP server. Please check your configuration." | ||||||||||||||||||||||||||||||||||||||||||||||
| ) from ex | ||||||||||||||||||||||||||||||||||||||||||||||
| if not ready_future.done(): | ||||||||||||||||||||||||||||||||||||||||||||||
| exc = KernelPluginInvalidConfigurationError( | ||||||||||||||||||||||||||||||||||||||||||||||
| "Failed to connect to the MCP server. Please check your configuration." | ||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||
| exc.__cause__ = ex | ||||||||||||||||||||||||||||||||||||||||||||||
| ready_future.set_exception(exc) | ||||||||||||||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||
| session = await self._exit_stack.enter_async_context( | ||||||||||||||||||||||||||||||||||||||||||||||
| ClientSession( | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -316,16 +320,24 @@ async def _inner_connect(self, ready_event: asyncio.Event) -> None: | |||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||
| except Exception as ex: | ||||||||||||||||||||||||||||||||||||||||||||||
| await self._exit_stack.aclose() | ||||||||||||||||||||||||||||||||||||||||||||||
| raise KernelPluginInvalidConfigurationError( | ||||||||||||||||||||||||||||||||||||||||||||||
| "Failed to create a session. Please check your configuration." | ||||||||||||||||||||||||||||||||||||||||||||||
| ) from ex | ||||||||||||||||||||||||||||||||||||||||||||||
| if not ready_future.done(): | ||||||||||||||||||||||||||||||||||||||||||||||
| exc = KernelPluginInvalidConfigurationError( | ||||||||||||||||||||||||||||||||||||||||||||||
| "Failed to create a session. Please check your configuration." | ||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||
| exc.__cause__ = ex | ||||||||||||||||||||||||||||||||||||||||||||||
| ready_future.set_exception(exc) | ||||||||||||||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||
| await session.initialize() | ||||||||||||||||||||||||||||||||||||||||||||||
| except Exception as ex: | ||||||||||||||||||||||||||||||||||||||||||||||
| await self._exit_stack.aclose() | ||||||||||||||||||||||||||||||||||||||||||||||
| raise KernelPluginInvalidConfigurationError( | ||||||||||||||||||||||||||||||||||||||||||||||
| "Failed to initialize session. Please check your configuration." | ||||||||||||||||||||||||||||||||||||||||||||||
| ) from ex | ||||||||||||||||||||||||||||||||||||||||||||||
| if not ready_future.done(): | ||||||||||||||||||||||||||||||||||||||||||||||
| exc = KernelPluginInvalidConfigurationError( | ||||||||||||||||||||||||||||||||||||||||||||||
| "Failed to initialize session. Please check your configuration." | ||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||
| exc.__cause__ = ex | ||||||||||||||||||||||||||||||||||||||||||||||
| ready_future.set_exception(exc) | ||||||||||||||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||||||||||||||
| self.session = session | ||||||||||||||||||||||||||||||||||||||||||||||
| elif self.session._request_id == 0: | ||||||||||||||||||||||||||||||||||||||||||||||
| # If the session is not initialized, we need to reinitialize it | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -344,7 +356,8 @@ async def _inner_connect(self, ready_event: asyncio.Event) -> None: | |||||||||||||||||||||||||||||||||||||||||||||
| except Exception: | ||||||||||||||||||||||||||||||||||||||||||||||
| logger.warning("Failed to set log level to %s", logger.level) | ||||||||||||||||||||||||||||||||||||||||||||||
| # Setting up is complete, will now signal the main loop that we are ready | ||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
334
to
358
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Deadlock bug: If
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||
| ready_event.set() | ||||||||||||||||||||||||||||||||||||||||||||||
| if not ready_future.done(): | ||||||||||||||||||||||||||||||||||||||||||||||
| ready_future.set_result(None) | ||||||||||||||||||||||||||||||||||||||||||||||
| # Create a stop event to signal the exit stack to close | ||||||||||||||||||||||||||||||||||||||||||||||
| self._stop_event = asyncio.Event() | ||||||||||||||||||||||||||||||||||||||||||||||
| await self._stop_event.wait() | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -466,11 +479,8 @@ async def message_handler( | |||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| async def load_prompts(self): | ||||||||||||||||||||||||||||||||||||||||||||||
| """Load prompts from the MCP server.""" | ||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||
| prompt_list = await self.session.list_prompts() | ||||||||||||||||||||||||||||||||||||||||||||||
| except Exception: | ||||||||||||||||||||||||||||||||||||||||||||||
| prompt_list = None | ||||||||||||||||||||||||||||||||||||||||||||||
| for prompt in prompt_list.prompts if prompt_list else []: | ||||||||||||||||||||||||||||||||||||||||||||||
| prompt_list = await self.session.list_prompts() | ||||||||||||||||||||||||||||||||||||||||||||||
| for prompt in prompt_list.prompts: | ||||||||||||||||||||||||||||||||||||||||||||||
| local_name = _normalize_mcp_name(prompt.name) | ||||||||||||||||||||||||||||||||||||||||||||||
| func = kernel_function(name=local_name, description=prompt.description)( | ||||||||||||||||||||||||||||||||||||||||||||||
| partial(self.get_prompt, prompt.name) | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -480,12 +490,8 @@ async def load_prompts(self): | |||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| async def load_tools(self): | ||||||||||||||||||||||||||||||||||||||||||||||
| """Load tools from the MCP server.""" | ||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||
| tool_list = await self.session.list_tools() | ||||||||||||||||||||||||||||||||||||||||||||||
| except Exception: | ||||||||||||||||||||||||||||||||||||||||||||||
| tool_list = None | ||||||||||||||||||||||||||||||||||||||||||||||
| # Create methods with the kernel_function decorator for each tool | ||||||||||||||||||||||||||||||||||||||||||||||
| for tool in tool_list.tools if tool_list else []: | ||||||||||||||||||||||||||||||||||||||||||||||
| tool_list = await self.session.list_tools() | ||||||||||||||||||||||||||||||||||||||||||||||
| for tool in tool_list.tools: | ||||||||||||||||||||||||||||||||||||||||||||||
| local_name = _normalize_mcp_name(tool.name) | ||||||||||||||||||||||||||||||||||||||||||||||
| func = kernel_function(name=local_name, description=tool.description)(partial(self.call_tool, tool.name)) | ||||||||||||||||||||||||||||||||||||||||||||||
| func.__kernel_function_parameters__ = _get_parameter_dicts_from_mcp_tool(tool) | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unlike
raise KernelPluginInvalidConfigurationError(...) from ex, manually setting__cause__does not set__suppress_context__ = True. This means Python will print 'During handling of the above exception…' instead of 'The above exception was the direct cause of…'. Addexc.__suppress_context__ = Trueafter setting__cause__.