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

integration-patterns.mddocs/guides/

Integration Patterns

Complete application examples demonstrating common integration patterns, best practices, and real-world scenarios.

Overview

This guide provides end-to-end examples of integrating embabel-agent-mcpserver into various application types:

  • Simple tool exporter
  • Multi-domain publisher application
  • Dynamic tool management system
  • Resource and prompt integration
  • Embabel framework integration

Pattern 1: Simple Tool Exporter

Basic application exposing custom tools via MCP.

Project Structure

src/main/kotlin/com/example/tools/
├── ToolsApplication.kt
├── config/
│   └── ToolsConfig.kt
├── publishers/
│   └── CalculatorPublisher.kt
└── tools/
    ├── AddTool.kt
    ├── SubtractTool.kt
    └── MultiplyTool.kt

Application Setup

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication

@SpringBootApplication
class ToolsApplication

fun main(args: Array<String>) {
    runApplication<ToolsApplication>(*args)
}

Configuration

# application.properties
spring.ai.mcp.server.type=SYNC
spring.application.name=calculator-tools

logging.level.com.embabel.agent.mcpserver=INFO

Tool Implementation

import org.springframework.ai.tool.ToolCallback
import com.fasterxml.jackson.databind.ObjectMapper

class AddTool : ToolCallback {

    private val objectMapper = ObjectMapper()

    override fun getName(): String = "add"

    override fun getDescription(): String = "Add two numbers together"

    override fun call(functionArguments: String): String {
        val args = objectMapper.readValue(functionArguments, AddArguments::class.java)
        val result = args.a + args.b
        return objectMapper.writeValueAsString(mapOf("result" to result))
    }

    data class AddArguments(val a: Double, val b: Double)
}

class SubtractTool : ToolCallback {

    private val objectMapper = ObjectMapper()

    override fun getName(): String = "subtract"

    override fun getDescription(): String = "Subtract second number from first"

    override fun call(functionArguments: String): String {
        val args = objectMapper.readValue(functionArguments, SubtractArguments::class.java)
        val result = args.a - args.b
        return objectMapper.writeValueAsString(mapOf("result" to result))
    }

    data class SubtractArguments(val a: Double, val b: Double)
}

Publisher

import com.embabel.agent.mcpserver.McpExportToolCallbackPublisher
import org.springframework.ai.tool.ToolCallback
import org.springframework.stereotype.Service

@Service
class CalculatorPublisher : McpExportToolCallbackPublisher {

    override val toolCallbacks: List<ToolCallback> = listOf(
        AddTool(),
        SubtractTool(),
        MultiplyTool()
    )

    override fun infoString(verbose: Boolean?, indent: Int): String {
        return "CalculatorPublisher: ${toolCallbacks.size} tools"
    }
}

Key Takeaways:

  • Simple synchronous mode for basic tools
  • Direct ToolCallback implementation
  • Minimal configuration

Pattern 2: Multi-Domain Application

Application with multiple publishers organized by domain.

Project Structure

src/main/kotlin/com/example/api/
├── ApiApplication.kt
├── config/
│   ├── McpConfig.kt
│   └── SecurityConfig.kt
├── publishers/
│   ├── user/
│   │   ├── UserToolsPublisher.kt
│   │   ├── UserResourcesPublisher.kt
│   │   └── UserPromptsPublisher.kt
│   ├── payment/
│   │   ├── PaymentToolsPublisher.kt
│   │   └── PaymentResourcesPublisher.kt
│   └── analytics/
│       ├── AnalyticsToolsPublisher.kt
│       └── AnalyticsResourcesPublisher.kt
└── services/
    ├── UserService.kt
    ├── PaymentService.kt
    └── AnalyticsService.kt

Configuration

# application.properties
spring.ai.mcp.server.type=ASYNC
spring.application.name=api-gateway

# Domain features
features.users.enabled=true
features.payments.enabled=true
features.analytics.enabled=false

# Limits
mcp.server.max-tools=500
mcp.server.max-resources=200

Domain Configuration

import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.context.annotation.Configuration

@Configuration
@ConfigurationProperties(prefix = "features")
data class FeaturesConfig(
    var users: DomainConfig = DomainConfig(),
    var payments: DomainConfig = DomainConfig(),
    var analytics: DomainConfig = DomainConfig()
)

data class DomainConfig(
    var enabled: Boolean = false
)

