Discover and Export available Agent(s) as MCP Servers
Comprehensive guide to creating custom configurations, strategies, factories, and extending base classes in embabel-agent-mcpserver.
The library is designed for extensibility at every layer. This guide shows how to extend and customize components to meet specific requirements.
┌─────────────────────────────────────────────┐
│ Extension Points │
├─────────────────────────────────────────────┤
│ │
│ Publishers │
│ ├─ McpExportToolCallbackPublisher │
│ ├─ McpResourcePublisher │
│ └─ McpPromptPublisher │
│ │
│ Registries │
│ └─ ToolRegistry │
│ │
│ Strategies │
│ └─ McpServerStrategy │
│ │
│ Factories │
│ ├─ Resource Factories │
│ └─ Prompt Factories │
│ │
│ Configuration │
│ ├─ AbstractMcpServerConfiguration │
│ └─ Custom Config Classes │
│ │
│ Utilities │
│ ├─ StringTransformer │
│ ├─ HealthIndicator │
│ └─ Error Handlers │
└─────────────────────────────────────────────┘Database-Backed Publisher: { .api }
@Service
class DatabaseToolPublisher(
private val toolRepository: ToolDefinitionRepository,
private val toolFactory: DynamicToolFactory
) : McpExportToolCallbackPublisher {
override val toolCallbacks: List<ToolCallback> by lazy {
logger.info("Loading tools from database...")
toolRepository.findAllActive()
.mapNotNull { definition ->
try {
toolFactory.createTool(definition)
} catch (e: Exception) {
logger.error("Failed to create tool: ${definition.name}", e)
null
}
}
}
override fun infoString(verbose: Boolean?, indent: Int): String {
return "DatabaseToolPublisher: ${toolCallbacks.size} tools"
}
companion object {
private val logger = LoggerFactory.getLogger(DatabaseToolPublisher::class.java)
}
}
@Repository
interface ToolDefinitionRepository : JpaRepository<ToolDefinition, Long> {
fun findAllActive(): List<ToolDefinition>
}
@Entity
data class ToolDefinition(
@Id @GeneratedValue
val id: Long? = null,
val name: String,
val description: String,
val implementation: String, // Script or class name
val inputSchema: String, // JSON schema
val active: Boolean = true
)
@Component
class DynamicToolFactory {
fun createTool(definition: ToolDefinition): ToolCallback {
return object : ToolCallback {
override fun getName() = definition.name
override fun getDescription() = definition.description
override fun getInputTypeSchema() = definition.inputSchema
override fun call(functionArguments: String): String {
// Execute dynamic implementation
return executeScript(definition.implementation, functionArguments)
}
}
}
private fun executeScript(script: String, args: String): String {
// Script execution logic (Groovy, JavaScript, etc.)
return "result"
}
}Load Tools from Plugins: { .api }
@Service
class PluginToolPublisher(
private val pluginLoader: PluginLoader
) : McpExportToolCallbackPublisher {
override val toolCallbacks: List<ToolCallback> by lazy {
logger.info("Loading tools from plugins...")
val plugins = pluginLoader.loadPlugins()
plugins.flatMap { plugin ->
try {
plugin.getTools()
} catch (e: Exception) {
logger.error("Failed to load tools from plugin: ${plugin.name}", e)
emptyList()
}
}
}
override fun infoString(verbose: Boolean?, indent: Int): String {
return "PluginToolPublisher: ${toolCallbacks.size} tools"
}
companion object {
private val logger = LoggerFactory.getLogger(PluginToolPublisher::class.java)
}
}
interface Plugin {
val name: String
val version: String
fun getTools(): List<ToolCallback>
fun initialize()
fun shutdown()
}
@Component
class PluginLoader(
@Value("\${plugins.directory:./plugins}") private val pluginsDir: String
) {
private val classLoader = URLClassLoader.newInstance(
File(pluginsDir).listFiles { file -> file.extension == "jar" }
?.map { it.toURI().toURL() }
?.toTypedArray() ?: emptyArray()
)
fun loadPlugins(): List<Plugin> {
return ServiceLoader.load(Plugin::class.java, classLoader).toList()
}
}Combine Multiple Publishers: { .api }
@Service
class CompositePublisher(
private val toolPublishers: List<McpExportToolCallbackPublisher>,
private val resourcePublishers: List<McpResourcePublisher>,
private val promptPublishers: List<McpPromptPublisher>
) : McpExportToolCallbackPublisher, McpResourcePublisher, McpPromptPublisher {
// Aggregate tools from all tool publishers
override val toolCallbacks: List<ToolCallback> by lazy {
toolPublishers.flatMap { publisher ->
try {
publisher.toolCallbacks
} catch (e: Exception) {
logger.error("Failed to get tools from ${publisher::class.simpleName}", e)
emptyList()
}
}
}
// Aggregate resources from all resource publishers
override fun resources(): List<McpServerFeatures.SyncResourceSpecification> {
return resourcePublishers.flatMap { publisher ->
try {
publisher.resources()
} catch (e: Exception) {
logger.error("Failed to get resources from ${publisher::class.simpleName}", e)
emptyList()
}
}
}
// Aggregate prompts from all prompt publishers
override fun prompts(): List<McpServerFeatures.SyncPromptSpecification> {
return promptPublishers.flatMap { publisher ->
try {
publisher.prompts()
} catch (e: Exception) {
logger.error("Failed to get prompts from ${publisher::class.simpleName}", e)
emptyList()
}
}
}
override fun infoString(verbose: Boolean?, indent: Int): String {
return buildString {
appendLine("CompositePublisher:")
appendLine(" Tools: ${toolCallbacks.size} from ${toolPublishers.size} publishers")
appendLine(" Resources: ${resources().size} from ${resourcePublishers.size} publishers")
appendLine(" Prompts: ${prompts().size} from ${promptPublishers.size} publishers")
}
}
companion object {
private val logger = LoggerFactory.getLogger(CompositePublisher::class.java)
}
}Distributed Tool Storage: { .api }
@Service
@Primary // Replace default registry
class RedisToolRegistry(
private val redisTemplate: RedisTemplate<String, ToolCallback>,
private val objectMapper: ObjectMapper
) : ToolRegistry {
private val keyPrefix = "mcp:tool:"
override fun register(toolCallback: ToolCallback): Mono<Void> {
return Mono.fromRunnable {
val key = keyPrefix + toolCallback.name
redisTemplate.opsForValue().set(key, toolCallback)
logger.info("Registered tool in Redis: ${toolCallback.name}")
}.subscribeOn(Schedulers.boundedElastic())
}
override fun unregister(toolName: String): Mono<Void> {
return Mono.fromRunnable {
val key = keyPrefix + toolName
redisTemplate.delete(key)
logger.info("Unregistered tool from Redis: $toolName")
}.subscribeOn(Schedulers.boundedElastic())
}
override fun findToolCallback(toolName: String): Mono<ToolCallback> {
return Mono.fromCallable {
val key = keyPrefix + toolName
redisTemplate.opsForValue().get(key)
?: throw ToolNotFoundException("Tool not found: $toolName")
}.subscribeOn(Schedulers.boundedElastic())
}
override fun listToolCallbacks(): Mono<List<ToolCallback>> {
return Mono.fromCallable {
val pattern = "$keyPrefix*"
val keys = redisTemplate.keys(pattern)
keys.mapNotNull { key ->
redisTemplate.opsForValue().get(key)
}
}.subscribeOn(Schedulers.boundedElastic())
}
override fun containsTool(toolName: String): Mono<Boolean> {
return Mono.fromCallable {
val key = keyPrefix + toolName
redisTemplate.hasKey(key)
}.subscribeOn(Schedulers.boundedElastic())
}
companion object {
private val logger = LoggerFactory.getLogger(RedisToolRegistry::class.java)
}
}
// Redis configuration
@Configuration
class RedisConfig {
@Bean
fun redisTemplate(
connectionFactory: RedisConnectionFactory,
objectMapper: ObjectMapper
): RedisTemplate<String, ToolCallback> {
val template = RedisTemplate<String, ToolCallback>()
template.connectionFactory = connectionFactory
// Custom serialization for ToolCallback
template.keySerializer = StringRedisSerializer()
template.valueSerializer = Jackson2JsonRedisSerializer(objectMapper, ToolCallback::class.java)
return template
}
}Multi-Level Tool Namespaces: { .api }
@Service
class HierarchicalToolRegistry : ToolRegistry {
private val rootRegistry = ConcurrentHashMap<String, ToolCallback>()
private val namespaceRegistries = ConcurrentHashMap<String, ConcurrentHashMap<String, ToolCallback>>()
// Register tool in namespace
fun registerInNamespace(namespace: String, toolCallback: ToolCallback): Mono<Void> {
return Mono.fromRunnable {
val registry = namespaceRegistries.computeIfAbsent(namespace) {
ConcurrentHashMap()
}
registry[toolCallback.name] = toolCallback
logger.info("Registered ${toolCallback.name} in namespace: $namespace")
}
}
// Find tool in specific namespace
fun findInNamespace(namespace: String, toolName: String): Mono<ToolCallback> {
return Mono.fromCallable {
namespaceRegistries[namespace]?.get(toolName)
?: throw ToolNotFoundException("Tool not found in namespace $namespace: $toolName")
}
}
// List tools in namespace
fun listInNamespace(namespace: String): Mono<List<ToolCallback>> {
return Mono.fromCallable {
namespaceRegistries[namespace]?.values?.toList() ?: emptyList()
}
}
// Default ToolRegistry implementation (searches all namespaces)
override fun register(toolCallback: ToolCallback): Mono<Void> {
return Mono.fromRunnable {
rootRegistry[toolCallback.name] = toolCallback
}
}
override fun findToolCallback(toolName: String): Mono<ToolCallback> {
return Mono.fromCallable {
// Search root registry
rootRegistry[toolName]
?: // Search all namespaces
namespaceRegistries.values
.mapNotNull { it[toolName] }
.firstOrNull()
?: throw ToolNotFoundException(toolName)
}
}
override fun listToolCallbacks(): Mono<List<ToolCallback>> {
return Mono.fromCallable {
val allTools = rootRegistry.values.toMutableList()
namespaceRegistries.values.forEach { registry ->
allTools.addAll(registry.values)
}
allTools
}
}
override fun unregister(toolName: String): Mono<Void> {
return Mono.fromRunnable {
rootRegistry.remove(toolName)
namespaceRegistries.values.forEach { it.remove(toolName) }
}
}
override fun containsTool(toolName: String): Mono<Boolean> {
return findToolCallback(toolName)
.map { true }
.onErrorReturn(false)
}
companion object {
private val logger = LoggerFactory.getLogger(HierarchicalToolRegistry::class.java)
}
}
// Usage
@Service
class NamespacedPublisher(
private val registry: HierarchicalToolRegistry
) {
@PostConstruct
fun registerTools() {
registry.registerInNamespace("api", createApiTool()).subscribe()
registry.registerInNamespace("admin", createAdminTool()).subscribe()
registry.registerInNamespace("internal", createInternalTool()).subscribe()
}
private fun createApiTool(): ToolCallback = TODO()
private fun createAdminTool(): ToolCallback = TODO()
private fun createInternalTool(): ToolCallback = TODO()
}Combine Sync and Async Behaviors: { .api }
@Service
class HybridServerStrategy(
private val syncServer: McpSyncServer,
private val asyncServer: McpAsyncServer,
private val toolRegistry: ToolRegistry,
private val serverInfo: ServerInfo,
@Value("\${mcp.hybrid.mode:ADAPTIVE}") private val hybridMode: HybridMode
) : McpServerStrategy {
enum class HybridMode {
ADAPTIVE, // Choose based on tool characteristics
PREFER_SYNC, // Use sync unless tool requires async
PREFER_ASYNC // Use async unless tool requires sync
}
override fun addToolCallback(toolCallback: ToolCallback): Mono<Void> {
return when (selectMode(toolCallback)) {
McpExecutionMode.SYNC -> {
Mono.fromRunnable {
syncServer.registerTool(toolCallback)
toolRegistry.register(toolCallback).block()
}
}
McpExecutionMode.ASYNC -> {
asyncServer.registerToolAsync(toolCallback)
.then(toolRegistry.register(toolCallback))
}
}
}
override fun removeToolCallback(toolName: String): Mono<Void> {
return toolRegistry.findToolCallback(toolName)
.flatMap { tool ->
when (selectMode(tool)) {
McpExecutionMode.SYNC -> Mono.fromRunnable {
syncServer.unregisterTool(toolName)
}
McpExecutionMode.ASYNC -> asyncServer.unregisterToolAsync(toolName)
}
}
.then(toolRegistry.unregister(toolName))
}
override fun getServerInfo(): Mono<ServerInfo> {
return Mono.just(serverInfo.copy(
executionMode = McpExecutionMode.SYNC, // Report as sync (primary)
capabilities = serverInfo.capabilities + McpCapability.HYBRID
))
}
override fun getExecutionMode(): McpExecutionMode {
return McpExecutionMode.SYNC // Report primary mode
}
override fun checkHealth(): Mono<ServerHealthStatus> {
return Mono.zip(
checkSyncHealth(),
checkAsyncHealth()
).map { (syncHealth, asyncHealth) ->
ServerHealthStatus(
isHealthy = syncHealth.isHealthy && asyncHealth.isHealthy,
message = "Hybrid mode: sync=${syncHealth.isHealthy}, async=${asyncHealth.isHealthy}",
details = mapOf(
"sync" to syncHealth,
"async" to asyncHealth
)
)
}
}
private fun selectMode(tool: ToolCallback): McpExecutionMode {
return when (hybridMode) {
HybridMode.ADAPTIVE -> {
// Heuristics: check if tool is I/O-bound, reactive, etc.
if (isIOBound(tool) || isReactive(tool)) {
McpExecutionMode.ASYNC
} else {
McpExecutionMode.SYNC
}
}
HybridMode.PREFER_SYNC -> {
if (requiresAsync(tool)) McpExecutionMode.ASYNC else McpExecutionMode.SYNC
}
HybridMode.PREFER_ASYNC -> {
if (requiresSync(tool)) McpExecutionMode.SYNC else McpExecutionMode.ASYNC
}
}
}
private fun isIOBound(tool: ToolCallback): Boolean {
// Check tool metadata, annotations, or characteristics
return tool.description.contains("database") ||
tool.description.contains("http") ||
tool.description.contains("file")
}
private fun isReactive(tool: ToolCallback): Boolean {
// Check if tool returns reactive types
return false // Implement based on tool inspection
}
private fun requiresAsync(tool: ToolCallback): Boolean = false
private fun requiresSync(tool: ToolCallback): Boolean = false
private fun checkSyncHealth(): Mono<ServerHealthStatus> = TODO()
private fun checkAsyncHealth(): Mono<ServerHealthStatus> = TODO()
}
enum class McpCapability {
TOOLS, RESOURCES, PROMPTS, HYBRID
}Version and Environment Aware: { .api }
class VersionedEnvironmentNamingStrategy(
private val version: String,
private val environment: String,
private val namespace: String
) : StringTransformer {
override fun transform(input: String): String {
return buildString {
// Environment prefix (dev, staging, prod)
append(environment.lowercase())
append("_")
// API version
append(version.replace(".", "_"))
append("_")
// Namespace
append(namespace.lowercase())
append("_")
// Original name
append(sanitize(input))
}
}
private fun sanitize(input: String): String {
return input
.replace(Regex("[^a-zA-Z0-9_]"), "_")
.lowercase()
.take(50)
}
}
// Usage
@Service
class VersionedPublisher(
@Value("\${api.version}") private val apiVersion: String,
@Value("\${spring.profiles.active}") private val environment: String
) : McpExportToolCallbackPublisher {
private val namingStrategy = VersionedEnvironmentNamingStrategy(
version = apiVersion,
environment = environment,
namespace = "api"
)
override val toolCallbacks: List<ToolCallback>
get() {
val toolObject = ToolObject(
objects = listOf(tool1, tool2),
namingStrategy = namingStrategy
)
return McpToolExport.fromToolObject(toolObject).toolCallbacks
}
override fun infoString(verbose: Boolean?, indent: Int) = "VersionedPublisher"
private val tool1: Any = TODO()
private val tool2: Any = TODO()
}
// Results in names like: "prod_v2_0_api_toolname"Tenant and User Context: { .api }
class TenantAwareNamingStrategy(
private val tenantResolver: TenantResolver,
private val baseStrategy: StringTransformer
) : StringTransformer {
override fun transform(input: String): String {
val tenant = tenantResolver.getCurrentTenant()
val baseName = baseStrategy.transform(input)
return "${tenant.id}_$baseName"
}
}
interface TenantResolver {
fun getCurrentTenant(): Tenant
}
data class Tenant(val id: String, val name: String)
@Component
class RequestContextTenantResolver : TenantResolver {
override fun getCurrentTenant(): Tenant {
// Extract from request context, security context, etc.
return Tenant("tenant1", "Example Tenant")
}
}Resource Generation from Templates: { .api }
@Component
class TemplateResourceFactory(
private val templateEngine: TemplateEngine,
private val resourceLoader: ResourceLoader
) {
fun createFromTemplate(
uri: String,
name: String,
templatePath: String,
dataProvider: () -> Map<String, Any>
): SyncResourceSpecification {
return SyncResourceSpecificationFactory.syncResourceSpecification(
uri = uri,
name = name,
description = "Generated from template: $templatePath",
resourceLoader = { exchange ->
val template = loadTemplate(templatePath)
val data = dataProvider()
renderTemplate(template, data)
},
mimeType = "text/html"
)
}
private fun loadTemplate(path: String): String {
return resourceLoader.getResource(path)
.inputStream
.bufferedReader()
.use { it.readText() }
}
private fun renderTemplate(template: String, data: Map<String, Any>): String {
return templateEngine.process(template, data)
}
}
@Service
class TemplateResourcePublisher(
private val factory: TemplateResourceFactory
) : McpResourcePublisher {
override fun resources(): List<SyncResourceSpecification> {
return listOf(
factory.createFromTemplate(
uri = "app://docs/api-reference",
name = "APIReference",
templatePath = "classpath:templates/api-reference.html",
dataProvider = { getApiData() }
),
factory.createFromTemplate(
uri = "app://reports/dashboard",
name = "Dashboard",
templatePath = "classpath:templates/dashboard.html",
dataProvider = { getDashboardData() }
)
)
}
override fun infoString(verbose: Boolean?, indent: Int) = "TemplateResourcePublisher"
private fun getApiData(): Map<String, Any> = mapOf(
"endpoints" to listOf("/api/v1/users", "/api/v1/data"),
"version" to "1.0.0"
)
private fun getDashboardData(): Map<String, Any> = mapOf(
"stats" to mapOf("users" to 100, "requests" to 1000)
)
}
interface TemplateEngine {
fun process(template: String, data: Map<String, Any>): String
}Generate Prompts with AI Assistance: { .api }
@Component
class IntelligentPromptFactory(
private val schemaGenerator: JsonSchemaGenerator,
private val descriptionEnhancer: DescriptionEnhancer
) {
fun createIntelligentPrompt(
goalName: String,
inputType: Class<*>,
baseDescription: String
): SyncPromptSpecification {
// Generate JSON schema with enhanced descriptions
val schema = schemaGenerator.generateSchema(inputType)
val enhancedSchema = descriptionEnhancer.enhanceSchema(schema)
// Create goal with enhanced description
val goal = object : Named, Described {
override val name = goalName
override val description = descriptionEnhancer.enhanceDescription(baseDescription)
}
return McpPromptFactory().syncPromptSpecificationForType(goal, inputType)
}
fun createConversationalPrompt(
goalName: String,
inputType: Class<*>,
conversationStarter: String
): SyncPromptSpecification {
val goal = object : Named, Described {
override val name = goalName
override val description = """
$conversationStarter
Please provide the following information:
${generateFieldPrompts(inputType)}
""".trimIndent()
}
return McpPromptFactory().syncPromptSpecificationForType(goal, inputType)
}
private fun generateFieldPrompts(inputType: Class<*>): String {
return inputType.declaredFields
.joinToString("\n") { field ->
val annotation = field.getAnnotation(JsonPropertyDescription::class.java)
"- ${field.name}: ${annotation?.value ?: "Required field"}"
}
}
}
@Component
class DescriptionEnhancer {
fun enhanceDescription(description: String): String {
// Use AI or NLP to enhance descriptions
return description + " (Enhanced with contextual information)"
}
fun enhanceSchema(schema: JsonSchema): JsonSchema {
// Add examples, constraints, better descriptions
return schema
}
}Environment-Specific Settings: { .api }
@Configuration
@ConfigurationProperties(prefix = "mcp.custom")
data class CustomMcpConfiguration(
var defaultTimeout: Long = 30000,
var maxToolsPerPublisher: Int = 100,
var enableMetrics: Boolean = true,
var enableCaching: Boolean = true,
var environments: Map<String, EnvironmentConfig> = emptyMap()
)
data class EnvironmentConfig(
var timeout: Long? = null,
var maxTools: Int? = null,
var features: Set<String> = emptySet()
)
@Configuration
class EnvironmentAwareConfiguration(
private val customConfig: CustomMcpConfiguration,
private val environment: Environment
) {
@Bean
fun environmentSettings(): EnvironmentSettings {
val activeProfile = environment.activeProfiles.firstOrNull() ?: "default"
val envConfig = customConfig.environments[activeProfile]
return EnvironmentSettings(
timeout = envConfig?.timeout ?: customConfig.defaultTimeout,
maxTools = envConfig?.maxTools ?: customConfig.maxToolsPerPublisher,
features = envConfig?.features ?: emptySet()
)
}
}
data class EnvironmentSettings(
val timeout: Long,
val maxTools: Int,
val features: Set<String>
)
// application.yml
/*
mcp:
custom:
default-timeout: 30000
max-tools-per-publisher: 100
enable-metrics: true
enable-caching: true
environments:
development:
timeout: 60000
max-tools: 200
features: [debug, profiling]
production:
timeout: 15000
max-tools: 50
features: [metrics, alerting]
*/Multi-Aspect Health Monitoring: { .api }
@Component
class ComprehensiveMcpHealthIndicator(
private val serverStrategy: McpServerStrategy,
private val toolRegistry: ToolRegistry,
private val meterRegistry: MeterRegistry
) : HealthIndicator {
override fun health(): Health {
val checks = listOf(
checkServerStatus(),
checkToolRegistry(),
checkMemoryUsage(),
checkThreadPool(),
checkErrorRate()
)
val allHealthy = checks.all { it.status == Status.UP }
val builder = if (allHealthy) Health.up() else Health.down()
checks.forEach { check ->
builder.withDetail(check.name, check.details)
}
return builder.build()
}
private fun checkServerStatus(): HealthCheck {
return try {
val serverInfo = serverStrategy.getServerInfo()
.block(Duration.ofSeconds(1))
HealthCheck(
name = "server",
status = Status.UP,
details = mapOf(
"name" to (serverInfo?.name ?: "unknown"),
"mode" to (serverInfo?.executionMode?.toString() ?: "unknown")
)
)
} catch (e: Exception) {
HealthCheck(
name = "server",
status = Status.DOWN,
details = mapOf("error" to (e.message ?: "Unknown error"))
)
}
}
private fun checkToolRegistry(): HealthCheck {
return try {
val toolCount = toolRegistry.listToolCallbacks()
.map { it.size }
.block(Duration.ofSeconds(1))
val status = if ((toolCount ?: 0) > 0) Status.UP else Status.DOWN
HealthCheck(
name = "tools",
status = status,
details = mapOf("count" to (toolCount ?: 0))
)
} catch (e: Exception) {
HealthCheck(
name = "tools",
status = Status.DOWN,
details = mapOf("error" to (e.message ?: "Unknown error"))
)
}
}
private fun checkMemoryUsage(): HealthCheck {
val runtime = Runtime.getRuntime()
val usedMemory = runtime.totalMemory() - runtime.freeMemory()
val maxMemory = runtime.maxMemory()
val usagePercent = (usedMemory.toDouble() / maxMemory * 100).toInt()
val status = when {
usagePercent > 90 -> Status.DOWN
usagePercent > 80 -> Status.OUT_OF_SERVICE
else -> Status.UP
}
return HealthCheck(
name = "memory",
status = status,
details = mapOf(
"usagePercent" to usagePercent,
"used" to usedMemory,
"max" to maxMemory
)
)
}
private fun checkThreadPool(): HealthCheck {
// Check thread pool metrics
return HealthCheck(
name = "threads",
status = Status.UP,
details = mapOf("active" to Thread.activeCount())
)
}
private fun checkErrorRate(): HealthCheck {
val errorRate = try {
meterRegistry.find("method.error").counters()
.sumOf { it.count() }
} catch (e: Exception) {
0.0
}
val status = if (errorRate > 100) Status.OUT_OF_SERVICE else Status.UP
return HealthCheck(
name = "errors",
status = status,
details = mapOf("totalErrors" to errorRate)
)
}
data class HealthCheck(
val name: String,
val status: Status,
val details: Map<String, Any>
)
}Centralized Error Management: { .api }
@ControllerAdvice
class McpServerErrorHandler(
private val meterRegistry: MeterRegistry
) {
@ExceptionHandler(ToolNotFoundException::class)
fun handleToolNotFound(e: ToolNotFoundException): ResponseEntity<ErrorResponse> {
meterRegistry.counter("errors", "type", "tool_not_found").increment()
return ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body(ErrorResponse(
error = "TOOL_NOT_FOUND",
message = e.message ?: "Tool not found",
timestamp = System.currentTimeMillis()
))
}
@ExceptionHandler(ToolRegistrationException::class)
fun handleRegistrationError(e: ToolRegistrationException): ResponseEntity<ErrorResponse> {
meterRegistry.counter("errors", "type", "registration_failed").increment()
logger.error("Tool registration failed", e)
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ErrorResponse(
error = "REGISTRATION_FAILED",
message = e.message ?: "Failed to register tool",
timestamp = System.currentTimeMillis()
))
}
@ExceptionHandler(Exception::class)
fun handleGenericError(e: Exception): ResponseEntity<ErrorResponse> {
meterRegistry.counter("errors", "type", "unknown").increment()
logger.error("Unexpected error", e)
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ErrorResponse(
error = "INTERNAL_ERROR",
message = "An unexpected error occurred",
timestamp = System.currentTimeMillis()
))
}
data class ErrorResponse(
val error: String,
val message: String,
val timestamp: Long
)
companion object {
private val logger = LoggerFactory.getLogger(McpServerErrorHandler::class.java)
}
}Enhance Tool Behavior: { .api }
class LoggingToolDecorator(
private val delegate: ToolCallback
) : ToolCallback by delegate {
override fun call(functionArguments: String): String {
val start = System.currentTimeMillis()
logger.info("Executing tool: ${delegate.name}")
return try {
val result = delegate.call(functionArguments)
val duration = System.currentTimeMillis() - start
logger.info("Tool ${delegate.name} completed in ${duration}ms")
result
} catch (e: Exception) {
logger.error("Tool ${delegate.name} failed", e)
throw e
}
}
companion object {
private val logger = LoggerFactory.getLogger(LoggingToolDecorator::class.java)
}
}
// Usage
@Service
class DecoratedPublisher : McpExportToolCallbackPublisher {
override val toolCallbacks = listOf(
LoggingToolDecorator(originalTool1),
LoggingToolDecorator(originalTool2)
)
override fun infoString(verbose: Boolean?, indent: Int) = "DecoratedPublisher"
private val originalTool1: ToolCallback = TODO()
private val originalTool2: ToolCallback = TODO()
}Processing Pipeline: { .api }
interface ToolProcessor {
fun process(tool: ToolCallback, args: String): ProcessResult
}
data class ProcessResult(
val success: Boolean,
val result: String?,
val continueProcessing: Boolean = true
)
class ValidationProcessor : ToolProcessor {
override fun process(tool: ToolCallback, args: String): ProcessResult {
return if (args.isNotBlank()) {
ProcessResult(true, null, true)
} else {
ProcessResult(false, """{"error":"Empty arguments"}""", false)
}
}
}
class CachingProcessor(private val cache: Cache) : ToolProcessor {
override fun process(tool: ToolCallback, args: String): ProcessResult {
val cached = cache.get("${tool.name}:$args")
return if (cached != null) {
ProcessResult(true, cached, false) // Don't continue, use cache
} else {
ProcessResult(true, null, true) // Continue processing
}
}
}
class ExecutionProcessor : ToolProcessor {
override fun process(tool: ToolCallback, args: String): ProcessResult {
return try {
val result = tool.call(args)
ProcessResult(true, result, false)
} catch (e: Exception) {
ProcessResult(false, """{"error":"${e.message}"}""", false)
}
}
}
class ProcessorChain(
private val processors: List<ToolProcessor>
) : ToolCallback {
lateinit var delegate: ToolCallback
override fun getName() = delegate.name
override fun getDescription() = delegate.description
override fun call(functionArguments: String): String {
for (processor in processors) {
val result = processor.process(delegate, functionArguments)
if (!result.continueProcessing) {
return result.result ?: """{"error":"No result"}"""
}
if (!result.success) {
return result.result ?: """{"error":"Processing failed"}"""
}
}
return """{"error":"No processor handled the request"}"""
}
}