Discover and Export available Agent(s) as MCP Servers
Complete application examples demonstrating common integration patterns, best practices, and real-world scenarios.
This guide provides end-to-end examples of integrating embabel-agent-mcpserver into various application types:
Basic application exposing custom tools via MCP.
src/main/kotlin/com/example/tools/
├── ToolsApplication.kt
├── config/
│ └── ToolsConfig.kt
├── publishers/
│ └── CalculatorPublisher.kt
└── tools/
├── AddTool.kt
├── SubtractTool.kt
└── MultiplyTool.ktimport org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
@SpringBootApplication
class ToolsApplication
fun main(args: Array<String>) {
runApplication<ToolsApplication>(*args)
}# application.properties
spring.ai.mcp.server.type=SYNC
spring.application.name=calculator-tools
logging.level.com.embabel.agent.mcpserver=INFOimport 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)
}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:
Application with multiple publishers organized by domain.
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# 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=200import 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
)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()
}
}@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:
Application with runtime tool management capabilities.
Application
├── Core Publishers (static)
│ ├── Built-in tools
│ └── Standard resources
└── Dynamic Management
├── Tool registry
├── Admin API
└── Feature togglesimport 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>
)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)
}
}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:
Application demonstrating comprehensive resource and prompt usage.
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()
}
}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:
Full integration with Embabel Agent Framework.
@SpringBootApplication
class EmbabelIntegrationApp
fun main(args: Array<String>) {
runApplication<EmbabelIntegrationApp>(*args)
}spring.ai.mcp.server.type=ASYNC
spring.application.name=embabel-agent
# Embabel configuration
embabel.agent.auto-discovery=true
embabel.agent.package-scan=com.example.agentsimport 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
)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:
✓ Organize publishers by domain
✓ Separate configuration classes
✓ Use meaningful package structure
✓ Group related functionality✓ Use type-safe configuration
✓ Provide sensible defaults
✓ Use profiles for environments
✓ Enable feature toggles✓ Handle errors gracefully
✓ Log important operations
✓ Provide fallbacks
✓ Use reactive error operators✓ Unit test publishers
✓ Integration test flows
✓ Test configuration variants
✓ Mock external dependencies✓ Use async mode for high load
✓ Batch operations when possible
✓ Cache expensive computations
✓ Monitor resource usage