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

dynamic-management.mddocs/guides/

Dynamic Tool Management

Guide to runtime tool management using McpServerStrategy for adding, removing, and querying tools dynamically.

Overview

The McpServerStrategy interface provides runtime control over MCP server tools:

  • Add Tools: Register new tools at runtime
  • Remove Tools: Unregister tools by name
  • Query Registry: Inspect registered tools
  • Mode Agnostic: Works in both sync and async modes

McpServerStrategy Interface

Access server operations through dependency injection:

import com.embabel.agent.mcpserver.McpServerStrategy
import org.springframework.stereotype.Service

@Service
class ToolManager(
    private val serverStrategy: McpServerStrategy
) {

    fun addTool(toolCallback: ToolCallback) {
        serverStrategy.addToolCallback(toolCallback)
            .subscribe(
                { println("Tool added successfully") },
                { error -> println("Failed to add tool: ${error.message}") }
            )
    }

    fun removeTool(toolName: String) {
        serverStrategy.removeToolCallback(toolName)
            .subscribe(
                { println("Tool removed successfully") },
                { error -> println("Failed to remove tool: ${error.message}") }
            )
    }
}

Key Points:

  • Operations return Mono<Void> for reactive composition
  • Subscribe to execute operations
  • Handle success and error cases

Adding Tools Dynamically

Basic Tool Addition

import org.springframework.ai.tool.ToolCallback
import reactor.core.publisher.Mono

@Service
class DynamicToolService(
    private val serverStrategy: McpServerStrategy
) {

    fun registerCalculatorTools() {
        val addTool = createAddTool()
        val subtractTool = createSubtractTool()

        serverStrategy.addToolCallback(addTool)
            .then(serverStrategy.addToolCallback(subtractTool))
            .subscribe(
                { logger.info("Calculator tools registered") },
                { error -> logger.error("Registration failed", error) }
            )
    }

    private fun createAddTool(): ToolCallback {
        return object : ToolCallback {
            override fun getName(): String = "add"
            override fun getDescription(): String = "Add two numbers"
            override fun call(functionArguments: String): String {
                // Parse arguments and perform addition
                return "Result"
            }
        }
    }

    private fun createSubtractTool(): ToolCallback {
        // Similar implementation
        TODO()
    }

    companion object {
        private val logger = LoggerFactory.getLogger(DynamicToolService::class.java)
    }
}

Conditional Tool Registration

Register tools based on configuration or runtime conditions:

@Service
class ConditionalToolService(
    private val serverStrategy: McpServerStrategy,
    @Value("\${features.experimental:false}")
    private val experimentalEnabled: Boolean
) {

    @PostConstruct
    fun registerTools() {
        // Always register core tools
        registerCoreTools()

        // Conditionally register experimental tools
        if (experimentalEnabled) {
            registerExperimentalTools()
        }
    }

    private fun registerCoreTools() {
        val coreTool = createCoreTool()
        serverStrategy.addToolCallback(coreTool).subscribe()
    }

    private fun registerExperimentalTools() {
        val experimentalTool = createExperimentalTool()
        serverStrategy.addToolCallback(experimentalTool)
            .subscribe(
                { logger.info("Experimental tools enabled") },
                { error -> logger.warn("Failed to register experimental tools", error) }
            )
    }

    private fun createCoreTool(): ToolCallback = TODO()
    private fun createExperimentalTool(): ToolCallback = TODO()

    companion object {
        private val logger = LoggerFactory.getLogger(ConditionalToolService::class.java)
    }
}

Batch Tool Registration

Register multiple tools efficiently:

@Service
class BatchToolService(
    private val serverStrategy: McpServerStrategy
) {

    fun registerToolBatch(tools: List<ToolCallback>) {
        Flux.fromIterable(tools)
            .flatMap { tool ->
                serverStrategy.addToolCallback(tool)
                    .doOnSuccess { logger.info("Registered: ${tool.name}") }
                    .onErrorResume { error ->
                        logger.error("Failed to register ${tool.name}", error)
                        Mono.empty()  // Continue with other tools
                    }
            }
            .then()
            .subscribe(
                { logger.info("Batch registration completed") },
                { error -> logger.error("Batch registration failed", error) }
            )
    }

    companion object {
        private val logger = LoggerFactory.getLogger(BatchToolService::class.java)
    }
}

Removing Tools Dynamically

Basic Tool Removal

