Spring Boot starter providing auto-configuration for Model Context Protocol (MCP) client with Spring WebFlux, enabling reactive AI applications to connect to MCP servers via SSE and Streamable HTTP transports
Auto-configuration for WebFlux-based Streamable HTTP client transport in the Model Context Protocol (MCP).
The Streamable HTTP transport auto-configuration provides reactive, bidirectional HTTP-based streaming for MCP communication. This transport is ideal for request-response patterns with streaming capabilities, allowing both the client and server to send and receive streaming data over HTTP.
package org.springframework.ai.mcp.client.webflux.autoconfigure;
/**
* Auto-configuration for WebFlux-based Streamable HTTP client transport.
* Conditionally enabled based on class presence and properties.
* Creates transport beans for each configured connection.
* Thread-safe - transports use reactive streams.
*/
@org.springframework.boot.autoconfigure.AutoConfiguration
@org.springframework.boot.autoconfigure.condition.ConditionalOnClass({
io.modelcontextprotocol.client.transport.WebClientStreamableHttpTransport.class,
org.springframework.web.reactive.function.client.WebClient.class
})
@org.springframework.boot.context.properties.EnableConfigurationProperties({
org.springframework.ai.mcp.client.common.autoconfigure.properties.McpStreamableHttpClientProperties.class,
org.springframework.ai.mcp.client.common.autoconfigure.properties.McpClientCommonProperties.class
})
@org.springframework.boot.autoconfigure.condition.ConditionalOnProperty(
prefix = "spring.ai.mcp.client",
name = "enabled",
havingValue = "true",
matchIfMissing = true
)
public class StreamableHttpWebFluxTransportAutoConfiguration {
}This auto-configuration is conditionally enabled when:
io.modelcontextprotocol.client.transport.WebClientStreamableHttpTransport and org.springframework.web.reactive.function.client.WebClient are on the classpathspring.ai.mcp.client.enabled is true (default) or not specifiedIf these conditions are not met, the auto-configuration is skipped.
Maven:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>io.modelcontextprotocol</groupId>
<artifactId>mcp-spring-webflux</artifactId>
</dependency>Gradle:
implementation 'org.springframework.boot:spring-boot-starter-webflux'
implementation 'io.modelcontextprotocol:mcp-spring-webflux'package org.springframework.ai.mcp.client.webflux.autoconfigure;
/**
* Creates a list of WebFlux-based Streamable HTTP transports for MCP communication.
*
* Each transport is configured with:
* - A cloned WebClient.Builder with server-specific base URL
* - ObjectMapper for JSON processing
* - Server connection parameters from properties
*
* Process:
* 1. Get WebClient.Builder (provided or default)
* 2. Get ObjectMapper (provided or default)
* 3. For each configured connection:
* a. Clone WebClient.Builder
* b. Set base URL from connection parameters
* c. Create WebClientStreamableHttpTransport with builder and endpoint
* d. Wrap in NamedClientMcpTransport with connection name
*
* Thread-safe - uses immutable configurations.
* Reactive - transports use Spring WebClient.
*
* @param streamableProperties Streamable HTTP client properties containing server configurations
* @param webClientBuilderProvider Provider for WebClient.Builder (uses WebClient.builder() if not provided)
* @param objectMapperProvider Provider for ObjectMapper (creates new ObjectMapper() if not provided)
* @return List of named MCP transports, one for each configured connection (never null, may be empty)
* @throws IllegalArgumentException if connection configuration is invalid
*/
@org.springframework.context.annotation.Bean
public java.util.List<org.springframework.ai.mcp.client.common.autoconfigure.NamedClientMcpTransport> streamableHttpWebFluxClientTransports(
org.springframework.ai.mcp.client.common.autoconfigure.properties.McpStreamableHttpClientProperties streamableProperties,
org.springframework.beans.factory.ObjectProvider<org.springframework.web.reactive.function.client.WebClient.Builder> webClientBuilderProvider,
org.springframework.beans.factory.ObjectProvider<com.fasterxml.jackson.databind.ObjectMapper> objectMapperProvider
);Return Type: List<NamedClientMcpTransport>
Configure Streamable HTTP connections in application.yml:
spring.ai.mcp.client.streamable-http:
connections:
analysis-service:
url: http://localhost:8080
report-service:
url: http://localhost:9000
endpoint: /custom/mcpProperties format:
spring.ai.mcp.client.streamable-http.connections.analysis-service.url=http://localhost:8080
spring.ai.mcp.client.streamable-http.connections.report-service.url=http://localhost:9000
spring.ai.mcp.client.streamable-http.connections.report-service.endpoint=/custom/mcpSee Configuration Properties for complete Streamable HTTP configuration options.
/mcp (used if endpoint is not specified)WebClient.builder() if not availablenew ObjectMapper() if not availableimport org.springframework.ai.mcp.client.common.autoconfigure.NamedClientMcpTransport;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* Service demonstrating Streamable HTTP transport usage.
* Typically you don't use transports directly - inject McpSyncClient or McpAsyncClient instead.
* This example shows how to access transports for advanced scenarios.
*/
@Service
public class McpStreamableHttpService {
private final List<NamedClientMcpTransport> streamableHttpTransports;
/**
* Constructor injection of Streamable HTTP transports.
* If no connections configured, list will be empty.
*
* @param streamableHttpTransports Auto-configured transports
*/
public McpStreamableHttpService(List<NamedClientMcpTransport> streamableHttpTransports) {
this.streamableHttpTransports = streamableHttpTransports;
}
/**
* Find specific transport by name.
* Throws if not found - check existence first in production.
*
* @throws IllegalStateException if transport not found
*/
public void connectToAnalysisService() {
// Find the analysis-service transport
NamedClientMcpTransport analysisTransport = streamableHttpTransports.stream()
.filter(t -> "analysis-service".equals(t.name()))
.findFirst()
.orElseThrow(() -> new IllegalStateException("analysis-service transport not found"));
// Use the transport (typically via an MCP client)
io.modelcontextprotocol.spec.McpClientTransport transport = analysisTransport.transport();
// ... connect and use transport
}
/**
* List all configured Streamable HTTP connections.
* Useful for debugging and monitoring.
*/
public void listAllStreamableHttpConnections() {
streamableHttpTransports.forEach(t ->
System.out.println("Streamable HTTP Transport: " + t.name())
);
}
}If your application uses both SSE and Streamable HTTP transports, you can filter by checking the transport implementation type:
import io.modelcontextprotocol.client.transport.WebClientStreamableHttpTransport;
/**
* Filter transports by type.
* Useful when multiple transport types are configured.
*
* @param allTransports All configured transports
*/
public void findStreamableHttpTransports(List<NamedClientMcpTransport> allTransports) {
List<NamedClientMcpTransport> streamableHttpTransports = allTransports.stream()
.filter(t -> t.transport() instanceof WebClientStreamableHttpTransport)
.toList();
// Use only Streamable HTTP transports
streamableHttpTransports.forEach(t -> {
System.out.println("Streamable HTTP: " + t.name());
});
}You can configure both SSE and Streamable HTTP transports in the same application:
spring.ai.mcp.client:
# SSE connections for streaming updates
sse:
connections:
notifications:
url: http://localhost:8080
sse-endpoint: /notifications/sse
# Streamable HTTP connections for request-response
streamable-http:
connections:
api:
url: http://localhost:9000
endpoint: /api/mcp
data:
url: http://localhost:9001Then use them based on your needs:
import org.springframework.ai.mcp.client.common.autoconfigure.NamedClientMcpTransport;
import io.modelcontextprotocol.client.transport.WebFluxSseClientTransport;
import io.modelcontextprotocol.client.transport.WebClientStreamableHttpTransport;
import java.util.List;
/**
* Service that uses both SSE and Streamable HTTP transports.
* Demonstrates categorizing transports by type.
*/
@Service
public class McpService {
private final List<NamedClientMcpTransport> allTransports;
public McpService(List<NamedClientMcpTransport> allTransports) {
this.allTransports = allTransports;
}
/**
* Categorize transports by type.
* Useful for routing requests to appropriate transport.
*/
public void categorizeTransports() {
allTransports.forEach(transport -> {
if (transport.transport() instanceof WebFluxSseClientTransport) {
System.out.println("SSE: " + transport.name());
} else if (transport.transport() instanceof WebClientStreamableHttpTransport) {
System.out.println("Streamable HTTP: " + transport.name());
}
});
}
}To customize the WebClient.Builder used by all Streamable HTTP transports:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.client.WebClient;
import io.netty.channel.ChannelOption;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import reactor.netty.http.client.HttpClient;
import reactor.netty.resources.ConnectionProvider;
import java.time.Duration;
/**
* Configuration for customizing WebClient used by Streamable HTTP transports.
* Applied to all Streamable HTTP connections.
*/
@Configuration
public class WebClientConfig {
/**
* Custom WebClient.Builder with connection pooling and timeouts.
* Will be cloned for each connection with connection-specific base URL.
* Thread-safe - builder is cloned, not shared.
*
* @return Configured WebClient.Builder
*/
@Bean
public WebClient.Builder webClientBuilder() {
// Configure connection pool
ConnectionProvider provider = ConnectionProvider.builder("mcp-pool")
.maxConnections(100) // Max total connections
.maxIdleTime(Duration.ofSeconds(30)) // Idle timeout
.maxLifeTime(Duration.ofMinutes(5)) // Max connection lifetime
.pendingAcquireTimeout(Duration.ofSeconds(60)) // Wait timeout
.evictInBackground(Duration.ofSeconds(120)) // Cleanup interval
.build();
// Configure HTTP client with custom timeouts
HttpClient httpClient = HttpClient.create(provider)
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000) // 5 second connect timeout
.option(ChannelOption.SO_KEEPALIVE, true) // Enable TCP keepalive
.option(ChannelOption.TCP_NODELAY, true) // Disable Nagle's algorithm
.responseTimeout(Duration.ofSeconds(30)) // 30 second response timeout
.compress(true); // Enable compression
return WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
.defaultHeader("User-Agent", "MyApp-MCP-Client/1.0")
.defaultHeader("Accept", "application/json")
.codecs(configurer -> configurer
.defaultCodecs()
.maxInMemorySize(16 * 1024 * 1024)); // 16MB buffer
}
}This WebClient.Builder will be cloned and customized for each Streamable HTTP connection with its specific base URL.
To customize JSON serialization for all Streamable HTTP transports:
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Configuration for JSON serialization in Streamable HTTP transports.
* Applied to all connections.
*/
@Configuration
public class JacksonConfig {
/**
* Custom ObjectMapper for JSON processing.
* Configures date handling, unknown properties, etc.
* Thread-safe after configuration.
*
* @return Configured ObjectMapper
*/
@Bean
public ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();
// Register Java 8 date/time module
mapper.registerModule(new JavaTimeModule());
// Configure serialization
mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
mapper.configure(SerializationFeature.INDENT_OUTPUT, true); // Pretty print for debugging
// Configure deserialization
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); // Lenient
mapper.configure(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL, true);
return mapper;
}
}Internally, the auto-configuration creates Streamable HTTP transports using the MCP SDK's builder pattern:
// Conceptual example (not for direct use) - shows internal process
package org.springframework.ai.mcp.client.webflux.autoconfigure;
// For each configured connection:
for (Map.Entry<String, ConnectionParameters> entry : connections.entrySet()) {
String connectionName = entry.getKey();
ConnectionParameters params = entry.getValue();
// Clone WebClient.Builder and set base URL
var webClientBuilder = webClientBuilderTemplate.clone()
.baseUrl(params.url());
// Determine endpoint (use default if not specified)
String streamableHttpEndpoint = params.endpoint() != null
? params.endpoint()
: "/mcp";
// Create transport
var transport = io.modelcontextprotocol.client.transport.WebClientStreamableHttpTransport.builder(webClientBuilder)
.endpoint(streamableHttpEndpoint)
.jsonMapper(new io.modelcontextprotocol.json.jackson.JacksonMcpJsonMapper(objectMapper))
.build();
// Wrap in named transport
transports.add(new NamedClientMcpTransport(connectionName, transport));
}Each configured connection gets its own transport instance, allowing independent configuration and lifecycle management.
application/json for request/responseConfigure different servers for different environments:
# application-dev.yml
spring.ai.mcp.client.streamable-http:
connections:
api:
url: http://localhost:9000
# application-prod.yml
spring.ai.mcp.client.streamable-http:
connections:
api:
url: https://api.production.com
endpoint: /v1/mcpYou can provide connections programmatically by creating a custom bean:
import org.springframework.ai.mcp.client.common.autoconfigure.properties.McpStreamableHttpClientProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Configuration providing dynamic connection details.
* Useful for service discovery, database-driven configuration, etc.
*/
@Configuration
public class DynamicConnectionConfig {
/**
* Provide connections dynamically.
* Can load from database, service registry, etc.
*
* @return Streamable HTTP properties with dynamic connections
*/
@Bean
public McpStreamableHttpClientProperties streamableHttpClientProperties() {
McpStreamableHttpClientProperties properties = new McpStreamableHttpClientProperties();
// Add connections programmatically
properties.getConnections().put(
"dynamic-server",
new McpStreamableHttpClientProperties.ConnectionParameters(
discoverServerUrl(), // Dynamic URL discovery
"/mcp/v2"
)
);
// Conditional connection based on runtime logic
if (System.getenv("ENABLE_BACKUP_SERVER") != null) {
properties.getConnections().put(
"backup-server",
new McpStreamableHttpClientProperties.ConnectionParameters(
"http://backup-host:8080",
"/mcp"
)
);
}
return properties;
}
private String discoverServerUrl() {
// Service discovery logic
return "http://discovered-host:8080";
}
}The WebFlux WebClient uses Reactor Netty under the hood, which provides connection pooling by default. You can customize the connection pool:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import reactor.netty.http.client.HttpClient;
import reactor.netty.resources.ConnectionProvider;
import java.time.Duration;
/**
* Configuration optimizing connection pooling for high-performance scenarios.
*/
@Configuration
public class ConnectionPoolConfig {
/**
* Configure optimized connection pool.
* Tuned for high-concurrency MCP operations.
*
* @return WebClient.Builder with optimized connection pool
*/
@Bean
public WebClient.Builder webClientBuilder() {
// Configure connection pool
ConnectionProvider provider = ConnectionProvider.builder("mcp-pool")
.maxConnections(100) // Increase for high concurrency
.maxIdleTime(Duration.ofSeconds(30)) // Keep connections alive
.maxLifeTime(Duration.ofMinutes(5)) // Recycle old connections
.pendingAcquireTimeout(Duration.ofSeconds(60)) // Wait for available connection
.evictInBackground(Duration.ofSeconds(120)) // Background cleanup
.metrics(true) // Enable metrics
.build();
HttpClient httpClient = HttpClient.create(provider);
return WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient));
}
}If no Streamable HTTP transports are created, check:
io.modelcontextprotocol.sdk:mcp-spring-webflux and Spring WebFlux dependencies are on the classpathspring.ai.mcp.client.streamable-http.connections is properly configuredspring.ai.mcp.client.enabled=true (or not set, as true is default)WebClient class is available (should be with Spring WebFlux)logging.level.org.springframework.ai.mcp=DEBUGIf Streamable HTTP connections fail:
url values are correct and accessible
curl -X POST http://localhost:9000/mcpendpoint matches the server's expected pathapplication/jsonIf experiencing performance problems:
maxInMemorySize in codec configurationIf experiencing memory problems:
maxInMemorySize in WebClient codec configurationIf you see bean definition conflicts:
@Primary or @Qualifier annotations to resolve ambiguity| Feature | SSE Transport | Streamable HTTP Transport |
|---|---|---|
| Direction | Unidirectional (server to client) | Bidirectional |
| Use Case | Server push, notifications, live updates | Request-response with streaming |
| Connection | Persistent connection | Per-request connection (can be kept alive) |
| Protocol | Server-Sent Events | HTTP with streaming |
| Overhead | Lower (persistent connection) | Slightly higher (HTTP overhead) |
| Firewall Friendly | Less (long-lived connections) | More (standard HTTP) |
| Reconnection | Automatic | Per-request |
| Complexity | Simple (server push) | More flexible (bidirectional) |
Choose SSE for:
Choose Streamable HTTP for:
For HTTPS connections:
import io.netty.handler.ssl.SslContextBuilder;
@Bean
public WebClient.Builder secureWebClientBuilder() throws Exception {
SslContext sslContext = SslContextBuilder
.forClient()
.trustManager(trustManagerFactory) // Use proper trust store
.build();
HttpClient httpClient = HttpClient.create()
.secure(sslSpec -> sslSpec.sslContext(sslContext));
return WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient));
}Add authentication:
@Bean
public WebClient.Builder authenticatedWebClientBuilder() {
return WebClient.builder()
.defaultHeader("Authorization", "Bearer " + getToken())
.filter((request, next) -> {
// Refresh token if needed
String token = getToken();
ClientRequest authenticated = ClientRequest.from(request)
.header("Authorization", "Bearer " + token)
.build();
return next.exchange(authenticated);
});
}tessl i tessl/maven-org-springframework-ai--spring-ai-starter-mcp-client-webflux@1.1.0