CtrlK
CommunityDocumentationLog inGet started
Tessl Logo

tessl/maven-org-springframework-ai--spring-ai-starter-mcp-client-webflux

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

Overview
Eval results
Files

mcp-clients.mddocs/reference/

Working with MCP Clients

Guide to using the MCP client beans provided by the auto-configuration for direct interaction with MCP servers.

Overview

The Spring AI MCP Client WebFlux starter automatically creates MCP client beans that you can inject into your Spring components. These clients provide direct access to MCP protocol operations like listing tools, executing tools, accessing resources, and using prompts.

There are two types of clients:

  • Synchronous (McpSyncClient): Blocking API for traditional synchronous code
  • Asynchronous (McpAsyncClient): Reactive API using Project Reactor for non-blocking operations

Key Features

  • Auto-Configuration: Clients created automatically based on configuration
  • Multiple Connections: One client per configured connection
  • Lifecycle Management: Automatic initialization and cleanup
  • Thread-Safe: Both client types are thread-safe
  • Spring Integration: Full Spring bean lifecycle support

Client Type Configuration

Configure which type of client to create using the spring.ai.mcp.client.type property:

spring.ai.mcp.client:
  type: SYNC  # or ASYNC (default: SYNC)

Mutually Exclusive: Only one client type is created per application (either all SYNC or all ASYNC).

Auto-Configuration Class

package org.springframework.ai.mcp.client.common.autoconfigure;

/**
 * Auto-configuration for MCP client support.
 * Creates either synchronous or asynchronous MCP clients based on configuration.
 * Conditionally enabled based on class presence and properties.
 * Thread-safe - clients are created during Spring context initialization.
 */
