Discover and Export available Agent(s) as MCP Servers
Understanding and choosing between synchronous and asynchronous execution modes for your MCP server.
The library supports two execution modes:
Mode selection affects server behavior, API signatures, and publisher interfaces.
Set mode in application.properties:
# Synchronous mode (default)
spring.ai.mcp.server.type=SYNC# Asynchronous mode
spring.ai.mcp.server.type=ASYNCOnly one mode is active per application instance. The configuration automatically activates the appropriate server implementation.
Choose SYNC mode when:
Sync mode uses:
McpResourcePublisher for resourcesMcpPromptPublisher for promptsMcpExportToolCallbackPublisher for tools (mode-agnostic)import com.embabel.agent.mcpserver.sync.McpResourcePublisher
import com.embabel.agent.mcpserver.sync.SyncResourceSpecificationFactory
import io.modelcontextprotocol.server.McpServerFeatures
import org.springframework.stereotype.Service
@Service
class SyncResourcesPublisher : McpResourcePublisher {
override fun resources(): List<McpServerFeatures.SyncResourceSpecification> {
return listOf(
SyncResourceSpecificationFactory.staticSyncResourceSpecification(
uri = "app://data/config",
name = "Configuration",
description = "Application configuration",
content = loadConfiguration(), // Blocking call OK
mimeType = "application/json"
)
)
}
override fun infoString(verbose: Boolean?, indent: Int): String =
"SyncResourcesPublisher: ${resources().size} resources"
private fun loadConfiguration(): String {
// Blocking I/O is acceptable in sync mode
return """{"setting": "value"}"""
}
}Mono.fromRunnable() or Mono.empty()McpSyncServer from MCP protocol libraryChoose ASYNC mode when:
Async mode uses:
McpAsyncResourcePublisher for resourcesMcpAsyncPromptPublisher for promptsMcpExportToolCallbackPublisher for tools (mode-agnostic)import com.embabel.agent.mcpserver.async.McpAsyncResourcePublisher
import io.modelcontextprotocol.server.McpServerFeatures
import io.modelcontextprotocol.spec.McpSchema
import org.springframework.stereotype.Service
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import reactor.core.publisher.Mono
@Service
@ConditionalOnProperty(
value = ["spring.ai.mcp.server.type"],
havingValue = "ASYNC"
)
class AsyncResourcesPublisher : McpAsyncResourcePublisher {
override fun resources(): List<McpServerFeatures.AsyncResourceSpecification> {
return listOf(
McpServerFeatures.AsyncResourceSpecification(
McpSchema.Resource(
"app://data/async-config",
"AsyncConfiguration",
"Asynchronously loaded configuration",
"application/json",
null
)
) { exchange, request ->
// Return Mono for non-blocking execution
loadConfigurationAsync()
.map { config ->
McpSchema.ReadResourceResult(
listOf(
McpSchema.TextResourceContents(
"app://data/async-config",
"application/json",
config
)
)
)
}
}
)
}
override fun infoString(verbose: Boolean?, indent: Int): String =
"AsyncResourcesPublisher: ${resources().size} resources"
private fun loadConfigurationAsync(): Mono<String> {
// Non-blocking I/O
return Mono.fromCallable { """{"setting": "value"}""" }
.subscribeOn(Schedulers.boundedElastic())
}
}Mono<T> or Flux<T>McpAsyncServer from MCP protocol library| Aspect | Sync Mode | Async Mode |
|---|---|---|
| Concurrency | Thread-per-request | Event loop |
| Blocking | Acceptable | Avoided |
| Resource Usage | Higher (thread overhead) | Lower (shared threads) |
| Complexity | Simpler | More complex |
| Scalability | Limited by threads | Highly scalable |
| Error Handling | Traditional exceptions | Mono error signals |
| Debugging | Easier | More challenging |
| Performance | Good for low concurrency | Excellent for high concurrency |
| Learning Curve | Gentle | Steeper (reactive) |
Simply update application.properties:
# From SYNC to ASYNC
spring.ai.mcp.server.type=ASYNCChange publisher interfaces:
// Before (Sync)
@Service
class MyPublisher : McpResourcePublisher {
override fun resources(): List<McpServerFeatures.SyncResourceSpecification> = // ...
}
// After (Async)
@Service
@ConditionalOnProperty(
value = ["spring.ai.mcp.server.type"],
havingValue = "ASYNC"
)
class MyPublisher : McpAsyncResourcePublisher {
override fun resources(): List<McpServerFeatures.AsyncResourceSpecification> = // ...
}Sync resources use factory:
SyncResourceSpecificationFactory.staticSyncResourceSpecification(
uri = "app://resource",
name = "Resource",
description = "Description",
content = "content",
mimeType = "text/plain"
)Async resources use constructor:
McpServerFeatures.AsyncResourceSpecification(
McpSchema.Resource(
"app://resource",
"Resource",
"Description",
"text/plain",
null
)
) { exchange, request ->
Mono.just(
McpSchema.ReadResourceResult(
listOf(
McpSchema.TextResourceContents(
"app://resource",
"text/plain",
"content"
)
)
)
)
}Change factory class:
// Sync
val factory = McpPromptFactory()
factory.syncPromptSpecificationForType(goal, inputType)
// Async
val factory = McpAsyncPromptFactory()
factory.asyncPromptSpecificationForType(goal, inputType)Use conditional annotations for mode-specific behavior:
// Shared base interface
interface ConfigPublisher {
fun getConfig(): String
}
// Sync implementation
@Service
@ConditionalOnProperty(
value = ["spring.ai.mcp.server.type"],
havingValue = "SYNC",
matchIfMissing = true
)
class SyncConfigPublisher : ConfigPublisher, McpResourcePublisher {
override fun getConfig(): String = loadBlocking()
override fun resources(): List<McpServerFeatures.SyncResourceSpecification> {
return listOf(
SyncResourceSpecificationFactory.staticSyncResourceSpecification(
uri = "app://config",
name = "Config",
description = "Configuration",
content = getConfig(),
mimeType = "application/json"
)
)
}
override fun infoString(verbose: Boolean?, indent: Int) =
"SyncConfigPublisher"
}
// Async implementation
@Service
@ConditionalOnProperty(
value = ["spring.ai.mcp.server.type"],
havingValue = "ASYNC"
)
class AsyncConfigPublisher : ConfigPublisher, McpAsyncResourcePublisher {
override fun getConfig(): String = runBlocking { loadAsync().await() }
override fun resources(): List<McpServerFeatures.AsyncResourceSpecification> {
return listOf(
McpServerFeatures.AsyncResourceSpecification(
McpSchema.Resource(
"app://config",
"Config",
"Configuration",
"application/json",
null
)
) { exchange, request ->
loadAsync().map { config ->
McpSchema.ReadResourceResult(
listOf(
McpSchema.TextResourceContents(
"app://config",
"application/json",
config
)
)
)
}
}
)
}
override fun infoString(verbose: Boolean?, indent: Int) =
"AsyncConfigPublisher"
private fun loadAsync(): Mono<String> =
Mono.fromCallable { """{"config": "value"}""" }
}McpExportToolCallbackPublisher works in both modesMcpServerStrategy interface for portability@ConditionalOnProperty for mode-specific componentsExample metrics (application-specific):
| Metric | Sync (100 threads) | Async (Event Loop) |
|---|---|---|
| Max Throughput | ~1000 req/s | ~5000 req/s |
| P99 Latency (low load) | 50ms | 45ms |
| P99 Latency (high load) | 500ms | 150ms |
| Memory Usage | 500MB | 200MB |
Run both modes in different environments:
# Development
spring.ai.mcp.server.type=SYNC
# Production
spring.ai.mcp.server.type=ASYNC