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

execution-modes.mddocs/guides/

Execution Modes: Sync vs Async

Understanding and choosing between synchronous and asynchronous execution modes for your MCP server.

Overview

The library supports two execution modes:

  • SYNC (Synchronous): Blocking operations, simpler programming model
  • ASYNC (Asynchronous): Non-blocking operations, better scalability

Mode selection affects server behavior, API signatures, and publisher interfaces.

Configuration

Set mode in application.properties:

# Synchronous mode (default)
spring.ai.mcp.server.type=SYNC
# Asynchronous mode
spring.ai.mcp.server.type=ASYNC

Only one mode is active per application instance. The configuration automatically activates the appropriate server implementation.

Synchronous Mode

When to Use

Choose SYNC mode when:

  • Application has low concurrency requirements
  • Blocking I/O is acceptable
  • Simpler code is preferred
  • Integrating with blocking libraries
  • Development/testing environments

Characteristics

  • Blocking Operations: Server operations block the calling thread
  • Thread-Per-Request: Traditional servlet model
  • Simple Error Handling: Exceptions propagate naturally
  • Easier Debugging: Linear execution flow

Publisher Interfaces

Sync mode uses:

  • McpResourcePublisher for resources
  • McpPromptPublisher for prompts
  • McpExportToolCallbackPublisher for tools (mode-agnostic)

Example Sync Publisher

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"}"""
    }
}

Sync Mode Internals

  • Wraps blocking calls in Mono.fromRunnable() or Mono.empty()
  • Compatible with reactive streams (returns Mono)
  • Uses McpSyncServer from MCP protocol library

Asynchronous Mode

When to Use

Choose ASYNC mode when:

  • High concurrency needed
  • Non-blocking I/O required
  • Scalability is critical
  • Using reactive libraries (WebFlux, R2DBC, etc.)
  • Production environments with high load

Characteristics

  • Non-Blocking Operations: Operations return immediately with Mono/Flux
  • Event Loop Model: Efficient resource utilization
  • Backpressure Support: Built-in flow control
  • Reactive Programming: Full reactive streams support

Publisher Interfaces

Async mode uses:

  • McpAsyncResourcePublisher for resources
  • McpAsyncPromptPublisher for prompts
  • McpExportToolCallbackPublisher for tools (mode-agnostic)

Example Async Publisher

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())
    }
}

Async Mode Internals

  • All operations return Mono<T> or Flux<T>
  • Uses McpAsyncServer from MCP protocol library
  • Integrates with Spring WebFlux

Mode Comparison

AspectSync ModeAsync Mode
ConcurrencyThread-per-requestEvent loop
BlockingAcceptableAvoided
Resource UsageHigher (thread overhead)Lower (shared threads)
ComplexitySimplerMore complex
ScalabilityLimited by threadsHighly scalable
Error HandlingTraditional exceptionsMono error signals
DebuggingEasierMore challenging
PerformanceGood for low concurrencyExcellent for high concurrency
Learning CurveGentleSteeper (reactive)

Switching Between Modes

Configuration Change

Simply update application.properties:

# From SYNC to ASYNC
spring.ai.mcp.server.type=ASYNC

Code Adjustments Required

Publishers

Change 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> = // ...
}

Resource Specifications

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"
                )
            )
        )
    )
}

Prompts

Change factory class:

// Sync
val factory = McpPromptFactory()
factory.syncPromptSpecificationForType(goal, inputType)

// Async
val factory = McpAsyncPromptFactory()
factory.asyncPromptSpecificationForType(goal, inputType)

Mode-Specific Publishers

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"}""" }
}

Best Practices

Sync Mode

  1. Keep Operations Simple: Leverage blocking model for straightforward code
  2. Acceptable Latency: Suitable when response time requirements are relaxed
  3. Thread Pool Sizing: Configure thread pools appropriately for expected load
  4. Database Access: Use traditional JDBC connections

Async Mode

  1. Avoid Blocking: Never block in async publishers or handlers
  2. Use Reactive Libraries: Leverage WebFlux, R2DBC, reactive Redis, etc.
  3. Schedulers: Use appropriate schedulers for different workloads
  4. Error Handling: Handle Mono/Flux errors with operators

Both Modes

  1. Tool Publishers: McpExportToolCallbackPublisher works in both modes
  2. Strategy Pattern: Code against McpServerStrategy interface for portability
  3. Conditional Beans: Use @ConditionalOnProperty for mode-specific components
  4. Testing: Test both modes if supporting both

Performance Considerations

Sync Mode Performance

  • Throughput: Limited by thread pool size
  • Latency: Acceptable for low concurrency
  • Memory: Higher due to thread overhead
  • Context Switching: More frequent with many threads

Async Mode Performance

  • Throughput: Excellent, scales with CPU cores
  • Latency: Lower under high load
  • Memory: Efficient memory usage
  • Context Switching: Minimal with event loop

Load Testing Results Example

Example metrics (application-specific):

MetricSync (100 threads)Async (Event Loop)
Max Throughput~1000 req/s~5000 req/s
P99 Latency (low load)50ms45ms
P99 Latency (high load)500ms150ms
Memory Usage500MB200MB

Migration Strategy

Gradual Migration

  1. Start with Sync: Develop in sync mode for simplicity
  2. Identify Bottlenecks: Profile and identify blocking operations
  3. Add Async Publishers: Introduce async publishers for hot paths
  4. Test Both Modes: Ensure application works in both modes
  5. Switch in Production: Change mode in production when ready

Coexistence Pattern

Run both modes in different environments:

# Development
spring.ai.mcp.server.type=SYNC

# Production
spring.ai.mcp.server.type=ASYNC

Related Documentation

  • Configuration Guide - All configuration options
  • Publishers Guide - Creating publishers for both modes
  • Performance Guide - Optimization strategies
  • Server Strategy API - Strategy interface details
tessl i tessl/maven-com-embabel-agent--embabel-agent-mcpserver@0.3.1

docs

index.md

tile.json