@org.springframework.boot.autoconfigure.AutoConfiguration
@org.springframework.boot.autoconfigure.condition.ConditionalOnClass(io.modelcontextprotocol.spec.McpSchema.class)
@org.springframework.boot.context.properties.EnableConfigurationProperties(
    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 McpClientAutoConfiguration {
}

Synchronous Clients

Bean Definition

package org.springframework.ai.mcp.client.common.autoconfigure;

/**
 * Creates a list of synchronous MCP clients, one per configured transport.
 * Created only when spring.ai.mcp.client.type=SYNC (default).
 * Thread-safe for concurrent use - all operations are synchronized internally.
 *
 * @param mcpSyncClientConfigurer Configurer for customizing client creation
 * @param commonProperties Common MCP client properties (timeout, name, etc.)
 * @param transportsProvider Provider of named MCP transports (SSE, stdio, streamable HTTP)
 * @param clientMcpSyncHandlersRegistry Optional registry for annotation-based handlers
 * @return List of configured synchronous MCP clients (never null, may be empty)
 * @throws IllegalStateException if client creation fails
 */
@org.springframework.context.annotation.Bean
@org.springframework.boot.autoconfigure.condition.ConditionalOnProperty(
    prefix = "spring.ai.mcp.client",
    name = "type",
    havingValue = "SYNC",
    matchIfMissing = true
)
public java.util.List<io.modelcontextprotocol.client.McpSyncClient> mcpSyncClients(
    org.springframework.ai.mcp.client.common.autoconfigure.configurer.McpSyncClientConfigurer mcpSyncClientConfigurer,
    org.springframework.ai.mcp.client.common.autoconfigure.properties.McpClientCommonProperties commonProperties,
    org.springframework.beans.factory.ObjectProvider<java.util.List<NamedClientMcpTransport>> transportsProvider,
    org.springframework.beans.factory.ObjectProvider<org.springframework.ai.mcp.annotation.spring.ClientMcpSyncHandlersRegistry> clientMcpSyncHandlersRegistry
);

Usage Example

import io.modelcontextprotocol.client.McpSyncClient;
import io.modelcontextprotocol.spec.McpSchema;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;

/**
 * Service demonstrating synchronous MCP client usage.
 * All operations block until complete - suitable for traditional code.
 * Thread-safe - multiple threads can safely use these clients.
 */
@Service
public class McpToolService {

    private final List<McpSyncClient> mcpClients;

    /**
     * Constructor injection of MCP clients.
     * Clients are already initialized and ready to use (if initialized=true in config).
     *
     * @param mcpClients Auto-configured MCP sync clients
     */
    public McpToolService(List<McpSyncClient> mcpClients) {
        this.mcpClients = mcpClients;
    }

    /**
     * List all available tools from all connected servers.
     * Blocks until all clients respond.
     * Thread-safe.
     *
     * @throws io.modelcontextprotocol.spec.McpException if MCP protocol error occurs
     * @throws java.util.concurrent.TimeoutException if request times out
     * @throws java.io.IOException if transport error occurs
     */
    public void listToolsFromAllServers() {
        for (McpSyncClient client : mcpClients) {
            // Get client info (local, doesn't require server call)
            McpSchema.Implementation clientInfo = client.getClientInfo();
            System.out.println("Client: " + clientInfo.name() + " v" + clientInfo.version());

            // Get server initialization result for server info (cached, no server call)
            McpSchema.InitializeResult initResult = client.getCurrentInitializationResult();
            System.out.println("Server: " + initResult.serverInfo().name());

            // List available tools (makes server call, may block)
            List<McpSchema.Tool> tools = client.listTools();
            tools.forEach(tool ->
                System.out.println("  Tool: " + tool.name() + " - " + 
                    (tool.description() != null ? tool.description() : "No description"))
            );
        }
    }

    /**
     * Execute a tool with given arguments.
     * Finds first client that has the tool and executes it.
     * Blocks until execution completes.
     *
     * @param toolName Name of tool to execute
     * @param arguments Tool arguments as map
     * @return Tool execution result content
     * @throws IllegalArgumentException if tool not found
     */
    public Object executeTool(String toolName, Map<String, Object> arguments) {
        // Find the first client that has the specified tool
        for (McpSyncClient client : mcpClients) {
            List<McpSchema.Tool> tools = client.listTools();
            boolean hasTool = tools.stream()
                .anyMatch(t -> t.name().equals(toolName));

            if (hasTool) {
                // Build call tool request
                McpSchema.CallToolRequest request = McpSchema.CallToolRequest.builder()
                    .name(toolName)
                    .arguments(arguments)
                    .build();
                
                // Execute tool (blocks until complete)
                McpSchema.CallToolResult result = client.callTool(request);
                
                // Return result content (may be text, image, or embedded resource)
                return result.content();
            }
        }
        throw new IllegalArgumentException("Tool not found: " + toolName);
    }

    /**
     * List all available resources from first client.
     * Resources are server-provided data (files, data, etc.).
     *
     * @return List of resources
     */
    public List<McpSchema.Resource> listResources() {
        if (!mcpClients.isEmpty()) {
            return mcpClients.get(0).listResources();
        }
        return List.of();
    }

    /**
     * Read resource content from server.
     *
     * @param uri Resource URI to read
     * @return Resource content
     */
    public McpSchema.ReadResourceResult readResource(String uri) {
        if (!mcpClients.isEmpty()) {
            McpSchema.ReadResourceRequest request = McpSchema.ReadResourceRequest.builder()
                .uri(uri)
                .build();
            return mcpClients.get(0).readResource(request);
        }
        throw new IllegalStateException("No MCP clients available");
    }

    /**
     * List all available prompts from first client.
     * Prompts are server-provided prompt templates.
     *
     * @return List of prompts
     */
    public List<McpSchema.Prompt> listPrompts() {
        if (!mcpClients.isEmpty()) {
            return mcpClients.get(0).listPrompts();
        }
        return List.of();
    }
}

Closeable Wrapper Bean

The auto-configuration also creates a CloseableMcpSyncClients wrapper bean that implements AutoCloseable to ensure proper cleanup of MCP client resources when the application context is closed.

package org.springframework.ai.mcp.client.common.autoconfigure;

/**
 * Creates a closeable wrapper for synchronous MCP clients.
 * This bean ensures all MCP clients are properly closed when the Spring
 * application context shuts down, preventing resource leaks.
 * Automatic - you don't need to close clients manually.
 *
 * @param clients List of MCP sync clients to wrap
 * @return Closeable wrapper for MCP sync clients
 */
@org.springframework.context.annotation.Bean
@org.springframework.boot.autoconfigure.condition.ConditionalOnProperty(
    prefix = "spring.ai.mcp.client",
    name = "type",
    havingValue = "SYNC",
    matchIfMissing = true
)
public McpClientAutoConfiguration.CloseableMcpSyncClients makeSyncClientsClosable(
    java.util.List<io.modelcontextprotocol.client.McpSyncClient> clients
);

CloseableMcpSyncClients Record:

package org.springframework.ai.mcp.client.common.autoconfigure;

/**
 * Closeable wrapper for MCP sync clients.
 * Implements AutoCloseable to ensure proper resource cleanup.
 * Registered as Spring bean with destroy method.
 * Thread-safe - close is idempotent and synchronized.
 */
public record CloseableMcpSyncClients(
    java.util.List<io.modelcontextprotocol.client.McpSyncClient> clients
) implements AutoCloseable {

    /**
     * Closes all wrapped MCP sync clients, releasing their resources.
     * Called automatically by Spring during context shutdown.
     * Thread-safe and idempotent - safe to call multiple times.
     * Closes clients in parallel for faster shutdown.
     *
     * @throws Exception if any client fails to close (logged but not propagated)
     */
    @Override
    public void close();
}

This wrapper is automatically managed by Spring's lifecycle and will close all MCP clients when the application context is destroyed.

Asynchronous Clients

Bean Definition

package org.springframework.ai.mcp.client.common.autoconfigure;

/**
 * Creates a list of asynchronous MCP clients, one per configured transport.
 * Created only when spring.ai.mcp.client.type=ASYNC.
 * Uses reactive streams (Project Reactor) for non-blocking operations.
 * Thread-safe - operations are non-blocking and use event loops.
 *
 * @param mcpAsyncClientConfigurer Configurer for customizing client creation
 * @param commonProperties Common MCP client properties (timeout, name, etc.)
 * @param transportsProvider Provider of named MCP transports (SSE, stdio, streamable HTTP)
 * @param clientMcpAsyncHandlersRegistry Optional registry for annotation-based handlers
 * @return List of configured asynchronous MCP clients (never null, may be empty)
 * @throws IllegalStateException if client creation fails
 */
@org.springframework.context.annotation.Bean
@org.springframework.boot.autoconfigure.condition.ConditionalOnProperty(
    prefix = "spring.ai.mcp.client",
    name = "type",
    havingValue = "ASYNC"
)
public java.util.List<io.modelcontextprotocol.client.McpAsyncClient> mcpAsyncClients(
    org.springframework.ai.mcp.client.common.autoconfigure.configurer.McpAsyncClientConfigurer mcpAsyncClientConfigurer,
    org.springframework.ai.mcp.client.common.autoconfigure.properties.McpClientCommonProperties commonProperties,
    org.springframework.beans.factory.ObjectProvider<java.util.List<NamedClientMcpTransport>> transportsProvider,
    org.springframework.beans.factory.ObjectProvider<org.springframework.ai.mcp.annotation.spring.ClientMcpAsyncHandlersRegistry> clientMcpAsyncHandlersRegistry
);

Usage Example

import io.modelcontextprotocol.client.McpAsyncClient;
import io.modelcontextprotocol.spec.McpSchema;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.List;
import java.util.Map;

/**
 * Service demonstrating asynchronous MCP client usage.
 * All operations are non-blocking and return reactive types.
 * High throughput for concurrent operations.
 */
@Service
public class McpAsyncToolService {

    private final List<McpAsyncClient> mcpClients;

    /**
     * Constructor injection of MCP async clients.
     * Clients use reactive streams for non-blocking operations.
     *
     * @param mcpClients Auto-configured MCP async clients
     */
    public McpAsyncToolService(List<McpAsyncClient> mcpClients) {
        this.mcpClients = mcpClients;
    }

    /**
     * List all tools from all clients reactively.
     * Non-blocking - returns immediately with a Flux.
     * Operations execute in parallel on event loops.
     *
     * @return Flux of all tools from all clients
     */
    public Flux<McpSchema.Tool> listAllTools() {
        // List tools from all clients reactively
        return Flux.fromIterable(mcpClients)
            .flatMap(client -> client.listTools().flatMapMany(Flux::fromIterable));
    }

    /**
     * Execute tool on the first client that has it.
     * Non-blocking - returns Mono that completes when tool execution finishes.
     *
     * @param toolName Tool name to execute
     * @param arguments Tool arguments
     * @return Mono with tool execution result
     */
    public Mono<Object> executeTool(String toolName, Map<String, Object> arguments) {
        // Execute tool on the first client that has it
        return Flux.fromIterable(mcpClients)
            .flatMap(client ->
                client.listTools()
                    .flatMapMany(Flux::fromIterable)
                    .filter(tool -> tool.name().equals(toolName))
                    .next()
                    .flatMap(tool -> {
                        McpSchema.CallToolRequest request = McpSchema.CallToolRequest.builder()
                            .name(toolName)
                            .arguments(arguments)
                            .build();
                        return client.callTool(request)
                            .map(McpSchema.CallToolResult::content);
                    })
            )
            .next()
            .switchIfEmpty(Mono.error(
                new IllegalArgumentException("Tool not found: " + toolName)
            ));
    }

    /**
     * List all resources from all clients.
     * Non-blocking with parallel execution.
     *
     * @return Flux of all resources
     */
    public Flux<McpSchema.Resource> listAllResources() {
        return Flux.fromIterable(mcpClients)
            .flatMap(client -> client.listResources().flatMapMany(Flux::fromIterable));
    }

    /**
     * Read resource from first available client.
     *
     * @param uri Resource URI
     * @return Mono with resource content
     */
    public Mono<McpSchema.ReadResourceResult> readResource(String uri) {
        if (!mcpClients.isEmpty()) {
            McpSchema.ReadResourceRequest request = McpSchema.ReadResourceRequest.builder()
                .uri(uri)
                .build();
            return mcpClients.get(0).readResource(request);
        }
        return Mono.error(new IllegalStateException("No MCP clients available"));
    }
    
    /**
     * Execute multiple tools in parallel.
     * Demonstrates parallel non-blocking execution.
     *
     * @param toolRequests Map of tool names to arguments
     * @return Flux of execution results
     */
    public Flux<ToolResult> executeToolsInParallel(Map<String, Map<String, Object>> toolRequests) {
        return Flux.fromIterable(toolRequests.entrySet())
            .flatMap(entry -> 
                executeTool(entry.getKey(), entry.getValue())
                    .map(result -> new ToolResult(entry.getKey(), result))
                    .onErrorResume(error -> 
                        Mono.just(new ToolResult(entry.getKey(), error))
                    )
            );
    }
    
    /**
     * Result of tool execution (success or error).
     */
    public record ToolResult(String toolName, Object result) {}
}

Closeable Wrapper Bean

The auto-configuration also creates a CloseableMcpAsyncClients wrapper bean that implements AutoCloseable to ensure proper cleanup of MCP client resources when the application context is closed.

package org.springframework.ai.mcp.client.common.autoconfigure;

/**
 * Creates a closeable wrapper for asynchronous MCP clients.
 * This bean ensures all MCP clients are properly closed when the Spring
 * application context shuts down, preventing resource leaks.
 * Automatic - you don't need to close clients manually.
 *
 * @param clients List of MCP async clients to wrap
 * @return Closeable wrapper for MCP async clients
 */
@org.springframework.context.annotation.Bean
@org.springframework.boot.autoconfigure.condition.ConditionalOnProperty(
    prefix = "spring.ai.mcp.client",
    name = "type",
    havingValue = "ASYNC"
)
public McpClientAutoConfiguration.CloseableMcpAsyncClients makeAsyncClientsClosable(
    java.util.List<io.modelcontextprotocol.client.McpAsyncClient> clients
);

CloseableMcpAsyncClients Record:

package org.springframework.ai.mcp.client.common.autoconfigure;

/**
 * Closeable wrapper for MCP async clients.
 * Implements AutoCloseable to ensure proper resource cleanup.
 * Registered as Spring bean with destroy method.
 * Thread-safe - close is idempotent and uses reactive coordination.
 */
public record CloseableMcpAsyncClients(
    java.util.List<io.modelcontextprotocol.client.McpAsyncClient> clients
) implements AutoCloseable {

    /**
     * Closes all wrapped MCP async clients, releasing their resources.
     * Called automatically by Spring during context shutdown.
     * Thread-safe and idempotent - safe to call multiple times.
     * Closes clients reactively for proper resource cleanup.
     *
     * @throws Exception if any client fails to close (logged but not propagated)
     */
    @Override
    public void close();
}

This wrapper is automatically managed by Spring's lifecycle and will close all MCP clients when the application context is destroyed.

Common MCP Client Operations

Both sync and async clients support the same MCP protocol operations:

Client and Server Information

// Get client information (Sync) - local operation, no server call
McpSchema.Implementation clientInfo = syncClient.getClientInfo();
System.out.println("Client: " + clientInfo.name() + " v" + clientInfo.version());

// Get server initialization result including server info (Sync) - cached, no server call
McpSchema.InitializeResult initResult = syncClient.getCurrentInitializationResult();
McpSchema.ServerInfo serverInfo = initResult.serverInfo();
System.out.println("Server: " + serverInfo.name() + " v" + serverInfo.version());
System.out.println("Protocol version: " + initResult.protocolVersion());

// Access server capabilities
McpSchema.ServerCapabilities serverCaps = initResult.capabilities();
System.out.println("Supports tools: " + (serverCaps.tools() != null));
System.out.println("Supports resources: " + (serverCaps.resources() != null));
System.out.println("Supports prompts: " + (serverCaps.prompts() != null));

// Async - client info (local operation)
McpSchema.Implementation asyncClientInfo = asyncClient.getClientInfo();

// Async - server initialization result (cached)
McpSchema.InitializeResult asyncInitResult = asyncClient.getCurrentInitializationResult();

Tools

// List tools (Sync) - makes server call, may block
List<McpSchema.Tool> tools = syncClient.listTools();
tools.forEach(tool -> {
    System.out.println("Tool: " + tool.name());
    System.out.println("  Description: " + tool.description());
    System.out.println("  Input schema: " + tool.inputSchema());
});

// List tools (Async) - non-blocking, returns Mono
Mono<List<McpSchema.Tool>> toolsMono = asyncClient.listTools();

// Call a tool (Sync) - blocks until execution completes
McpSchema.CallToolRequest request = McpSchema.CallToolRequest.builder()
    .name("toolName")
    .arguments(Map.of("param1", "value1", "param2", "value2"))
    .build();
McpSchema.CallToolResult result = syncClient.callTool(request);

// Access result content (may be various types)
Object content = result.content();
boolean isError = result.isError();

// Call a tool (Async) - non-blocking
Mono<McpSchema.CallToolResult> resultMono = asyncClient.callTool(request);

Resources

// List resources (Sync) - makes server call
List<McpSchema.Resource> resources = syncClient.listResources();
resources.forEach(resource -> {
    System.out.println("Resource: " + resource.uri());
    System.out.println("  Name: " + resource.name());
    System.out.println("  MIME type: " + resource.mimeType());
    System.out.println("  Description: " + resource.description());
});

// List resources (Async)
Mono<List<McpSchema.Resource>> resourcesMono = asyncClient.listResources();

// Read a resource (Sync) - blocks until content is retrieved
McpSchema.ReadResourceRequest request = McpSchema.ReadResourceRequest.builder()
    .uri("file:///path/to/resource")
    .build();
McpSchema.ReadResourceResult result = syncClient.readResource(request);

// Access resource content
Object contents = result.contents(); // May be text, blob, or other types

// Read a resource (Async)
Mono<McpSchema.ReadResourceResult> resultMono = asyncClient.readResource(request);

Prompts

// List prompts (Sync)
List<McpSchema.Prompt> prompts = syncClient.listPrompts();
prompts.forEach(prompt -> {
    System.out.println("Prompt: " + prompt.name());
    System.out.println("  Description: " + prompt.description());
    prompt.arguments().forEach(arg -> {
        System.out.println("  Argument: " + arg.name() + 
            (arg.required() ? " (required)" : " (optional)"));
    });
});

// List prompts (Async)
Mono<List<McpSchema.Prompt>> promptsMono = asyncClient.listPrompts();

// Get a prompt (Sync)
McpSchema.GetPromptRequest request = McpSchema.GetPromptRequest.builder()
    .name("promptName")
    .arguments(Map.of("arg", "value"))
    .build();
McpSchema.GetPromptResult result = syncClient.getPrompt(request);

// Access prompt messages
List<McpSchema.PromptMessage> messages = result.messages();

// Get a prompt (Async)
Mono<McpSchema.GetPromptResult> resultMono = asyncClient.getPrompt(request);

Sampling

// Request sampling (LLM completion from server) - Sync
McpSchema.CreateMessageRequest request = McpSchema.CreateMessageRequest.builder()
    .messages(List.of(McpSchema.SamplingMessage.builder()
        .role(McpSchema.Role.USER)
        .content(McpSchema.TextContent.builder()
            .text("Complete this: Hello,")
            .build())
        .build()))
    .metadata(Map.of())
    .build();
McpSchema.CreateMessageResult result = syncClient.createMessage(request);

// Access result
McpSchema.Role responseRole = result.role();
Object responseContent = result.content();
McpSchema.StopReason stopReason = result.stopReason();

// Request sampling (Async)
Mono<McpSchema.CreateMessageResult> resultMono = asyncClient.createMessage(request);

Client Initialization

By default, clients are automatically initialized when created. You can control this with:

spring.ai.mcp.client:
  initialized: true  # default, auto-initialize on creation

If set to false, you must manually initialize clients:

// Sync client - check if initialized
McpSyncClient client = mcpClients.get(0);
if (!client.isInitialized()) {
    client.initialize(); // Blocks until initialization completes
}

// Async client - reactive initialization
McpAsyncClient asyncClient = mcpAsyncClients.get(0);
asyncClient.initialize().block(); // Block for initialization
// OR subscribe for non-blocking
asyncClient.initialize()
    .doOnSuccess(v -> log.info("Client initialized"))
    .doOnError(e -> log.error("Initialization failed", e))
    .subscribe();

Initialization Process:

  1. Connect to MCP server via transport
  2. Send MCP initialize request with client capabilities
  3. Receive server initialization response with server capabilities
  4. Mark client as initialized
  5. Ready for operations

Client Lifecycle

Clients are automatically closed when the Spring application context shuts down. The auto-configuration creates closeable wrapper beans that handle cleanup:

package org.springframework.ai.mcp.client.common.autoconfigure.McpClientAutoConfiguration;

/**
 * Wrapper that ensures proper cleanup of MCP sync clients.
 * Implements AutoCloseable for automatic resource management.
 * Thread-safe.
 */
public record CloseableMcpSyncClients(
    java.util.List<io.modelcontextprotocol.client.McpSyncClient> clients
) implements AutoCloseable {
    /**
     * Close all clients, releasing resources.
     * Idempotent - safe to call multiple times.
     * Logs errors but doesn't propagate them.
     */
    @Override
    public void close();
}

/**
 * Wrapper that ensures proper cleanup of MCP async clients.
 * Implements AutoCloseable for automatic resource management.
 * Thread-safe with reactive coordination.
 */
public record CloseableMcpAsyncClients(
    java.util.List<io.modelcontextprotocol.client.McpAsyncClient> clients
) implements AutoCloseable {
    /**
     * Close all clients, releasing resources.
     * Idempotent - safe to call multiple times.
     * Uses reactive close for proper cleanup.
     */
    @Override
    public void close();
}

You don't need to manually close clients - Spring handles this automatically.

Lifecycle Phases:

  1. Creation: Clients created during Spring context refresh
  2. Initialization: Clients initialized (if initialized=true)
  3. Usage: Clients used for MCP operations
  4. Shutdown: Clients closed during Spring context close
  5. Cleanup: Resources released (connections, threads, etc.)

Identifying Clients by Connection Name

Each client corresponds to a configured transport connection. To identify which client connects to which server, use the client info:

for (McpSyncClient client : mcpClients) {
    // Get client information
    McpSchema.Implementation clientInfo = client.getClientInfo();
    
    // Get server information
    McpSchema.InitializeResult initResult = client.getCurrentInitializationResult();
    McpSchema.ServerInfo serverInfo = initResult.serverInfo();
    
    // Client and server names help identify connections
    System.out.println("Client: " + clientInfo.name());
    System.out.println("Server: " + serverInfo.name());
    System.out.println("Protocol: " + initResult.protocolVersion());
}

The client name follows the pattern: {configured-client-name} - {connection-name}

For example, with this configuration:

spring.ai.mcp.client:
  name: my-app
  sse:
    connections:
      weather-api:
        url: http://localhost:8080

The client name will be: my-app - weather-api

Configuration Properties

Common client configuration properties:

spring.ai.mcp.client:
  enabled: true                    # Enable MCP client (default: true)
  type: SYNC                       # Client type: SYNC or ASYNC (default: SYNC)
  name: my-app                     # Client name (default: "spring-ai-mcp-client")
  version: 1.0.0                   # Client version (default: "1.0.0")
  initialized: true                # Auto-initialize (default: true)
  request-timeout: 20s             # Request timeout (default: 20 seconds)
  root-change-notification: true   # Enable root change notifications (default: true)

See Configuration Properties for complete details.

Error Handling

Synchronous Clients

import io.modelcontextprotocol.spec.McpException;
import java.util.concurrent.TimeoutException;
import java.io.IOException;

try {
    McpSchema.CallToolResult result = client.callTool(request);
    // Process result
} catch (McpException e) {
    // MCP protocol errors (e.g., server error, invalid request)
    log.error("MCP error: code={}, message={}", e.getCode(), e.getMessage(), e);
} catch (TimeoutException e) {
    // Request timeout
    log.error("Request timed out after {} seconds", 
        client.getRequestTimeout().getSeconds(), e);
} catch (IOException e) {
    // Transport error (e.g., connection lost)
    log.error("Transport error: {}", e.getMessage(), e);
} catch (com.fasterxml.jackson.core.JsonProcessingException e) {
    // JSON serialization/deserialization error
    log.error("JSON processing error: {}", e.getMessage(), e);
} catch (IllegalStateException e) {
    // Client not initialized or connection closed
    log.error("Client state error: {}", e.getMessage(), e);
} catch (Exception e) {
    // Unexpected error
    log.error("Unexpected error: {}", e.getMessage(), e);
}

Asynchronous Clients

asyncClient.callTool(request)
    .doOnNext(result -> {
        // Process result
        log.info("Tool executed successfully");
    })
    .onErrorResume(McpException.class, e -> {
        // Handle MCP protocol errors
        log.error("MCP error: {}", e.getMessage(), e);
        return Mono.empty(); // or provide fallback
    })
    .onErrorResume(TimeoutException.class, e -> {
        // Handle timeout
        log.error("Request timed out", e);
        return Mono.empty();
    })
    .onErrorResume(IOException.class, e -> {
        // Handle transport errors
        log.error("Transport error", e);
        return Mono.empty();
    })
    .onErrorResume(e -> {
        // Handle other errors
        log.error("Unexpected error", e);
        return Mono.empty();
    })
    .subscribe();

Best Practices

  1. Prefer Spring AI Tool Integration: For most use cases, use the Tool Callbacks integration instead of calling clients directly
  2. Handle Missing Servers: Always check if clients list is not empty before accessing
  3. Use Connection Names: Configure meaningful connection names to identify servers
  4. Set Appropriate Timeouts: Adjust request-timeout based on expected operation duration
  5. Async for High Throughput: Use async clients for high-concurrency scenarios
  6. Resource Management: Let Spring manage client lifecycle - don't manually close
  7. Error Handling: Always handle errors appropriately for your use case
  8. Check Initialization: Verify client is initialized before use (if initialized=false)
  9. Thread Safety: Both client types are thread-safe for concurrent use
  10. Reactive Best Practices: For async clients, follow reactive programming patterns (don't block)

Anti-Patterns to Avoid

Don't Block in Async Context:

// BAD - blocks reactive chain
Mono<String> result = asyncClient.listTools()
    .map(tools -> {
        Thread.sleep(1000); // DON'T DO THIS!
        return processTools(tools);
    });

// GOOD - use reactive operators
Mono<String> result = asyncClient.listTools()
    .delayElement(Duration.ofSeconds(1))
    .map(this::processTools);

Don't Ignore Errors:

// BAD - error silently lost
asyncClient.callTool(request).subscribe();

// GOOD - handle errors
asyncClient.callTool(request)
    .doOnError(e -> log.error("Tool execution failed", e))
    .subscribe();

Don't Create Clients Manually:

// BAD - bypasses Spring lifecycle management
McpSyncClient client = McpClient.sync(transport)...build();

// GOOD - inject auto-configured clients
@Service
public class MyService {
    private final List<McpSyncClient> clients;
    
    public MyService(List<McpSyncClient> clients) {
        this.clients = clients;
    }
}

Related Documentation

tessl i tessl/maven-org-springframework-ai--spring-ai-starter-mcp-client-webflux@1.1.0

docs

index.md

tile.json