423 lines
11 KiB
Python
423 lines
11 KiB
Python
"""
|
|
FastMCP Error Handling Template
|
|
================================
|
|
Comprehensive error handling patterns with structured responses and retry logic.
|
|
"""
|
|
|
|
from fastmcp import FastMCP
|
|
import asyncio
|
|
import httpx
|
|
from enum import Enum
|
|
from typing import Dict, Any, Optional
|
|
from datetime import datetime
|
|
|
|
mcp = FastMCP("Error Handling Examples")
|
|
|
|
# ============================================================================
|
|
# Error Code Enum
|
|
# ============================================================================
|
|
|
|
class ErrorCode(Enum):
|
|
"""Standard error codes."""
|
|
VALIDATION_ERROR = "VALIDATION_ERROR"
|
|
NOT_FOUND = "NOT_FOUND"
|
|
UNAUTHORIZED = "UNAUTHORIZED"
|
|
RATE_LIMITED = "RATE_LIMITED"
|
|
API_ERROR = "API_ERROR"
|
|
TIMEOUT = "TIMEOUT"
|
|
NETWORK_ERROR = "NETWORK_ERROR"
|
|
UNKNOWN_ERROR = "UNKNOWN_ERROR"
|
|
|
|
|
|
# ============================================================================
|
|
# Response Formatters
|
|
# ============================================================================
|
|
|
|
def create_success(data: Any, message: str = "Success") -> Dict[str, Any]:
|
|
"""Create structured success response."""
|
|
return {
|
|
"success": True,
|
|
"message": message,
|
|
"data": data,
|
|
"timestamp": datetime.now().isoformat()
|
|
}
|
|
|
|
|
|
def create_error(
|
|
code: ErrorCode,
|
|
message: str,
|
|
details: Optional[Dict[str, Any]] = None
|
|
) -> Dict[str, Any]:
|
|
"""Create structured error response."""
|
|
return {
|
|
"success": False,
|
|
"error": {
|
|
"code": code.value,
|
|
"message": message,
|
|
"details": details or {},
|
|
"timestamp": datetime.now().isoformat()
|
|
}
|
|
}
|
|
|
|
|
|
# ============================================================================
|
|
# Retry Logic
|
|
# ============================================================================
|
|
|
|
async def retry_with_backoff(
|
|
func,
|
|
max_retries: int = 3,
|
|
initial_delay: float = 1.0,
|
|
exponential_base: float = 2.0,
|
|
catch_exceptions: tuple = (Exception,)
|
|
):
|
|
"""
|
|
Retry function with exponential backoff.
|
|
|
|
Args:
|
|
func: Async function to retry
|
|
max_retries: Maximum number of retry attempts
|
|
initial_delay: Initial delay in seconds
|
|
exponential_base: Base for exponential backoff
|
|
catch_exceptions: Tuple of exceptions to catch and retry
|
|
|
|
Returns:
|
|
Function result if successful
|
|
|
|
Raises:
|
|
Last exception if all retries fail
|
|
"""
|
|
delay = initial_delay
|
|
last_exception = None
|
|
|
|
for attempt in range(max_retries):
|
|
try:
|
|
return await func()
|
|
except catch_exceptions as e:
|
|
last_exception = e
|
|
if attempt < max_retries - 1:
|
|
await asyncio.sleep(delay)
|
|
delay *= exponential_base
|
|
|
|
raise last_exception
|
|
|
|
|
|
# ============================================================================
|
|
# Tools with Error Handling
|
|
# ============================================================================
|
|
|
|
@mcp.tool()
|
|
def divide_numbers(a: float, b: float) -> dict:
|
|
"""
|
|
Divide two numbers with error handling.
|
|
|
|
Args:
|
|
a: Numerator
|
|
b: Denominator
|
|
|
|
Returns:
|
|
Result or error
|
|
"""
|
|
try:
|
|
if b == 0:
|
|
return create_error(
|
|
ErrorCode.VALIDATION_ERROR,
|
|
"Division by zero is not allowed",
|
|
{"a": a, "b": b}
|
|
)
|
|
|
|
result = a / b
|
|
return create_success(
|
|
{"result": result, "a": a, "b": b},
|
|
"Division successful"
|
|
)
|
|
|
|
except Exception as e:
|
|
return create_error(
|
|
ErrorCode.UNKNOWN_ERROR,
|
|
f"Unexpected error: {str(e)}"
|
|
)
|
|
|
|
|
|
@mcp.tool()
|
|
async def validated_operation(data: str, min_length: int = 1) -> dict:
|
|
"""
|
|
Operation with input validation.
|
|
|
|
Args:
|
|
data: Input data to validate
|
|
min_length: Minimum required length
|
|
|
|
Returns:
|
|
Processed result or validation error
|
|
"""
|
|
# Validate input
|
|
if not data:
|
|
return create_error(
|
|
ErrorCode.VALIDATION_ERROR,
|
|
"Data is required",
|
|
{"field": "data", "constraint": "not_empty"}
|
|
)
|
|
|
|
if len(data) < min_length:
|
|
return create_error(
|
|
ErrorCode.VALIDATION_ERROR,
|
|
f"Data must be at least {min_length} characters",
|
|
{"field": "data", "min_length": min_length, "actual_length": len(data)}
|
|
)
|
|
|
|
# Process data
|
|
try:
|
|
processed = data.upper()
|
|
return create_success(
|
|
{"original": data, "processed": processed},
|
|
"Data processed successfully"
|
|
)
|
|
except Exception as e:
|
|
return create_error(
|
|
ErrorCode.UNKNOWN_ERROR,
|
|
str(e)
|
|
)
|
|
|
|
|
|
@mcp.tool()
|
|
async def resilient_api_call(url: str) -> dict:
|
|
"""
|
|
API call with retry logic and comprehensive error handling.
|
|
|
|
Args:
|
|
url: URL to fetch
|
|
|
|
Returns:
|
|
API response or detailed error
|
|
"""
|
|
async def make_request():
|
|
async with httpx.AsyncClient(timeout=10) as client:
|
|
response = await client.get(url)
|
|
response.raise_for_status()
|
|
return response.json()
|
|
|
|
try:
|
|
data = await retry_with_backoff(
|
|
make_request,
|
|
max_retries=3,
|
|
catch_exceptions=(httpx.TimeoutException, httpx.NetworkError)
|
|
)
|
|
|
|
return create_success(
|
|
data,
|
|
"API call successful"
|
|
)
|
|
|
|
except httpx.TimeoutException:
|
|
return create_error(
|
|
ErrorCode.TIMEOUT,
|
|
"Request timed out",
|
|
{"url": url, "timeout_seconds": 10}
|
|
)
|
|
|
|
except httpx.NetworkError as e:
|
|
return create_error(
|
|
ErrorCode.NETWORK_ERROR,
|
|
"Network error occurred",
|
|
{"url": url, "error": str(e)}
|
|
)
|
|
|
|
except httpx.HTTPStatusError as e:
|
|
if e.response.status_code == 404:
|
|
return create_error(
|
|
ErrorCode.NOT_FOUND,
|
|
"Resource not found",
|
|
{"url": url, "status_code": 404}
|
|
)
|
|
elif e.response.status_code == 401:
|
|
return create_error(
|
|
ErrorCode.UNAUTHORIZED,
|
|
"Unauthorized access",
|
|
{"url": url, "status_code": 401}
|
|
)
|
|
elif e.response.status_code == 429:
|
|
return create_error(
|
|
ErrorCode.RATE_LIMITED,
|
|
"Rate limit exceeded",
|
|
{"url": url, "status_code": 429}
|
|
)
|
|
else:
|
|
return create_error(
|
|
ErrorCode.API_ERROR,
|
|
f"HTTP {e.response.status_code}",
|
|
{"url": url, "status_code": e.response.status_code}
|
|
)
|
|
|
|
except Exception as e:
|
|
return create_error(
|
|
ErrorCode.UNKNOWN_ERROR,
|
|
f"Unexpected error: {str(e)}",
|
|
{"url": url}
|
|
)
|
|
|
|
|
|
@mcp.tool()
|
|
async def batch_with_error_recovery(items: list[str]) -> dict:
|
|
"""
|
|
Batch process items with individual error recovery.
|
|
|
|
Args:
|
|
items: List of items to process
|
|
|
|
Returns:
|
|
Results with successes and failures tracked separately
|
|
"""
|
|
results = []
|
|
errors = []
|
|
|
|
for i, item in enumerate(items):
|
|
try:
|
|
# Simulate processing
|
|
if not item:
|
|
raise ValueError("Empty item")
|
|
|
|
await asyncio.sleep(0.1)
|
|
|
|
results.append({
|
|
"index": i,
|
|
"item": item,
|
|
"processed": item.upper(),
|
|
"success": True
|
|
})
|
|
|
|
except ValueError as e:
|
|
errors.append({
|
|
"index": i,
|
|
"item": item,
|
|
"error": str(e),
|
|
"code": ErrorCode.VALIDATION_ERROR.value
|
|
})
|
|
|
|
except Exception as e:
|
|
errors.append({
|
|
"index": i,
|
|
"item": item,
|
|
"error": str(e),
|
|
"code": ErrorCode.UNKNOWN_ERROR.value
|
|
})
|
|
|
|
return {
|
|
"success": len(errors) == 0,
|
|
"total": len(items),
|
|
"successful": len(results),
|
|
"failed": len(errors),
|
|
"results": results,
|
|
"errors": errors,
|
|
"timestamp": datetime.now().isoformat()
|
|
}
|
|
|
|
|
|
@mcp.tool()
|
|
async def safe_database_operation(query: str) -> dict:
|
|
"""
|
|
Simulated database operation with error handling.
|
|
|
|
Args:
|
|
query: SQL query (simulated)
|
|
|
|
Returns:
|
|
Query result or error
|
|
"""
|
|
try:
|
|
# Validate query
|
|
if "DROP" in query.upper():
|
|
return create_error(
|
|
ErrorCode.UNAUTHORIZED,
|
|
"DROP operations not allowed",
|
|
{"query": query}
|
|
)
|
|
|
|
if not query.strip():
|
|
return create_error(
|
|
ErrorCode.VALIDATION_ERROR,
|
|
"Query cannot be empty"
|
|
)
|
|
|
|
# Simulate query execution
|
|
await asyncio.sleep(0.1)
|
|
|
|
# Simulate success
|
|
mock_data = [
|
|
{"id": 1, "name": "Alice"},
|
|
{"id": 2, "name": "Bob"}
|
|
]
|
|
|
|
return create_success(
|
|
{"rows": mock_data, "count": len(mock_data)},
|
|
"Query executed successfully"
|
|
)
|
|
|
|
except Exception as e:
|
|
return create_error(
|
|
ErrorCode.API_ERROR,
|
|
f"Database error: {str(e)}",
|
|
{"query": query}
|
|
)
|
|
|
|
|
|
# ============================================================================
|
|
# Resources with Error Handling
|
|
# ============================================================================
|
|
|
|
@mcp.resource("health://detailed")
|
|
async def detailed_health_check() -> dict:
|
|
"""Comprehensive health check with error tracking."""
|
|
checks = {}
|
|
errors = []
|
|
|
|
# Check API connectivity
|
|
try:
|
|
async with httpx.AsyncClient(timeout=5) as client:
|
|
response = await client.get("https://api.example.com/health")
|
|
checks["api"] = {
|
|
"status": "healthy",
|
|
"status_code": response.status_code
|
|
}
|
|
except Exception as e:
|
|
checks["api"] = {
|
|
"status": "unhealthy",
|
|
"error": str(e)
|
|
}
|
|
errors.append(f"API check failed: {e}")
|
|
|
|
# Check system resources
|
|
try:
|
|
import psutil
|
|
checks["system"] = {
|
|
"status": "healthy",
|
|
"cpu_percent": psutil.cpu_percent(),
|
|
"memory_percent": psutil.virtual_memory().percent
|
|
}
|
|
except Exception as e:
|
|
checks["system"] = {
|
|
"status": "error",
|
|
"error": str(e)
|
|
}
|
|
|
|
# Overall status
|
|
all_healthy = all(
|
|
check.get("status") == "healthy"
|
|
for check in checks.values()
|
|
)
|
|
|
|
return {
|
|
"status": "healthy" if all_healthy else "degraded",
|
|
"checks": checks,
|
|
"errors": errors if errors else None,
|
|
"timestamp": datetime.now().isoformat()
|
|
}
|
|
|
|
|
|
# ============================================================================
|
|
# Main
|
|
# ============================================================================
|
|
|
|
if __name__ == "__main__":
|
|
mcp.run()
|