Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 103 additions & 39 deletions cloud_engineer_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Comment on lines +132 to +133
Copy link
Contributor

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.

Suggested change
if not mcp_initialized:
raise RuntimeError("Failed to initialize any MCP clients")
# Only raise error if this is unexpected - graceful degradation should handle MCP failures
# 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:")
Expand All @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛑 Exception Handling Risk: Using bare except: clauses can mask critical errors and make debugging difficult. This could hide important failure information during client cleanup operations.

Suggested change
aws_docs_mcp_client.stop()
except:
pass
try:
aws_docs_mcp_client.stop()
except Exception as e:
print(f"Warning: Error stopping AWS docs MCP client: {e}")
pass

if aws_diagram_mcp_client is not None:
try:
aws_diagram_mcp_client.stop()
except:
pass
Comment on lines +166 to +169
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛑 Exception Handling Risk: Using bare except: clauses can mask critical errors and make debugging difficult. This could hide important failure information during client cleanup operations.

Suggested change
try:
aws_diagram_mcp_client.stop()
except:
pass
try:
aws_diagram_mcp_client.stop()
except Exception as e:
print(f"Warning: Error stopping AWS diagram MCP client: {e}")
pass


# 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 []
Expand Down
193 changes: 193 additions & 0 deletions test_exception_handling.py
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicating the initialize_mcp_client function creates maintenance overhead and potential inconsistencies. Consider importing the actual function from the main module or extracting it to a shared utility module.



# 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)
Loading