@Service
class ToolRemovalService(
    private val serverStrategy: McpServerStrategy
) {

    fun unregisterTool(toolName: String) {
        serverStrategy.removeToolCallback(toolName)
            .subscribe(
                { logger.info("Tool removed: $toolName") },
                { error -> logger.error("Failed to remove tool", error) }
            )
    }

    fun unregisterMultipleTools(toolNames: List<String>) {
        Flux.fromIterable(toolNames)
            .flatMap { name ->
                serverStrategy.removeToolCallback(name)
                    .doOnSuccess { logger.info("Removed: $name") }
            }
            .then()
            .subscribe()
    }

    companion object {
        private val logger = LoggerFactory.getLogger(ToolRemovalService::class.java)
    }
}

Safe Removal with Verification

Check if tool exists before removing:

@Service
class SafeToolRemovalService(
    private val serverStrategy: McpServerStrategy,
    private val toolRegistry: ToolRegistry
) {

    fun safeRemoveTool(toolName: String): Mono<Boolean> {
        return toolRegistry.findToolCallback(toolName)
            .flatMap { existingTool ->
                serverStrategy.removeToolCallback(toolName)
                    .thenReturn(true)
            }
            .defaultIfEmpty(false)
            .doOnSuccess { removed ->
                if (removed) {
                    logger.info("Successfully removed: $toolName")
                } else {
                    logger.warn("Tool not found: $toolName")
                }
            }
    }

    companion object {
        private val logger = LoggerFactory.getLogger(SafeToolRemovalService::class.java)
    }
}

Querying Tools

Using ToolRegistry

Query registered tools through the registry:

import com.embabel.agent.mcpserver.ToolRegistry

@Service
class ToolQueryService(
    private val toolRegistry: ToolRegistry
) {

    fun getAllToolNames(): Mono<List<String>> {
        return toolRegistry.listToolCallbacks()
            .map { callbacks -> callbacks.map { it.name } }
    }

    fun findTool(toolName: String): Mono<ToolCallback> {
        return toolRegistry.findToolCallback(toolName)
    }

    fun toolExists(toolName: String): Mono<Boolean> {
        return toolRegistry.findToolCallback(toolName)
            .map { true }
            .defaultIfEmpty(false)
    }

    fun getToolCount(): Mono<Int> {
        return toolRegistry.listToolCallbacks()
            .map { it.size }
    }
}

Tool Inspection

Inspect tool properties:

@Service
class ToolInspectionService(
    private val toolRegistry: ToolRegistry
) {

    fun inspectTool(toolName: String): Mono<ToolInfo> {
        return toolRegistry.findToolCallback(toolName)
            .map { callback ->
                ToolInfo(
                    name = callback.name,
                    description = callback.description,
                    schemaType = callback.inputTypeSchema?.typeName ?: "unknown"
                )
            }
    }

    fun listAllTools(): Mono<List<ToolInfo>> {
        return toolRegistry.listToolCallbacks()
            .map { callbacks ->
                callbacks.map { callback ->
                    ToolInfo(
                        name = callback.name,
                        description = callback.description,
                        schemaType = callback.inputTypeSchema?.typeName ?: "unknown"
                    )
                }
            }
    }
}

data class ToolInfo(
    val name: String,
    val description: String,
    val schemaType: String
)

Lifecycle Management

Tool Registration on Startup

Register tools during application initialization:

@Service
class StartupToolRegistration(
    private val serverStrategy: McpServerStrategy
) {

    @PostConstruct
    fun registerStartupTools() {
        val startupTools = listOf(
            createHealthCheckTool(),
            createVersionTool(),
            createHelpTool()
        )

        Flux.fromIterable(startupTools)
            .flatMap { serverStrategy.addToolCallback(it) }
            .then()
            .subscribe(
                { logger.info("Startup tools registered") },
                { error -> logger.error("Startup registration failed", error) }
            )
    }

    private fun createHealthCheckTool(): ToolCallback = TODO()
    private fun createVersionTool(): ToolCallback = TODO()
    private fun createHelpTool(): ToolCallback = TODO()

    companion object {
        private val logger = LoggerFactory.getLogger(StartupToolRegistration::class.java)
    }
}

Tool Registration on Demand

Register tools when needed:

@RestController
@RequestMapping("/admin/tools")
class ToolManagementController(
    private val serverStrategy: McpServerStrategy
) {

    @PostMapping("/enable/{feature}")
    fun enableFeature(@PathVariable feature: String): Mono<ResponseEntity<String>> {
        val tool = createFeatureTool(feature)

        return serverStrategy.addToolCallback(tool)
            .thenReturn(ResponseEntity.ok("Feature enabled: $feature"))
            .onErrorReturn(ResponseEntity.status(500).body("Failed to enable feature"))
    }

    @DeleteMapping("/disable/{toolName}")
    fun disableTool(@PathVariable toolName: String): Mono<ResponseEntity<String>> {
        return serverStrategy.removeToolCallback(toolName)
            .thenReturn(ResponseEntity.ok("Tool disabled: $toolName"))
            .onErrorReturn(ResponseEntity.status(500).body("Failed to disable tool"))
    }

    private fun createFeatureTool(feature: String): ToolCallback = TODO()
}

Cleanup on Shutdown

Remove tools during shutdown:

@Service
class ShutdownToolCleanup(
    private val serverStrategy: McpServerStrategy,
    private val toolRegistry: ToolRegistry
) {

    @PreDestroy
    fun cleanupTools() {
        toolRegistry.listToolCallbacks()
            .flatMapMany { Flux.fromIterable(it) }
            .filter { callback -> callback.name.startsWith("temp_") }
            .flatMap { callback ->
                serverStrategy.removeToolCallback(callback.name)
                    .doOnSuccess { logger.info("Cleaned up: ${callback.name}") }
            }
            .then()
            .block()  // Block on shutdown
    }

    companion object {
        private val logger = LoggerFactory.getLogger(ShutdownToolCleanup::class.java)
    }
}

Advanced Patterns

Hot Reload

Reload tool definitions without restart:

@Service
class HotReloadService(
    private val serverStrategy: McpServerStrategy,
    private val toolRegistry: ToolRegistry
) {

    fun reloadTool(toolName: String, newImplementation: ToolCallback): Mono<Void> {
        return toolRegistry.findToolCallback(toolName)
            .flatMap { existingTool ->
                // Remove old implementation
                serverStrategy.removeToolCallback(toolName)
            }
            .then(
                // Add new implementation
                serverStrategy.addToolCallback(newImplementation)
            )
            .doOnSuccess { logger.info("Reloaded: $toolName") }
            .doOnError { error -> logger.error("Reload failed for $toolName", error) }
    }
}

Feature Flags

Dynamic tool registration based on feature flags:

@Service
class FeatureFlagService(
    private val serverStrategy: McpServerStrategy,
    private val toolRegistry: ToolRegistry
) {

    fun toggleFeature(featureName: String, enabled: Boolean): Mono<Void> {
        return if (enabled) {
            enableFeature(featureName)
        } else {
            disableFeature(featureName)
        }
    }

    private fun enableFeature(featureName: String): Mono<Void> {
        val tool = createFeatureTool(featureName)
        return serverStrategy.addToolCallback(tool)
            .doOnSuccess { logger.info("Feature enabled: $featureName") }
    }

    private fun disableFeature(featureName: String): Mono<Void> {
        return toolRegistry.listToolCallbacks()
            .flatMapMany { Flux.fromIterable(it) }
            .filter { callback -> callback.name.startsWith("feature_$featureName") }
            .flatMap { callback ->
                serverStrategy.removeToolCallback(callback.name)
            }
            .then()
            .doOnSuccess { logger.info("Feature disabled: $featureName") }
    }

    private fun createFeatureTool(featureName: String): ToolCallback = TODO()

    companion object {
        private val logger = LoggerFactory.getLogger(FeatureFlagService::class.java)
    }
}

Tool Versioning

Manage multiple versions of tools:

@Service
class ToolVersioningService(
    private val serverStrategy: McpServerStrategy
) {

    fun registerVersionedTool(
        baseName: String,
        version: String,
        implementation: ToolCallback
    ): Mono<Void> {
        val versionedName = "${baseName}_$version"
        val versionedTool = object : ToolCallback by implementation {
            override fun getName(): String = versionedName
        }

        return serverStrategy.addToolCallback(versionedTool)
            .doOnSuccess { logger.info("Registered $baseName version $version") }
    }

    fun deprecateVersion(baseName: String, version: String): Mono<Void> {
        val toolName = "${baseName}_$version"
        return serverStrategy.removeToolCallback(toolName)
            .doOnSuccess { logger.info("Deprecated $baseName version $version") }
    }

    companion object {
        private val logger = LoggerFactory.getLogger(ToolVersioningService::class.java)
    }
}

