-
Notifications
You must be signed in to change notification settings - Fork 5
Fix MCP Client Initialization Error Handling and Import Issues #8
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 all 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 | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -42,56 +42,106 @@ | |||||||||||||||||||
|
|
||||||||||||||||||||
| # Track whether MCP clients were successfully initialized | ||||||||||||||||||||
| mcp_initialized = False | ||||||||||||||||||||
| aws_docs_mcp_client = None | ||||||||||||||||||||
| aws_diagram_mcp_client = None | ||||||||||||||||||||
|
|
||||||||||||||||||||
| def initialize_mcp_client(client_name, client_factory): | ||||||||||||||||||||
| """ | ||||||||||||||||||||
| Safely initialize a single MCP client with proper error handling. | ||||||||||||||||||||
| Returns (client, success) tuple. | ||||||||||||||||||||
| """ | ||||||||||||||||||||
| try: | ||||||||||||||||||||
| print(f"Initializing {client_name}...") | ||||||||||||||||||||
| client = client_factory() | ||||||||||||||||||||
| print(f"Starting {client_name}...") | ||||||||||||||||||||
| client.start() | ||||||||||||||||||||
| print(f"{client_name} started successfully.") | ||||||||||||||||||||
| return client, True | ||||||||||||||||||||
| except BaseException as e: | ||||||||||||||||||||
| # Catch BaseException to handle both Exception and ExceptionGroup (from TaskGroup) | ||||||||||||||||||||
| error_type = type(e).__name__ | ||||||||||||||||||||
| error_message = str(e) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| # Handle ExceptionGroup specially (Python 3.11+ async TaskGroup errors) | ||||||||||||||||||||
| if error_type == "ExceptionGroup": | ||||||||||||||||||||
| print(f"Error initializing {client_name}: TaskGroup exception occurred") | ||||||||||||||||||||
| print(f" Exception type: {error_type}") | ||||||||||||||||||||
| # Extract sub-exceptions from ExceptionGroup | ||||||||||||||||||||
| if hasattr(e, 'exceptions'): | ||||||||||||||||||||
| print(f" Sub-exceptions ({len(e.exceptions)}):") | ||||||||||||||||||||
| for i, sub_exc in enumerate(e.exceptions, 1): | ||||||||||||||||||||
| print(f" {i}. {type(sub_exc).__name__}: {sub_exc}") | ||||||||||||||||||||
| else: | ||||||||||||||||||||
| print(f" Details: {error_message}") | ||||||||||||||||||||
| else: | ||||||||||||||||||||
| print(f"Error initializing {client_name}: {error_message}") | ||||||||||||||||||||
|
|
||||||||||||||||||||
| return None, False | ||||||||||||||||||||
|
|
||||||||||||||||||||
| try: | ||||||||||||||||||||
| if is_windows: | ||||||||||||||||||||
| # Windows-specific configuration | ||||||||||||||||||||
| print("Using Windows-specific MCP configuration...") | ||||||||||||||||||||
| # Set up AWS Documentation MCP client for Windows | ||||||||||||||||||||
| aws_docs_mcp_client = MCPClient(lambda: stdio_client( | ||||||||||||||||||||
| StdioServerParameters( | ||||||||||||||||||||
| command="uv", | ||||||||||||||||||||
| args=["tool", "run", "--from", "awslabs.aws-documentation-mcp-server@latest", "awslabs.aws-documentation-mcp-server.exe"], | ||||||||||||||||||||
| env={"FASTMCP_LOG_LEVEL": "ERROR"} | ||||||||||||||||||||
| ) | ||||||||||||||||||||
| )) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| # Set up AWS Diagram MCP client for Windows | ||||||||||||||||||||
| aws_diagram_mcp_client = MCPClient(lambda: stdio_client( | ||||||||||||||||||||
| StdioServerParameters( | ||||||||||||||||||||
| command="uv", | ||||||||||||||||||||
| args=["tool", "run", "--from", "awslabs.aws-diagram-mcp-server@latest", "awslabs.aws-diagram-mcp-server.exe"], | ||||||||||||||||||||
| env={"FASTMCP_LOG_LEVEL": "ERROR"} | ||||||||||||||||||||
| ) | ||||||||||||||||||||
| )) | ||||||||||||||||||||
| # Try to initialize AWS Documentation MCP client | ||||||||||||||||||||
| aws_docs_mcp_client, docs_success = initialize_mcp_client( | ||||||||||||||||||||
| "AWS Documentation MCP client", | ||||||||||||||||||||
| lambda: MCPClient(lambda: stdio_client( | ||||||||||||||||||||
| StdioServerParameters( | ||||||||||||||||||||
| command="uv", | ||||||||||||||||||||
| args=["tool", "run", "--from", "awslabs.aws-documentation-mcp-server@latest", "awslabs.aws-documentation-mcp-server.exe"], | ||||||||||||||||||||
| env={"FASTMCP_LOG_LEVEL": "ERROR"} | ||||||||||||||||||||
| ) | ||||||||||||||||||||
| )) | ||||||||||||||||||||
| ) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| # Try to initialize AWS Diagram MCP client | ||||||||||||||||||||
| aws_diagram_mcp_client, diagram_success = initialize_mcp_client( | ||||||||||||||||||||
| "AWS Diagram MCP client", | ||||||||||||||||||||
| lambda: MCPClient(lambda: stdio_client( | ||||||||||||||||||||
| StdioServerParameters( | ||||||||||||||||||||
| command="uv", | ||||||||||||||||||||
| args=["tool", "run", "--from", "awslabs.aws-diagram-mcp-server@latest", "awslabs.aws-diagram-mcp-server.exe"], | ||||||||||||||||||||
| env={"FASTMCP_LOG_LEVEL": "ERROR"} | ||||||||||||||||||||
| ) | ||||||||||||||||||||
| )) | ||||||||||||||||||||
| ) | ||||||||||||||||||||
| else: | ||||||||||||||||||||
| # Non-Windows configuration (Linux/macOS) | ||||||||||||||||||||
| print("Using standard MCP configuration for Linux/macOS...") | ||||||||||||||||||||
| # Set up AWS Documentation MCP client | ||||||||||||||||||||
| aws_docs_mcp_client = MCPClient(lambda: stdio_client( | ||||||||||||||||||||
| StdioServerParameters(command="uvx", args=["awslabs.aws-documentation-mcp-server@latest"]) | ||||||||||||||||||||
| )) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| # Set up AWS Diagram MCP client | ||||||||||||||||||||
| aws_diagram_mcp_client = MCPClient(lambda: stdio_client( | ||||||||||||||||||||
| StdioServerParameters(command="uvx", args=["awslabs.aws-diagram-mcp-server@latest"]) | ||||||||||||||||||||
| )) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| # Start both MCP clients | ||||||||||||||||||||
| print("Starting AWS Documentation MCP client...") | ||||||||||||||||||||
| aws_docs_mcp_client.start() | ||||||||||||||||||||
| print("AWS Documentation MCP client started successfully.") | ||||||||||||||||||||
| # Try to initialize AWS Documentation MCP client | ||||||||||||||||||||
| aws_docs_mcp_client, docs_success = initialize_mcp_client( | ||||||||||||||||||||
| "AWS Documentation MCP client", | ||||||||||||||||||||
| lambda: MCPClient(lambda: stdio_client( | ||||||||||||||||||||
| StdioServerParameters(command="uvx", args=["awslabs.aws-documentation-mcp-server@latest"]) | ||||||||||||||||||||
| )) | ||||||||||||||||||||
| ) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| # Try to initialize AWS Diagram MCP client | ||||||||||||||||||||
| aws_diagram_mcp_client, diagram_success = initialize_mcp_client( | ||||||||||||||||||||
| "AWS Diagram MCP client", | ||||||||||||||||||||
| lambda: MCPClient(lambda: stdio_client( | ||||||||||||||||||||
| StdioServerParameters(command="uvx", args=["awslabs.aws-diagram-mcp-server@latest"]) | ||||||||||||||||||||
| )) | ||||||||||||||||||||
| ) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| print("Starting AWS Diagram MCP client...") | ||||||||||||||||||||
| aws_diagram_mcp_client.start() | ||||||||||||||||||||
| print("AWS Diagram MCP client started successfully.") | ||||||||||||||||||||
| # Mark MCP as successfully initialized if at least one client started | ||||||||||||||||||||
| mcp_initialized = (aws_docs_mcp_client is not None or aws_diagram_mcp_client is not None) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| # Mark MCP as successfully initialized | ||||||||||||||||||||
| mcp_initialized = True | ||||||||||||||||||||
| if not mcp_initialized: | ||||||||||||||||||||
| raise RuntimeError("Failed to initialize any MCP clients") | ||||||||||||||||||||
|
|
||||||||||||||||||||
| except Exception as e: | ||||||||||||||||||||
| print(f"\nMCP initialization complete. Active clients: " + | ||||||||||||||||||||
| f"Documentation={'Yes' if aws_docs_mcp_client else 'No'}, " + | ||||||||||||||||||||
| f"Diagram={'Yes' if aws_diagram_mcp_client else 'No'}") | ||||||||||||||||||||
|
|
||||||||||||||||||||
| except BaseException as e: | ||||||||||||||||||||
| # Catch any remaining exceptions including ExceptionGroup | ||||||||||||||||||||
| error_type = type(e).__name__ | ||||||||||||||||||||
| error_message = str(e) | ||||||||||||||||||||
| print(f"Error initializing MCP clients: {error_message}") | ||||||||||||||||||||
|
|
||||||||||||||||||||
| print(f"\nMCP client initialization failed ({error_type}): {error_message}") | ||||||||||||||||||||
|
|
||||||||||||||||||||
| if is_windows: | ||||||||||||||||||||
| print("\nWindows-specific troubleshooting tips:") | ||||||||||||||||||||
|
|
@@ -106,11 +156,25 @@ | |||||||||||||||||||
| print("2. Check your network connection") | ||||||||||||||||||||
| print("3. Try running the Streamlit app instead: streamlit run app.py") | ||||||||||||||||||||
|
|
||||||||||||||||||||
| # Continue with limited functionality instead of raising exception | ||||||||||||||||||||
| # This allows the module to be imported even if MCP initialization fails | ||||||||||||||||||||
| print("\nContinuing with limited functionality (MCP tools disabled)...") | ||||||||||||||||||||
| # Ensure graceful degradation - clean up any partially initialized clients | ||||||||||||||||||||
| if aws_docs_mcp_client is not None: | ||||||||||||||||||||
| try: | ||||||||||||||||||||
| aws_docs_mcp_client.stop() | ||||||||||||||||||||
| except: | ||||||||||||||||||||
| pass | ||||||||||||||||||||
|
Comment on lines
+162
to
+164
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. 🛑 Exception Handling Risk: Using bare
Suggested change
|
||||||||||||||||||||
| if aws_diagram_mcp_client is not None: | ||||||||||||||||||||
| try: | ||||||||||||||||||||
| aws_diagram_mcp_client.stop() | ||||||||||||||||||||
| except: | ||||||||||||||||||||
| pass | ||||||||||||||||||||
|
Comment on lines
+166
to
+169
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. 🛑 Exception Handling Risk: Using bare
Suggested change
|
||||||||||||||||||||
|
|
||||||||||||||||||||
| # Reset to None for clean state | ||||||||||||||||||||
| aws_docs_mcp_client = None | ||||||||||||||||||||
| aws_diagram_mcp_client = None | ||||||||||||||||||||
| mcp_initialized = False | ||||||||||||||||||||
|
|
||||||||||||||||||||
| # Continue with limited functionality instead of crashing | ||||||||||||||||||||
| print("\nContinuing with limited functionality (MCP tools disabled)...") | ||||||||||||||||||||
|
|
||||||||||||||||||||
| # Get tools from MCP clients (if initialized) | ||||||||||||||||||||
| docs_tools = aws_docs_mcp_client.list_tools_sync() if mcp_initialized and aws_docs_mcp_client is not None else [] | ||||||||||||||||||||
|
|
||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,193 @@ | ||
| #!/usr/bin/env python3 | ||
| """ | ||
| Unit test to verify the exception handling logic for MCP client initialization. | ||
| This test simulates the error handling without requiring actual MCP dependencies. | ||
| """ | ||
|
|
||
| import sys | ||
|
|
||
|
|
||
| def initialize_mcp_client(client_name, client_factory): | ||
| """ | ||
| Copy of the initialize_mcp_client function from cloud_engineer_agent.py | ||
| to test the exception handling logic independently. | ||
| """ | ||
| try: | ||
| print(f"Initializing {client_name}...") | ||
| client = client_factory() | ||
| print(f"Starting {client_name}...") | ||
| # Simulate the start call | ||
| if hasattr(client, 'start'): | ||
| client.start() | ||
| print(f"{client_name} started successfully.") | ||
| return client, True | ||
| except BaseException as e: | ||
| # Catch BaseException to handle both Exception and ExceptionGroup (from TaskGroup) | ||
| error_type = type(e).__name__ | ||
| error_message = str(e) | ||
|
|
||
| # Handle ExceptionGroup specially (Python 3.11+ async TaskGroup errors) | ||
| if error_type == "ExceptionGroup": | ||
| print(f"Error initializing {client_name}: TaskGroup exception occurred") | ||
| print(f" Exception type: {error_type}") | ||
| # Extract sub-exceptions from ExceptionGroup | ||
| if hasattr(e, 'exceptions'): | ||
| print(f" Sub-exceptions ({len(e.exceptions)}):") | ||
| for i, sub_exc in enumerate(e.exceptions, 1): | ||
| print(f" {i}. {type(sub_exc).__name__}: {sub_exc}") | ||
| else: | ||
| print(f" Details: {error_message}") | ||
| else: | ||
| print(f"Error initializing {client_name}: {error_message}") | ||
|
|
||
| return None, False | ||
|
Comment on lines
+10
to
+43
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. Duplicating the |
||
|
|
||
|
|
||
| # Mock client classes for testing | ||
| class MockSuccessfulClient: | ||
| """Mock client that initializes successfully""" | ||
| def start(self): | ||
| pass | ||
|
|
||
|
|
||
| class MockFailingClient: | ||
| """Mock client that raises an exception on start""" | ||
| def start(self): | ||
| raise RuntimeError("Simulated connection error") | ||
|
|
||
|
|
||
| class MockExceptionGroupClient: | ||
| """Mock client that simulates a Python 3.11+ ExceptionGroup""" | ||
| def start(self): | ||
| # Create a mock ExceptionGroup-like error | ||
| class MockExceptionGroup(BaseException): | ||
| def __init__(self, message, exceptions): | ||
| self.message = message | ||
| self.exceptions = exceptions | ||
| super().__init__(message) | ||
|
|
||
| def __str__(self): | ||
| return f"{self.message} ({len(self.exceptions)} sub-exceptions)" | ||
|
|
||
| # Simulate nested exceptions from TaskGroup | ||
| sub_exceptions = [ | ||
| ConnectionError("Failed to connect to MCP server"), | ||
| ] | ||
| raise MockExceptionGroup("unhandled errors in a TaskGroup", sub_exceptions) | ||
|
|
||
|
|
||
| def test_successful_initialization(): | ||
| """Test successful client initialization""" | ||
| print("\n" + "=" * 80) | ||
| print("TEST 1: Successful Client Initialization") | ||
| print("=" * 80) | ||
|
|
||
| client, success = initialize_mcp_client( | ||
| "Test Successful Client", | ||
| lambda: MockSuccessfulClient() | ||
| ) | ||
|
|
||
| assert success is True, "Initialization should succeed" | ||
| assert client is not None, "Client should not be None" | ||
| print("✓ TEST PASSED: Successful initialization works correctly") | ||
| return True | ||
|
|
||
|
|
||
| def test_regular_exception_handling(): | ||
| """Test handling of regular exceptions""" | ||
| print("\n" + "=" * 80) | ||
| print("TEST 2: Regular Exception Handling") | ||
| print("=" * 80) | ||
|
|
||
| client, success = initialize_mcp_client( | ||
| "Test Failing Client", | ||
| lambda: MockFailingClient() | ||
| ) | ||
|
|
||
| assert success is False, "Initialization should fail" | ||
| assert client is None, "Client should be None on failure" | ||
| print("✓ TEST PASSED: Regular exceptions are handled correctly") | ||
| return True | ||
|
|
||
|
|
||
| def test_exception_group_handling(): | ||
| """Test handling of ExceptionGroup (TaskGroup errors)""" | ||
| print("\n" + "=" * 80) | ||
| print("TEST 3: ExceptionGroup Handling (TaskGroup errors)") | ||
| print("=" * 80) | ||
|
|
||
| client, success = initialize_mcp_client( | ||
| "Test ExceptionGroup Client", | ||
| lambda: MockExceptionGroupClient() | ||
| ) | ||
|
|
||
| assert success is False, "Initialization should fail" | ||
| assert client is None, "Client should be None on failure" | ||
| print("✓ TEST PASSED: ExceptionGroup exceptions are handled correctly") | ||
| return True | ||
|
|
||
|
|
||
| def test_graceful_degradation(): | ||
| """Test that application can continue with partial MCP failure""" | ||
| print("\n" + "=" * 80) | ||
| print("TEST 4: Graceful Degradation with Partial Failures") | ||
| print("=" * 80) | ||
|
|
||
| # Simulate initializing two clients where one fails | ||
| client1, success1 = initialize_mcp_client( | ||
| "Documentation Client", | ||
| lambda: MockSuccessfulClient() | ||
| ) | ||
|
|
||
| client2, success2 = initialize_mcp_client( | ||
| "Diagram Client", | ||
| lambda: MockFailingClient() | ||
| ) | ||
|
|
||
| # Check that we have partial functionality | ||
| assert success1 is True, "First client should succeed" | ||
| assert success2 is False, "Second client should fail" | ||
| assert client1 is not None, "First client should be available" | ||
| assert client2 is None, "Second client should be None" | ||
|
|
||
| # Simulate the mcp_initialized logic from cloud_engineer_agent.py | ||
| mcp_initialized = (client1 is not None or client2 is not None) | ||
| assert mcp_initialized is True, "MCP should be marked as initialized with partial success" | ||
|
|
||
| print("✓ TEST PASSED: Application can continue with partial MCP functionality") | ||
| return True | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| print("\n" + "=" * 80) | ||
| print("MCP Client Exception Handling Unit Tests") | ||
| print("=" * 80) | ||
| print(f"Python version: {sys.version}") | ||
|
|
||
| all_passed = True | ||
|
|
||
| try: | ||
| all_passed = test_successful_initialization() and all_passed | ||
| all_passed = test_regular_exception_handling() and all_passed | ||
| all_passed = test_exception_group_handling() and all_passed | ||
| all_passed = test_graceful_degradation() and all_passed | ||
|
|
||
| print("\n" + "=" * 80) | ||
| if all_passed: | ||
| print("ALL TESTS PASSED ✓") | ||
| print("=" * 80) | ||
| print("\nSummary:") | ||
| print("- Exception handling works correctly for all error types") | ||
| print("- ExceptionGroup errors are properly caught and reported") | ||
| print("- Application can continue with partial MCP functionality") | ||
| print("- Clean error messages provide good diagnostics") | ||
| sys.exit(0) | ||
| else: | ||
| print("SOME TESTS FAILED ✗") | ||
| print("=" * 80) | ||
| sys.exit(1) | ||
| except Exception as e: | ||
| print(f"\n✗ UNEXPECTED ERROR: {type(e).__name__}: {e}") | ||
| import traceback | ||
| traceback.print_exc() | ||
| sys.exit(1) | ||
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.
🛑 Logic Error: The RuntimeError on line 133 will always be raised when both MCP clients fail to initialize, causing the application to crash despite the graceful degradation logic in the exception handler. This contradicts the PR's goal of allowing the application to continue with limited functionality.