Discover and Export available Agent(s) as MCP Servers
Comprehensive guide to error handling strategies, graceful degradation, recovery patterns, and best practices for embabel-agent-mcpserver.
Key Principles:
Error Occurs
│
├─→ Log Error (with context)
│
├─→ Attempt Recovery (if possible)
│ │
│ ├─→ Success → Continue
│ └─→ Failure → Continue to next step
│
├─→ Apply Fallback (if available)
│ │
│ ├─→ Success → Return fallback result
│ └─→ Failure → Continue to next step
│
└─→ Return Error Response
│
└─→ MCP Client receives errorDescription: Errors during server initialization or configuration
Examples:
Handling: Fail fast at startup { .api }
@Configuration
class ValidatedMcpConfiguration {
@Bean
fun mcpServerConfig(environment: Environment): McpServerConfig {
val mode = environment.getProperty("spring.ai.mcp.server.type")
require(mode in setOf("SYNC", "ASYNC", null)) {
"Invalid MCP server type: $mode. Must be SYNC or ASYNC"
}
val maxTools = environment.getProperty("mcp.server.max-tools", Int::class.java, 100)
require(maxTools in 1..10000) {
"Invalid max-tools: $maxTools. Must be between 1 and 10000"
}
return McpServerConfig(
maxTools = maxTools,
executionMode = mode?.let { McpExecutionMode.valueOf(it) } ?: McpExecutionMode.SYNC
)
}
}Description: Errors when registering tools, resources, or prompts
Examples:
Handling: Log and skip problematic items { .api }
@Service
class RobustPublisher(
private val serverStrategy: McpServerStrategy
) : McpExportToolCallbackPublisher {
override val toolCallbacks: List<ToolCallback>
get() {
val validTools = mutableListOf<ToolCallback>()
listOf(tool1, tool2, tool3).forEach { tool ->
try {
validateTool(tool)
validTools.add(tool)
} catch (e: Exception) {
logger.error("Failed to validate tool: ${tool?.name}", e)
// Continue with other tools
}
}
return validTools
}
override fun infoString(verbose: Boolean?, indent: Int) =
"RobustPublisher: ${toolCallbacks.size} tools"
private fun validateTool(tool: ToolCallback) {
require(tool.name.isNotBlank()) { "Tool name cannot be blank" }
require(tool.description.isNotBlank()) { "Tool description cannot be blank" }
require(tool.name.length <= 100) { "Tool name too long: ${tool.name}" }
}
companion object {
private val logger = LoggerFactory.getLogger(RobustPublisher::class.java)
}
private val tool1: ToolCallback = TODO()
private val tool2: ToolCallback = TODO()
private val tool3: ToolCallback = TODO()
}Description: Errors during tool execution
Examples:
Handling: Return structured error responses { .api }
@Service
class SafeToolPublisher : McpExportToolCallbackPublisher {
private val safeTool = object : ToolCallback {
override fun getName() = "safe_tool"
override fun getDescription() = "Tool with comprehensive error handling"
override fun call(functionArguments: String): String {
return try {
// Parse input
val input = parseInput(functionArguments)
// Validate
validateInput(input)
// Execute
val result = execute(input)
// Serialize success
serializeSuccess(result)
} catch (e: JsonProcessingException) {
serializeError("INVALID_INPUT", "Failed to parse input: ${e.message}")
} catch (e: IllegalArgumentException) {
serializeError("VALIDATION_ERROR", e.message ?: "Invalid input")
} catch (e: TimeoutException) {
serializeError("TIMEOUT", "Operation timed out after 30 seconds")
} catch (e: ExternalServiceException) {
serializeError("EXTERNAL_ERROR", "External service unavailable: ${e.message}")
} catch (e: Exception) {
logger.error("Unexpected error in tool execution", e)
serializeError("INTERNAL_ERROR", "An internal error occurred")
}
}
private fun parseInput(args: String): ToolInput {
return objectMapper.readValue(args, ToolInput::class.java)
}
private fun validateInput(input: ToolInput) {
require(input.data.isNotBlank()) { "Data cannot be blank" }
require(input.data.length <= 1000) { "Data too long" }
}
private fun execute(input: ToolInput): ToolOutput {
return ToolOutput(result = "processed: ${input.data}")
}
private fun serializeSuccess(output: ToolOutput): String {
return objectMapper.writeValueAsString(
mapOf(
"success" to true,
"data" to output
)
)
}
private fun serializeError(code: String, message: String): String {
return objectMapper.writeValueAsString(
mapOf(
"success" to false,
"error" to mapOf(
"code" to code,
"message" to message
)
)
)
}
}
override val toolCallbacks = listOf(safeTool)
override fun infoString(verbose: Boolean?, indent: Int) = "SafeToolPublisher"
data class ToolInput(val data: String)
data class ToolOutput(val result: String)
class ExternalServiceException(message: String) : RuntimeException(message)
companion object {
private val logger = LoggerFactory.getLogger(SafeToolPublisher::class.java)
private val objectMapper = ObjectMapper()
}
}Description: Errors during server operation
Examples:
Handling: Monitor, alert, and gracefully degrade { .api }
@Component
class RuntimeErrorMonitor(
private val meterRegistry: MeterRegistry
) {
@Scheduled(fixedRate = 60000) // Every minute
fun checkSystemHealth() {
try {
checkMemoryUsage()
checkThreadPoolStatus()
checkConnectionPool()
} catch (e: Exception) {
logger.error("Health check failed", e)
meterRegistry.counter("health.check.failures").increment()
}
}
private fun checkMemoryUsage() {
val runtime = Runtime.getRuntime()
val usedMemory = runtime.totalMemory() - runtime.freeMemory()
val maxMemory = runtime.maxMemory()
val usagePercent = (usedMemory.toDouble() / maxMemory * 100).toInt()
meterRegistry.gauge("memory.usage.percent", usagePercent)
if (usagePercent > 90) {
logger.error("CRITICAL: Memory usage at $usagePercent%")
// Trigger garbage collection
System.gc()
} else if (usagePercent > 80) {
logger.warn("WARNING: Memory usage at $usagePercent%")
}
}
private fun checkThreadPoolStatus() {
// Check thread pool metrics
// Alert if approaching limits
}
private fun checkConnectionPool() {
// Check connection pool metrics
// Alert if exhausted
}
companion object {
private val logger = LoggerFactory.getLogger(RuntimeErrorMonitor::class.java)
}
}Basic Error Handling: { .api }
@Service
class SyncErrorHandlingService(
private val serverStrategy: McpServerStrategy
) {
fun registerToolSafely(tool: ToolCallback): Boolean {
return try {
serverStrategy.addToolCallback(tool)
.block(Duration.ofSeconds(5))
logger.info("Tool registered: ${tool.name}")
true
} catch (e: TimeoutException) {
logger.error("Timeout registering tool: ${tool.name}", e)
false
} catch (e: IllegalArgumentException) {
logger.error("Invalid tool: ${tool.name}", e)
false
} catch (e: Exception) {
logger.error("Failed to register tool: ${tool.name}", e)
false
}
}
fun registerToolsWithRetry(tools: List<ToolCallback>, maxAttempts: Int = 3): List<String> {
val failed = mutableListOf<String>()
tools.forEach { tool ->
var attempts = 0
var success = false
while (attempts < maxAttempts && !success) {
attempts++
success = try {
serverStrategy.addToolCallback(tool)
.block(Duration.ofSeconds(5))
true
} catch (e: Exception) {
logger.warn("Attempt $attempts failed for ${tool.name}: ${e.message}")
if (attempts < maxAttempts) {
Thread.sleep(1000L * attempts) // Exponential backoff
}
false
}
}
if (!success) {
failed.add(tool.name)
logger.error("Failed to register ${tool.name} after $maxAttempts attempts")
}
}
return failed
}
companion object {
private val logger = LoggerFactory.getLogger(SyncErrorHandlingService::class.java)
}
}Comprehensive Reactive Error Handling: { .api }
@Service
class AsyncErrorHandlingService(
private val serverStrategy: McpServerStrategy,
private val toolRegistry: ToolRegistry
) {
fun registerToolWithFallback(tool: ToolCallback): Mono<Void> {
return serverStrategy.addToolCallback(tool)
.timeout(Duration.ofSeconds(5))
.doOnSuccess {
logger.info("Tool registered: ${tool.name}")
}
.doOnError { error ->
logger.error("Failed to register tool: ${tool.name}", error)
}
.onErrorResume(TimeoutException::class.java) { error ->
logger.error("Timeout registering ${tool.name}, attempting rollback")
toolRegistry.unregister(tool.name)
Mono.error(error)
}
.onErrorResume(IllegalArgumentException::class.java) { error ->
logger.error("Invalid tool ${tool.name}: ${error.message}")
Mono.empty() // Swallow error, continue
}
.onErrorResume { error ->
logger.error("Unexpected error registering ${tool.name}", error)
Mono.error(ToolRegistrationException("Failed to register ${tool.name}", error))
}
}
fun registerToolsParallel(tools: List<ToolCallback>): Mono<RegistrationResult> {
return Flux.fromIterable(tools)
.flatMap { tool ->
serverStrategy.addToolCallback(tool)
.timeout(Duration.ofSeconds(5))
.doOnSuccess { logger.info("Registered: ${tool.name}") }
.doOnError { logger.error("Failed: ${tool.name}") }
.map { tool.name to true }
.onErrorReturn(tool.name to false)
}
.collectList()
.map { results ->
val successful = results.count { it.second }
val failed = results.filterNot { it.second }.map { it.first }
RegistrationResult(
total = tools.size,
successful = successful,
failed = failed
)
}
}
fun registerToolWithRetry(tool: ToolCallback, maxAttempts: Int = 3): Mono<Void> {
return serverStrategy.addToolCallback(tool)
.timeout(Duration.ofSeconds(5))
.retryWhen(Retry.backoff(maxAttempts.toLong(), Duration.ofSeconds(1))
.maxBackoff(Duration.ofSeconds(10))
.filter { error ->
// Retry only on transient errors
error is TimeoutException ||
error is IOException ||
(error is RuntimeException && error.message?.contains("temporary") == true)
}
.doBeforeRetry { signal ->
logger.warn("Retrying ${tool.name}, attempt ${signal.totalRetries() + 1}")
}
)
.doOnSuccess {
logger.info("Tool registered after retries: ${tool.name}")
}
.onErrorResume { error ->
logger.error("Failed to register ${tool.name} after $maxAttempts attempts", error)
Mono.empty()
}
}
data class RegistrationResult(
val total: Int,
val successful: Int,
val failed: List<String>
)
class ToolRegistrationException(message: String, cause: Throwable) :
RuntimeException(message, cause)
companion object {
private val logger = LoggerFactory.getLogger(AsyncErrorHandlingService::class.java)
}
}Complex Error Handling Chains: { .api }
@Service
class ErrorChainingService(
private val serverStrategy: McpServerStrategy,
private val toolRegistry: ToolRegistry,
private val backupRegistry: ToolRegistry
) {
fun registerWithFullErrorHandling(tool: ToolCallback): Mono<Void> {
return Mono.defer { serverStrategy.addToolCallback(tool) }
.timeout(Duration.ofSeconds(5))
// Log all stages
.doOnSubscribe { logger.info("Starting registration: ${tool.name}") }
.doOnSuccess { logger.info("Successfully registered: ${tool.name}") }
.doOnError { logger.error("Error registering: ${tool.name}") }
// Retry transient errors
.retryWhen(Retry.fixedDelay(3, Duration.ofSeconds(1))
.filter { it is TimeoutException }
)
// Fallback to backup registry
.onErrorResume(TimeoutException::class.java) { error ->
logger.warn("Primary registry timeout, using backup")
backupRegistry.register(tool)
}
// Graceful degradation
.onErrorResume(RegistryFullException::class.java) { error ->
logger.error("Registry full, cannot register ${tool.name}")
Mono.empty() // Continue without this tool
}
// Validate after registration
.then(validateRegistration(tool))
// Cleanup on any error
.doOnError {
cleanup(tool)
}
}
private fun validateRegistration(tool: ToolCallback): Mono<Void> {
return toolRegistry.findToolCallback(tool.name)
.flatMap { registeredTool ->
if (registeredTool.name == tool.name) {
Mono.empty()
} else {
Mono.error(ValidationException("Tool validation failed"))
}
}
}
private fun cleanup(tool: ToolCallback) {
logger.info("Cleaning up after error: ${tool.name}")
toolRegistry.unregister(tool.name).subscribe()
}
class RegistryFullException(message: String) : RuntimeException(message)
class ValidationException(message: String) : RuntimeException(message)
companion object {
private val logger = LoggerFactory.getLogger(ErrorChainingService::class.java)
}
}Standard Error Format: { .api }
data class ToolError(
val code: ErrorCode,
val message: String,
val details: Map<String, Any> = emptyMap(),
val timestamp: Long = System.currentTimeMillis()
)
enum class ErrorCode {
INVALID_INPUT,
VALIDATION_ERROR,
TIMEOUT,
EXTERNAL_SERVICE_ERROR,
RESOURCE_NOT_FOUND,
PERMISSION_DENIED,
RATE_LIMIT_EXCEEDED,
INTERNAL_ERROR
}
object ErrorResponseFactory {
private val objectMapper = ObjectMapper()
fun createErrorResponse(error: ToolError): String {
return objectMapper.writeValueAsString(
mapOf(
"success" to false,
"error" to error
)
)
}
fun invalidInput(message: String, details: Map<String, Any> = emptyMap()): String {
return createErrorResponse(
ToolError(ErrorCode.INVALID_INPUT, message, details)
)
}
fun timeout(operation: String): String {
return createErrorResponse(
ToolError(
ErrorCode.TIMEOUT,
"Operation timed out",
mapOf("operation" to operation)
)
)
}
fun externalServiceError(service: String, cause: String): String {
return createErrorResponse(
ToolError(
ErrorCode.EXTERNAL_SERVICE_ERROR,
"External service unavailable",
mapOf("service" to service, "cause" to cause)
)
)
}
}
// Usage in tools
@Service
class ErrorAwarePublisher : McpExportToolCallbackPublisher {
private val tool = object : ToolCallback {
override fun getName() = "error_aware_tool"
override fun getDescription() = "Tool with structured errors"
override fun call(functionArguments: String): String {
return try {
val input = objectMapper.readValue(functionArguments, Input::class.java)
if (input.value <= 0) {
return ErrorResponseFactory.invalidInput(
"Value must be positive",
mapOf("provided" to input.value)
)
}
val result = performOperation(input)
serializeSuccess(result)
} catch (e: TimeoutException) {
ErrorResponseFactory.timeout("performOperation")
} catch (e: ExternalServiceException) {
ErrorResponseFactory.externalServiceError(e.serviceName, e.message ?: "Unknown")
} catch (e: Exception) {
logger.error("Unexpected error", e)
ErrorResponseFactory.createErrorResponse(
ToolError(ErrorCode.INTERNAL_ERROR, "An unexpected error occurred")
)
}
}
private fun performOperation(input: Input): String = "result"
private fun serializeSuccess(result: String) = """{"success":true,"data":"$result"}"""
}
override val toolCallbacks = listOf(tool)
override fun infoString(verbose: Boolean?, indent: Int) = "ErrorAwarePublisher"
data class Input(val value: Int)
class ExternalServiceException(val serviceName: String, message: String) :
RuntimeException(message)
companion object {
private val logger = LoggerFactory.getLogger(ErrorAwarePublisher::class.java)
private val objectMapper = ObjectMapper()
}
}Fail-Safe Publisher: { .api }
@Service
class DefensivePublisher : McpExportToolCallbackPublisher {
override val toolCallbacks: List<ToolCallback>
get() {
val tools = mutableListOf<ToolCallback>()
// Safely create each tool
safeCreateTool { createTool1() }?.let { tools.add(it) }
safeCreateTool { createTool2() }?.let { tools.add(it) }
safeCreateTool { createTool3() }?.let { tools.add(it) }
if (tools.isEmpty()) {
logger.error("No tools created successfully!")
// Return at least one fallback tool
tools.add(createFallbackTool())
}
return tools
}
override fun infoString(verbose: Boolean?, indent: Int): String {
return try {
"DefensivePublisher: ${toolCallbacks.size} tools"
} catch (e: Exception) {
logger.error("Error in infoString", e)
"DefensivePublisher: Error retrieving tool count"
}
}
private fun safeCreateTool(creator: () -> ToolCallback): ToolCallback? {
return try {
val tool = creator()
validateTool(tool)
tool
} catch (e: Exception) {
logger.error("Failed to create tool", e)
null
}
}
private fun validateTool(tool: ToolCallback) {
require(tool.name.isNotBlank()) { "Tool name cannot be blank" }
require(tool.description.isNotBlank()) { "Tool description cannot be blank" }
}
private fun createTool1(): ToolCallback = TODO()
private fun createTool2(): ToolCallback = TODO()
private fun createTool3(): ToolCallback = TODO()
private fun createFallbackTool(): ToolCallback {
return object : ToolCallback {
override fun getName() = "fallback_tool"
override fun getDescription() = "Fallback tool when primary tools fail"
override fun call(functionArguments: String) = """{"error":"Primary tools unavailable"}"""
}
}
companion object {
private val logger = LoggerFactory.getLogger(DefensivePublisher::class.java)
}
}Degrade Non-Essential Features: { .api }
@Service
class GracefulDegradationService(
private val primaryService: PrimaryService,
private val fallbackService: FallbackService,
private val cacheService: CacheService
) {
private val circuitBreaker = CircuitBreakerRegistry.ofDefaults()
.circuitBreaker("primary-service")
fun performOperation(input: String): String {
return try {
// Try primary service with circuit breaker
circuitBreaker.executeSupplier {
primaryService.execute(input)
}
} catch (e: CallNotPermittedException) {
logger.warn("Circuit breaker open, using fallback")
// Circuit breaker open, use fallback
useFallback(input)
} catch (e: Exception) {
logger.error("Primary service failed, attempting degradation", e)
// Attempt degradation chain
degradeGracefully(input, e)
}
}
private fun degradeGracefully(input: String, originalError: Exception): String {
// Level 1: Try fallback service
return try {
logger.info("Attempting fallback service")
fallbackService.execute(input)
} catch (e: Exception) {
logger.error("Fallback service failed, trying cache", e)
// Level 2: Try cached result
try {
val cached = cacheService.get(input)
if (cached != null) {
logger.info("Returning cached result")
return cached
}
// Level 3: Return safe default
logger.error("No cached result, returning safe default")
createSafeDefault(input)
} catch (e: Exception) {
logger.error("Cache access failed, returning safe default", e)
createSafeDefault(input)
}
}
}
private fun useFallback(input: String): String {
return try {
fallbackService.execute(input)
} catch (e: Exception) {
logger.error("Fallback also failed", e)
createSafeDefault(input)
}
}
private fun createSafeDefault(input: String): String {
return """{"success":false,"error":"Service temporarily unavailable","retry":true}"""
}
companion object {
private val logger = LoggerFactory.getLogger(GracefulDegradationService::class.java)
}
}Self-Healing Service: { .api }
@Service
class SelfHealingService(
private val serverStrategy: McpServerStrategy,
private val toolRegistry: ToolRegistry,
private val healthChecker: HealthChecker
) {
@Scheduled(fixedRate = 60000) // Every minute
fun performHealthCheck() {
val healthStatus = healthChecker.checkHealth()
if (!healthStatus.isHealthy) {
logger.warn("Unhealthy state detected: ${healthStatus.issues}")
attemptRecovery(healthStatus)
}
}
private fun attemptRecovery(healthStatus: HealthStatus) {
healthStatus.issues.forEach { issue ->
when (issue) {
is MissingToolsIssue -> recoverMissingTools(issue)
is StaleConnectionIssue -> recoverConnections(issue)
is MemoryIssue -> recoverMemory(issue)
else -> logger.warn("No recovery strategy for: $issue")
}
}
}
private fun recoverMissingTools(issue: MissingToolsIssue) {
logger.info("Attempting to recover ${issue.missingTools.size} missing tools")
issue.missingTools.forEach { toolName ->
try {
val tool = recreateTool(toolName)
serverStrategy.addToolCallback(tool)
.timeout(Duration.ofSeconds(5))
.subscribe(
{ logger.info("Recovered tool: $toolName") },
{ error -> logger.error("Failed to recover tool: $toolName", error) }
)
} catch (e: Exception) {
logger.error("Failed to recreate tool: $toolName", e)
}
}
}
private fun recoverConnections(issue: StaleConnectionIssue) {
logger.info("Attempting to recover connections")
// Reconnect logic
}
private fun recoverMemory(issue: MemoryIssue) {
logger.info("Attempting memory recovery")
System.gc()
// Clear caches
// Remove unused resources
}
private fun recreateTool(toolName: String): ToolCallback = TODO()
data class HealthStatus(val isHealthy: Boolean, val issues: List<HealthIssue>)
interface HealthIssue
data class MissingToolsIssue(val missingTools: List<String>) : HealthIssue
data class StaleConnectionIssue(val count: Int) : HealthIssue
data class MemoryIssue(val usagePercent: Int) : HealthIssue
companion object {
private val logger = LoggerFactory.getLogger(SelfHealingService::class.java)
}
}Resilience4j Integration: { .api }
@Service
class CircuitBreakerPublisher : McpExportToolCallbackPublisher {
private val circuitBreaker = CircuitBreakerRegistry.of(
CircuitBreakerConfig.custom()
.failureRateThreshold(50.0) // Open at 50% failure
.waitDurationInOpenState(Duration.ofSeconds(30)) // Wait 30s before half-open
.slidingWindowSize(10) // Track last 10 calls
.permittedNumberOfCallsInHalfOpenState(3) // Allow 3 test calls
.build()
).circuitBreaker("external-service")
private val protectedTool = object : ToolCallback {
override fun getName() = "protected_tool"
override fun getDescription() = "Tool with circuit breaker protection"
override fun call(functionArguments: String): String {
return try {
circuitBreaker.executeSupplier {
callExternalService(functionArguments)
}
} catch (e: CallNotPermittedException) {
logger.warn("Circuit breaker open, returning cached result")
getCachedResult(functionArguments)
} catch (e: Exception) {
logger.error("Tool execution failed", e)
ErrorResponseFactory.externalServiceError("external-service", e.message ?: "Unknown")
}
}
private fun callExternalService(args: String): String {
// Potentially failing external call
return externalService.call(args)
}
private fun getCachedResult(args: String): String {
return """{"success":true,"data":"cached-result","cached":true}"""
}
}
override val toolCallbacks = listOf(protectedTool)
override fun infoString(verbose: Boolean?, indent: Int) = "CircuitBreakerPublisher"
fun getCircuitBreakerMetrics(): String {
val metrics = circuitBreaker.metrics
return """
Circuit Breaker State: ${circuitBreaker.state}
Failure Rate: ${metrics.failureRate}%
Slow Call Rate: ${metrics.slowCallRate}%
Number of Failed Calls: ${metrics.numberOfFailedCalls}
Number of Successful Calls: ${metrics.numberOfSuccessfulCalls}
""".trimIndent()
}
companion object {
private val logger = LoggerFactory.getLogger(CircuitBreakerPublisher::class.java)
private val externalService = ExternalService()
}
class ExternalService {
fun call(args: String): String = "result"
}
}Advanced Retry Logic: { .api }
@Service
class RetryService(
private val serverStrategy: McpServerStrategy
) {
fun registerWithExponentialBackoff(tool: ToolCallback): Mono<Void> {
return serverStrategy.addToolCallback(tool)
.retryWhen(
Retry.backoff(5, Duration.ofSeconds(1))
.maxBackoff(Duration.ofSeconds(30))
.jitter(0.5) // Add randomness to prevent thundering herd
.filter { error -> isRetryable(error) }
.doBeforeRetry { signal ->
logger.info(
"Retrying ${tool.name}, attempt ${signal.totalRetries() + 1}, " +
"error: ${signal.failure().message}"
)
}
.onRetryExhaustedThrow { _, retrySignal ->
MaxRetriesExceededException(
"Failed after ${retrySignal.totalRetries()} attempts",
retrySignal.failure()
)
}
)
}
fun registerWithCustomRetry(tool: ToolCallback): Mono<Void> {
return Mono.defer { serverStrategy.addToolCallback(tool) }
.retryWhen(Retry.from { companion ->
companion.zipWith(Flux.range(1, 5)) { signal, attempt ->
when {
!isRetryable(signal.failure()) -> {
throw signal.failure()
}
attempt >= 5 -> {
throw MaxRetriesExceededException(
"Max retries exceeded",
signal.failure()
)
}
else -> {
val delay = calculateBackoff(attempt, signal.failure())
logger.info("Retry attempt $attempt after ${delay}ms")
delay
}
}
}.flatMap { delay -> Mono.delay(Duration.ofMillis(delay)) }
})
}
private fun isRetryable(error: Throwable): Boolean {
return when (error) {
is TimeoutException -> true
is IOException -> true
is ConnectException -> true
is TransientException -> true
else -> false
}
}
private fun calculateBackoff(attempt: Int, error: Throwable): Long {
val baseDelay = 1000L // 1 second
val maxDelay = 30000L // 30 seconds
return when (error) {
is TimeoutException -> minOf(baseDelay * (1L shl attempt), maxDelay) // Exponential
is IOException -> baseDelay * attempt // Linear
else -> baseDelay
}
}
class MaxRetriesExceededException(message: String, cause: Throwable) :
RuntimeException(message, cause)
class TransientException(message: String) : RuntimeException(message)
companion object {
private val logger = LoggerFactory.getLogger(RetryService::class.java)
}
}Multi-Level Fallback: { .api }
@Service
class FallbackPublisher : McpExportToolCallbackPublisher {
private val multiLevelTool = object : ToolCallback {
override fun getName() = "multi_level_fallback"
override fun getDescription() = "Tool with multiple fallback levels"
override fun call(functionArguments: String): String {
// Level 1: Primary service
tryPrimaryService(functionArguments)?.let { return it }
// Level 2: Secondary service
trySecondaryService(functionArguments)?.let { return it }
// Level 3: Cached result
tryCachedResult(functionArguments)?.let { return it }
// Level 4: Degraded service
tryDegradedService(functionArguments)?.let { return it }
// Level 5: Safe default
return createSafeDefault(functionArguments)
}
private fun tryPrimaryService(args: String): String? {
return try {
logger.debug("Trying primary service")
primaryService.execute(args)
} catch (e: Exception) {
logger.warn("Primary service failed: ${e.message}")
null
}
}
private fun trySecondaryService(args: String): String? {
return try {
logger.debug("Trying secondary service")
secondaryService.execute(args)
} catch (e: Exception) {
logger.warn("Secondary service failed: ${e.message}")
null
}
}
private fun tryCachedResult(args: String): String? {
return try {
logger.debug("Trying cache")
cache.get(args)
} catch (e: Exception) {
logger.warn("Cache access failed: ${e.message}")
null
}
}
private fun tryDegradedService(args: String): String? {
return try {
logger.debug("Using degraded service")
degradedService.execute(args)
} catch (e: Exception) {
logger.warn("Degraded service failed: ${e.message}")
null
}
}
private fun createSafeDefault(args: String): String {
logger.info("All fallbacks failed, returning safe default")
return """{"success":false,"message":"Service temporarily unavailable","retry":true}"""
}
}
override val toolCallbacks = listOf(multiLevelTool)
override fun infoString(verbose: Boolean?, indent: Int) = "FallbackPublisher"
companion object {
private val logger = LoggerFactory.getLogger(FallbackPublisher::class.java)
private val primaryService = Service("primary")
private val secondaryService = Service("secondary")
private val degradedService = Service("degraded")
private val cache = Cache()
}
class Service(private val name: String) {
fun execute(args: String): String = "result from $name"
}
class Cache {
fun get(key: String): String? = null
}
}Comprehensive Error Logging: { .api }
@Aspect
@Component
class ErrorLoggingAspect(
private val meterRegistry: MeterRegistry
) {
@Around("@annotation(com.embabel.agent.mcpserver.ErrorMonitored)")
fun logErrors(joinPoint: ProceedingJoinPoint): Any? {
val methodName = joinPoint.signature.name
val className = joinPoint.target.javaClass.simpleName
return try {
val result = joinPoint.proceed()
meterRegistry.counter("method.success", "class", className, "method", methodName).increment()
result
} catch (e: Exception) {
// Record error metrics
meterRegistry.counter(
"method.error",
"class", className,
"method", methodName,
"error", e.javaClass.simpleName
).increment()
// Structured logging
logger.error(
"Method execution failed: $className.$methodName",
StructuredArguments.kv("class", className),
StructuredArguments.kv("method", methodName),
StructuredArguments.kv("error_type", e.javaClass.simpleName),
StructuredArguments.kv("error_message", e.message),
e
)
// Alert on critical errors
if (isCritical(e)) {
alertingService.sendAlert(
"Critical error in $className.$methodName: ${e.message}"
)
}
throw e
}
}
private fun isCritical(error: Exception): Boolean {
return error is OutOfMemoryError ||
error is StackOverflowError ||
error.message?.contains("critical", ignoreCase = true) == true
}
companion object {
private val logger = LoggerFactory.getLogger(ErrorLoggingAspect::class.java)
private val alertingService = AlertingService()
}
class AlertingService {
fun sendAlert(message: String) {
// Send alert via email, Slack, PagerDuty, etc.
}
}
}
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class ErrorMonitored