CtrlK
CommunityDocumentationLog inGet started
Tessl Logo

tessl/maven-com-embabel-agent--embabel-agent-mcpserver

Discover and Export available Agent(s) as MCP Servers

Overview
Eval results
Files

error-handling.mddocs/advanced/

Error Handling

Comprehensive guide to error handling strategies, graceful degradation, recovery patterns, and best practices for embabel-agent-mcpserver.

Table of Contents

Error Handling Overview

Error Handling Philosophy

Key Principles:

  1. Fail Fast: Detect and report errors early
  2. Fail Safe: Continue operation when possible
  3. Clear Messages: Provide actionable error information
  4. Observability: Log errors for debugging and monitoring
  5. User Experience: Return meaningful errors to clients

Error Flow

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 error

Error Categories

1. Configuration Errors

Description: Errors during server initialization or configuration

Examples:

  • Invalid execution mode
  • Missing required properties
  • Bean creation failures
  • Publisher initialization errors

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
        )
    }
}

2. Registration Errors

Description: Errors when registering tools, resources, or prompts

Examples:

  • Duplicate tool names
  • Invalid tool specifications
  • Registration timeouts
  • Registry full

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()
}

3. Invocation Errors

Description: Errors during tool execution

Examples:

  • Invalid input parameters
  • Business logic errors
  • External service failures
  • Timeout errors

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()
    }
}

4. Runtime Errors

Description: Errors during server operation

Examples:

  • Memory exhaustion
  • Thread pool saturation
  • Connection failures
  • Resource leaks

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)
    }
}

Synchronous Error Handling

Try-Catch Pattern

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)
    }
}

Asynchronous Error Handling

Reactive Error Operators

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)
    }
}

Error Chaining

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)
    }
}

Tool Error Patterns

Structured Error Responses

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()
    }
}

Publisher Error Patterns

Defensive Publisher Implementation

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)
    }
}

Graceful Degradation

Feature Degradation

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)
    }
}

Recovery Strategies

Automatic Recovery

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)
    }
}

Circuit Breaker Pattern

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"
    }
}

Retry Strategies

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)
    }
}

Fallback Mechanisms

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
    }
}

Error Logging and Monitoring

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

Related Documentation

  • Architecture - Architecture overview
  • Performance - Performance optimization
  • Custom Implementations - Extensions
  • Server Strategy API - Strategy interface
  • Integration Patterns Guide - Complete examples
tessl i tessl/maven-com-embabel-agent--embabel-agent-mcpserver@0.3.1

docs

advanced

architecture.md

custom-implementations.md

embabel-integration.md

error-handling.md

imports.md

performance.md

index.md

tile.json