Error Handling

Graceful Error Recovery

@Service
class ResilientToolService(
    private val serverStrategy: McpServerStrategy
) {

    fun registerWithRetry(
        toolCallback: ToolCallback,
        maxRetries: Int = 3
    ): Mono<Void> {
        return serverStrategy.addToolCallback(toolCallback)
            .retry(maxRetries.toLong())
            .doOnError { error ->
                logger.error("Failed to register ${toolCallback.name} after $maxRetries attempts", error)
            }
    }

    fun registerWithFallback(
        primaryTool: ToolCallback,
        fallbackTool: ToolCallback
    ): Mono<Void> {
        return serverStrategy.addToolCallback(primaryTool)
            .onErrorResume { error ->
                logger.warn("Primary tool registration failed, using fallback", error)
                serverStrategy.addToolCallback(fallbackTool)
            }
    }

    companion object {
        private val logger = LoggerFactory.getLogger(ResilientToolService::class.java)
    }
}

Validation Before Registration

@Service
class ValidatingToolService(
    private val serverStrategy: McpServerStrategy,
    private val toolRegistry: ToolRegistry
) {

    fun registerWithValidation(toolCallback: ToolCallback): Mono<Void> {
        return validateTool(toolCallback)
            .flatMap { isValid ->
                if (isValid) {
                    serverStrategy.addToolCallback(toolCallback)
                } else {
                    Mono.error(IllegalArgumentException("Tool validation failed"))
                }
            }
    }

    private fun validateTool(toolCallback: ToolCallback): Mono<Boolean> {
        return Mono.fromCallable {
            // Check name is not empty
            if (toolCallback.name.isBlank()) return@fromCallable false

            // Check description is provided
            if (toolCallback.description.isBlank()) return@fromCallable false

            // Check name doesn't conflict
            true
        }
    }
}

Best Practices

1. Use Reactive Operators

// Good: Chain operations
serverStrategy.addToolCallback(tool1)
    .then(serverStrategy.addToolCallback(tool2))
    .then(serverStrategy.addToolCallback(tool3))
    .subscribe()

// Less efficient: Sequential subscribes
serverStrategy.addToolCallback(tool1).subscribe()
serverStrategy.addToolCallback(tool2).subscribe()
serverStrategy.addToolCallback(tool3).subscribe()

2. Handle Errors Appropriately

// Good: Handle errors
serverStrategy.addToolCallback(tool)
    .subscribe(
        { logger.info("Success") },
        { error -> logger.error("Failed", error) }
    )

// Bad: Ignore errors
serverStrategy.addToolCallback(tool).subscribe()

3. Use Meaningful Tool Names

// Good: Clear naming
val toolName = "payment_process_refund"

// Bad: Generic naming
val toolName = "tool1"

4. Log Operations

serverStrategy.addToolCallback(tool)
    .doOnSubscribe { logger.info("Registering tool: ${tool.name}") }
    .doOnSuccess { logger.info("Successfully registered: ${tool.name}") }
    .doOnError { error -> logger.error("Failed to register: ${tool.name}", error) }
    .subscribe()

Testing

Unit Testing Tool Management

import reactor.test.StepVerifier

class ToolManagementServiceTest {

    private lateinit var serverStrategy: McpServerStrategy
    private lateinit var service: DynamicToolService

    @BeforeEach
    fun setup() {
        serverStrategy = mock()
        service = DynamicToolService(serverStrategy)
    }

    @Test
    fun `should add tool successfully`() {
        val tool = mock<ToolCallback>()
        whenever(serverStrategy.addToolCallback(any())).thenReturn(Mono.empty())

        StepVerifier.create(serverStrategy.addToolCallback(tool))
            .verifyComplete()
    }

    @Test
    fun `should handle registration failure`() {
        val tool = mock<ToolCallback>()
        val error = RuntimeException("Registration failed")
        whenever(serverStrategy.addToolCallback(any())).thenReturn(Mono.error(error))

        StepVerifier.create(serverStrategy.addToolCallback(tool))
            .expectError(RuntimeException::class.java)
            .verify()
    }
}

Related Documentation

  • Server Strategy API - Complete API reference
  • Tool Registry API - Registry operations
  • Creating Publishers - Publisher patterns
  • Integration Patterns - Complete examples
tessl i tessl/maven-com-embabel-agent--embabel-agent-mcpserver@0.3.1

docs

index.md

tile.json