User Domain Publisher

import com.embabel.agent.mcpserver.McpExportToolCallbackPublisher
import com.embabel.agent.mcpserver.McpToolExport
import com.embabel.agent.api.common.ToolObject
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.stereotype.Service

@Service
@ConditionalOnProperty(
    name = ["features.users.enabled"],
    havingValue = "true"
)
class UserToolsPublisher(
    private val userService: UserService
) : McpExportToolCallbackPublisher {

    override val toolCallbacks: List<ToolCallback>
        get() {
            val tools = ToolObject(
                objects = listOf(
                    userService.getCreateUserTool(),
                    userService.getUpdateUserTool(),
                    userService.getDeleteUserTool()
                ),
                namingStrategy = { "user_$it" }
            )
            return McpToolExport.fromToolObject(tools).toolCallbacks
        }

    override fun infoString(verbose: Boolean?, indent: Int) =
        "UserToolsPublisher: ${toolCallbacks.size} tools"
}

@Service
@ConditionalOnProperty(
    name = ["features.users.enabled"],
    havingValue = "true"
)
@ConditionalOnProperty(
    value = ["spring.ai.mcp.server.type"],
    havingValue = "ASYNC"
)
class UserResourcesPublisher(
    private val userService: UserService
) : McpAsyncResourcePublisher {

    override fun resources(): List<McpServerFeatures.AsyncResourceSpecification> {
        return listOf(
            createUserListResource(),
            createUserSchemaResource()
        )
    }

    override fun infoString(verbose: Boolean?, indent: Int) =
        "UserResourcesPublisher: ${resources().size} resources"

    private fun createUserListResource(): McpServerFeatures.AsyncResourceSpecification {
        return McpServerFeatures.AsyncResourceSpecification(
            McpSchema.Resource(
                "app://users/list",
                "UserList",
                "List of all users",
                "application/json",
                null
            )
        ) { exchange, request ->
            userService.listUsersAsync()
                .map { users ->
                    McpSchema.ReadResourceResult(
                        listOf(
                            McpSchema.TextResourceContents(
                                "app://users/list",
                                "application/json",
                                objectMapper.writeValueAsString(users)
                            )
                        )
                    )
                }
        }
    }

    private fun createUserSchemaResource(): McpServerFeatures.AsyncResourceSpecification {
        return McpServerFeatures.AsyncResourceSpecification(
            McpSchema.Resource(
                "app://users/schema",
                "UserSchema",
                "User data schema",
                "application/json",
                null
            )
        ) { exchange, request ->
            Mono.just(
                McpSchema.ReadResourceResult(
                    listOf(
                        McpSchema.TextResourceContents(
                            "app://users/schema",
                            "application/json",
                            userService.getUserSchema()
                        )
                    )
                )
            )
        }
    }

    companion object {
        private val objectMapper = ObjectMapper()
    }
}

Payment Domain Publisher

@Service
@ConditionalOnProperty(
    name = ["features.payments.enabled"],
    havingValue = "true"
)
class PaymentToolsPublisher(
    private val paymentService: PaymentService
) : McpExportToolCallbackPublisher {

    override val toolCallbacks: List<ToolCallback>
        get() {
            val tools = ToolObject(
                objects = listOf(
                    paymentService.getProcessPaymentTool(),
                    paymentService.getRefundTool(),
                    paymentService.getCheckStatusTool()
                ),
                namingStrategy = { "payment_$it" }
            )
            return McpToolExport.fromToolObject(tools).toolCallbacks
        }

    override fun infoString(verbose: Boolean?, indent: Int) =
        "PaymentToolsPublisher: ${toolCallbacks.size} tools"
}

Key Takeaways:

  • Feature flags control domain activation
  • Domain-specific namespacing
  • Async mode for scalability
  • Organized by business domain

Pattern 3: Dynamic Tool Management

Application with runtime tool management capabilities.

Architecture

Application
├── Core Publishers (static)
│   ├── Built-in tools
│   └── Standard resources
└── Dynamic Management
    ├── Tool registry
    ├── Admin API
    └── Feature toggles

Dynamic Tool Manager

import com.embabel.agent.mcpserver.McpServerStrategy
import com.embabel.agent.mcpserver.ToolRegistry
import org.springframework.stereotype.Service
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono

@Service
class DynamicToolManager(
    private val serverStrategy: McpServerStrategy,
    private val toolRegistry: ToolRegistry,
    private val toolFactory: ToolFactory
) {

    fun enableFeature(featureName: String): Mono<FeatureStatus> {
        return Mono.fromCallable {
            val tools = toolFactory.createToolsForFeature(featureName)
            tools
        }
        .flatMapMany { Flux.fromIterable(it) }
        .flatMap { tool ->
            serverStrategy.addToolCallback(tool)
                .thenReturn(tool.name)
        }
        .collectList()
        .map { registeredTools ->
            FeatureStatus(
                feature = featureName,
                enabled = true,
                tools = registeredTools
            )
        }
        .doOnSuccess { status ->
            logger.info("Feature enabled: ${status.feature} with ${status.tools.size} tools")
        }
    }

    fun disableFeature(featureName: String): Mono<FeatureStatus> {
        return toolRegistry.listToolCallbacks()
            .flatMapMany { Flux.fromIterable(it) }
            .filter { callback -> callback.name.startsWith("${featureName}_") }
            .flatMap { callback ->
                serverStrategy.removeToolCallback(callback.name)
                    .thenReturn(callback.name)
            }
            .collectList()
            .map { removedTools ->
                FeatureStatus(
                    feature = featureName,
                    enabled = false,
                    tools = removedTools
                )
            }
            .doOnSuccess { status ->
                logger.info("Feature disabled: ${status.feature}, removed ${status.tools.size} tools")
            }
    }

    fun getFeatureStatus(featureName: String): Mono<FeatureStatus> {
        return toolRegistry.listToolCallbacks()
            .map { callbacks ->
                val featureTools = callbacks
                    .filter { it.name.startsWith("${featureName}_") }
                    .map { it.name }

                FeatureStatus(
                    feature = featureName,
                    enabled = featureTools.isNotEmpty(),
                    tools = featureTools
                )
            }
    }

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

data class FeatureStatus(
    val feature: String,
    val enabled: Boolean,
    val tools: List<String>
)

Admin REST API

import org.springframework.web.bind.annotation.*
import reactor.core.publisher.Mono

@RestController
@RequestMapping("/admin/features")
class FeatureManagementController(
    private val toolManager: DynamicToolManager
) {

    @PostMapping("/{feature}/enable")
    fun enableFeature(@PathVariable feature: String): Mono<FeatureStatus> {
        return toolManager.enableFeature(feature)
    }

    @PostMapping("/{feature}/disable")
    fun disableFeature(@PathVariable feature: String): Mono<FeatureStatus> {
        return toolManager.disableFeature(feature)
    }

    @GetMapping("/{feature}")
    fun getFeatureStatus(@PathVariable feature: String): Mono<FeatureStatus> {
        return toolManager.getFeatureStatus(feature)
    }
}

Tool Factory

import org.springframework.ai.tool.ToolCallback
import org.springframework.stereotype.Component

@Component
class ToolFactory {

    fun createToolsForFeature(featureName: String): List<ToolCallback> {
        return when (featureName) {
            "analytics" -> createAnalyticsTools()
            "reporting" -> createReportingTools()
            "export" -> createExportTools()
            else -> emptyList()
        }
    }

    private fun createAnalyticsTools(): List<ToolCallback> {
        return listOf(
            createTool("analytics_track_event", "Track an analytics event"),
            createTool("analytics_generate_report", "Generate analytics report")
        )
    }

    private fun createReportingTools(): List<ToolCallback> {
        return listOf(
            createTool("reporting_create_report", "Create a new report"),
            createTool("reporting_schedule_report", "Schedule report generation")
        )
    }

    private fun createExportTools(): List<ToolCallback> {
        return listOf(
            createTool("export_csv", "Export data as CSV"),
            createTool("export_pdf", "Export data as PDF")
        )
    }

    private fun createTool(name: String, description: String): ToolCallback {
        return object : ToolCallback {
            override fun getName(): String = name
            override fun getDescription(): String = description
            override fun call(functionArguments: String): String {
                return """{"status": "success"}"""
            }
        }
    }
}

Key Takeaways:

  • Runtime tool management via API
  • Feature toggle pattern
  • Reactive operations throughout
  • Tool factory for dynamic creation

Pattern 4: Resource and Prompt Integration

Application demonstrating comprehensive resource and prompt usage.

Documentation Publisher

import com.embabel.agent.mcpserver.sync.McpResourcePublisher
import com.embabel.agent.mcpserver.sync.SyncResourceSpecificationFactory
import org.springframework.stereotype.Service
import org.springframework.core.io.ResourceLoader

@Service
class DocumentationPublisher(
    private val resourceLoader: ResourceLoader
) : McpResourcePublisher {

    override fun resources(): List<McpServerFeatures.SyncResourceSpecification> {
        return listOf(
            createApiDocumentation(),
            createUserGuide(),
            createExamples(),
            createChangelog()
        )
    }

    override fun infoString(verbose: Boolean?, indent: Int) =
        "DocumentationPublisher: ${resources().size} resources"

    private fun createApiDocumentation() =
        SyncResourceSpecificationFactory.staticSyncResourceSpecification(
            uri = "app://docs/api",
            name = "APIDocumentation",
            description = "Complete API documentation",
            content = loadResource("classpath:docs/api.md"),
            mimeType = "text/markdown"
        )

    private fun createUserGuide() =
        SyncResourceSpecificationFactory.staticSyncResourceSpecification(
            uri = "app://docs/user-guide",
            name = "UserGuide",
            description = "User guide and tutorials",
            content = loadResource("classpath:docs/user-guide.md"),
            mimeType = "text/markdown"
        )

    private fun createExamples() =
        SyncResourceSpecificationFactory.syncResourceSpecification(
            uri = "app://docs/examples",
            name = "Examples",
            description = "Code examples",
            resourceLoader = { loadExamples() },
            mimeType = "application/json"
        )

    private fun createChangelog() =
        SyncResourceSpecificationFactory.staticSyncResourceSpecification(
            uri = "app://docs/changelog",
            name = "Changelog",
            description = "Version history and changes",
            content = loadResource("classpath:docs/CHANGELOG.md"),
            mimeType = "text/markdown"
        )

    private fun loadResource(location: String): String {
        return resourceLoader.getResource(location)
            .inputStream
            .bufferedReader()
            .use { it.readText() }
    }

    private fun loadExamples(): String {
        // Dynamic example generation
        return """
        {
            "examples": [
                {"name": "Basic usage", "code": "..."},
                {"name": "Advanced usage", "code": "..."}
            ]
        }
        """.trimIndent()
    }
}

Comprehensive Prompt Publisher

import com.embabel.agent.mcpserver.sync.McpPromptPublisher
import com.embabel.agent.mcpserver.sync.McpPromptFactory
import com.embabel.common.core.types.Named
import com.embabel.common.core.types.Described
import org.springframework.stereotype.Service

@Service
class ComprehensivePromptsPublisher : McpPromptPublisher {

    private val factory = McpPromptFactory()

    override fun prompts(): List<McpServerFeatures.SyncPromptSpecification> {
        return listOf(
            createUserPrompt(),
            createTaskPrompt(),
            createQueryPrompt(),
            createBatchPrompt()
        )
    }

    override fun infoString(verbose: Boolean?, indent: Int) =
        "ComprehensivePromptsPublisher: ${prompts().size} prompts"

    private fun createUserPrompt() = factory.syncPromptSpecificationForType(
        goal = createGoal("createUser", "Create a new user account"),
        inputType = CreateUserInput::class.java
    )

    private fun createTaskPrompt() = factory.syncPromptSpecificationForType(
        goal = createGoal("createTask", "Create a new task"),
        inputType = CreateTaskInput::class.java
    )

    private fun createQueryPrompt() = factory.syncPromptSpecificationForType(
        goal = createGoal("searchData", "Search for data"),
        inputType = SearchInput::class.java
    )

    private fun createBatchPrompt() = factory.syncPromptSpecificationForType(
        goal = createGoal("batchProcess", "Process multiple items"),
        inputType = BatchInput::class.java
    )

    private fun createGoal(name: String, description: String) = object : Named, Described {
        override val name = name
        override val description = description
    }
}

data class CreateUserInput(
    @JsonPropertyDescription("User email address")
    val email: String,

    @JsonPropertyDescription("User full name")
    val name: String,

    @JsonPropertyDescription("User role (admin, user, viewer)")
    val role: String = "user"
)

data class CreateTaskInput(
    @JsonPropertyDescription("Task title")
    val title: String,

    @JsonPropertyDescription("Task description")
    val description: String,

    @JsonPropertyDescription("Priority (1-5)")
    val priority: Int = 3,

    @JsonPropertyDescription("Assigned user ID")
    val assigneeId: String? = null
)

data class SearchInput(
    @JsonPropertyDescription("Search query")
    val query: String,

    @JsonPropertyDescription("Search fields")
    val fields: List<String> = emptyList(),

    @JsonPropertyDescription("Maximum results")
    val limit: Int = 10
)

data class BatchInput(
    @JsonPropertyDescription("Operation to perform")
    val operation: String,

    @JsonPropertyDescription("List of item IDs")
    val items: List<String>,

    @JsonPropertyDescription("Operation parameters")
    val parameters: Map<String, Any> = emptyMap()
)

Key Takeaways:

  • Static resources from classpath
  • Dynamic resources generated on-demand
  • Rich prompt specifications
  • Comprehensive input types

Pattern 5: Embabel Framework Integration

Full integration with Embabel Agent Framework.

Project Setup

@SpringBootApplication
class EmbabelIntegrationApp

fun main(args: Array<String>) {
    runApplication<EmbabelIntegrationApp>(*args)
}

Configuration

spring.ai.mcp.server.type=ASYNC
spring.application.name=embabel-agent

# Embabel configuration
embabel.agent.auto-discovery=true
embabel.agent.package-scan=com.example.agents

Agent Definition

import com.embabel.agent.api.common.Agent
import com.embabel.agent.api.common.Goal

@Agent
class DataProcessingAgent {

    @Goal(name = "processData", description = "Process data records")
    fun processData(input: ProcessDataInput): ProcessDataOutput {
        // Agent implementation
        return ProcessDataOutput(
            processed = input.records.size,
            results = input.records.map { "Processed: $it" }
        )
    }

    @Goal(name = "validateData", description = "Validate data records")
    fun validateData(input: ValidateDataInput): ValidateDataOutput {
        // Agent implementation
        val valid = input.records.filter { it.length > 5 }
        return ValidateDataOutput(
            totalRecords = input.records.size,
            validRecords = valid.size,
            invalidRecords = input.records.size - valid.size
        )
    }
}

data class ProcessDataInput(val records: List<String>)
data class ProcessDataOutput(val processed: Int, val results: List<String>)
data class ValidateDataInput(val records: List<String>)
data class ValidateDataOutput(
    val totalRecords: Int,
    val validRecords: Int,
    val invalidRecords: Int
)

Embabel Publisher

import com.embabel.agent.api.common.LlmReference
import com.embabel.agent.mcpserver.McpToolExport
import org.springframework.stereotype.Service

@Service
class EmbabelAgentPublisher(
    private val llmReferences: List<LlmReference>
) : McpExportToolCallbackPublisher {

    override val toolCallbacks: List<ToolCallback>
        get() = llmReferences.flatMap { reference ->
            McpToolExport.fromLlmReference(
                llmReference = reference,
                namingStrategy = { "${reference.name.lowercase()}_$it" }
            ).toolCallbacks
        }

    override fun infoString(verbose: Boolean?, indent: Int): String {
        return "EmbabelAgentPublisher: ${toolCallbacks.size} tools from ${llmReferences.size} agents"
    }
}

Key Takeaways:

  • Automatic agent discovery
  • Goal methods exposed as tools
  • LlmReference integration
  • Type-safe agent definitions

Best Practices Summary

1. Organization

✓ Organize publishers by domain
✓ Separate configuration classes
✓ Use meaningful package structure
✓ Group related functionality

2. Configuration

✓ Use type-safe configuration
✓ Provide sensible defaults
✓ Use profiles for environments
✓ Enable feature toggles

3. Error Handling

✓ Handle errors gracefully
✓ Log important operations
✓ Provide fallbacks
✓ Use reactive error operators

4. Testing

✓ Unit test publishers
✓ Integration test flows
✓ Test configuration variants
✓ Mock external dependencies

5. Performance

✓ Use async mode for high load
✓ Batch operations when possible
✓ Cache expensive computations
✓ Monitor resource usage

Related Documentation

  • Getting Started - Basic setup
  • Creating Publishers - Publisher patterns
  • Resources and Prompts - Resource/prompt creation
  • Dynamic Management - Runtime tool management
  • Configuration - Configuration options
tessl i tessl/maven-com-embabel-agent--embabel-agent-mcpserver@0.3.1

docs

index.md

tile.json