Refactor McpClient for async and sync interactions

Introduce new builder patterns and enhance type safety

- Add new builder patterns for McpClient and McpServer with sync/async variants
- Introduce dedicated McpClientFeatures and McpServerFeatures classes
- Enhance type safety and reactive support with proper Mono integration
- Add migration guide for 0.6.0 release
- Mark old builder methods as deprecated
- Update documentation to reflect new APIs

BREAKING CHANGE: The old builder patterns using McpClient.using() and McpServer.using()
are now deprecated in favor of new sync() and async() builders that provide
better type safety and clearer sync/async separation. Follow the migration guide.

Resolves #48

Signed-off-by: Dariusz Jędrzejczyk <dariusz.jedrzejczyk@broadcom.com>
Co-authored-by: Christian Tzolov <ctzolov@vmware.com>
This commit is contained in:
Dariusz Jędrzejczyk 2025-01-20 18:33:06 +01:00 • committed by Christian Tzolov
commit 6839d3b740
9 changed files with 3494 additions and 753 deletions

View file

@ -27,6 +27,7 @@ import java.util.function.Function;
import com.fasterxml.jackson.core.type.TypeReference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
@ -55,10 +56,39 @@ import org.springframework.ai.mcp.util.Utils;
/**
* The Model Context Protocol (MCP) client implementation that provides asynchronous
* communication with MCP servers.
* communication with MCP servers using Project Reactor's Mono and Flux types.
*
* <p>
* This client implements the MCP specification, enabling AI models to interact with
* external tools and resources through a standardized interface. Key features include:
* <ul>
* <li>Asynchronous communication using reactive programming patterns
* <li>Tool discovery and invocation for server-provided functionality
* <li>Resource access and management with URI-based addressing
* <li>Prompt template handling for standardized AI interactions
* <li>Real-time notifications for tools, resources, and prompts changes
* <li>Structured logging with configurable severity levels
* <li>Message sampling for AI model interactions
* </ul>
*
* <p>
* The client follows a lifecycle:
* <ol>
* <li>Initialization - Establishes connection and negotiates capabilities
* <li>Normal Operation - Handles requests and notifications
* <li>Graceful Shutdown - Ensures clean connection termination
* </ol>
*
* <p>
* This implementation uses Project Reactor for non-blocking operations, making it
* suitable for high-throughput scenarios and reactive applications. All operations return
* Mono or Flux types that can be composed into reactive pipelines.
*
* @author Dariusz Jędrzejczyk
* @author Christian Tzolov
* @see McpClient
* @see McpSchema
* @see DefaultMcpSession
*/
public class McpAsyncClient {
@ -109,13 +139,96 @@ public class McpAsyncClient {
* request text or image-based interactions and optionally include context from MCP
* servers in their prompts.
*/
private Function<CreateMessageRequest, CreateMessageResult> samplingHandler;
private Function<CreateMessageRequest, Mono<CreateMessageResult>> samplingHandler;
/**
* Client transport implementation.
*/
private final McpTransport transport;
/**
* Create a new McpAsyncClient with the given transport and session request-response
* timeout.
* @param transport the transport to use.
* @param requestTimeout the session request-response timeout.
* @param features the MCP Client supported features.
*/
McpAsyncClient(ClientMcpTransport transport, Duration requestTimeout, McpClientFeatures.Async features) {
Assert.notNull(transport, "Transport must not be null");
Assert.notNull(requestTimeout, "Request timeout must not be null");
this.clientInfo = features.clientInfo();
this.clientCapabilities = features.clientCapabilities();
this.transport = transport;
this.roots = new ConcurrentHashMap<>(features.roots());
// Request Handlers
Map<String, RequestHandler<?>> requestHandlers = new HashMap<>();
// Roots List Request Handler
if (this.clientCapabilities.roots() != null) {
requestHandlers.put(McpSchema.METHOD_ROOTS_LIST, rootsListRequestHandler());
}
// Sampling Handler
if (this.clientCapabilities.sampling() != null) {
if (features.samplingHandler() == null) {
throw new McpError("Sampling handler must not be null when client capabilities include sampling");
}
this.samplingHandler = features.samplingHandler();
requestHandlers.put(McpSchema.METHOD_SAMPLING_CREATE_MESSAGE, samplingCreateMessageHandler());
}
// Notification Handlers
Map<String, NotificationHandler> notificationHandlers = new HashMap<>();
// Tools Change Notification
List<Function<List<McpSchema.Tool>, Mono<Void>>> toolsChangeConsumersFinal = new ArrayList<>();
toolsChangeConsumersFinal
.add((notification) -> Mono.fromRunnable(() -> logger.info("Tools changed: {}", notification)));
if (!Utils.isEmpty(features.toolsChangeConsumers())) {
toolsChangeConsumersFinal.addAll(features.toolsChangeConsumers());
}
notificationHandlers.put(McpSchema.METHOD_NOTIFICATION_TOOLS_LIST_CHANGED,
asyncToolsChangeNotificationHandler(toolsChangeConsumersFinal));
// Resources Change Notification
List<Function<List<McpSchema.Resource>, Mono<Void>>> resourcesChangeConsumersFinal = new ArrayList<>();
resourcesChangeConsumersFinal
.add((notification) -> Mono.fromRunnable(() -> logger.info("Resources changed: {}", notification)));
if (!Utils.isEmpty(features.resourcesChangeConsumers())) {
resourcesChangeConsumersFinal.addAll(features.resourcesChangeConsumers());
}
notificationHandlers.put(McpSchema.METHOD_NOTIFICATION_RESOURCES_LIST_CHANGED,
asyncResourcesChangeNotificationHandler(resourcesChangeConsumersFinal));
// Prompts Change Notification
List<Function<List<McpSchema.Prompt>, Mono<Void>>> promptsChangeConsumersFinal = new ArrayList<>();
promptsChangeConsumersFinal
.add((notification) -> Mono.fromRunnable(() -> logger.info("Prompts changed: {}", notification)));
if (!Utils.isEmpty(features.promptsChangeConsumers())) {
promptsChangeConsumersFinal.addAll(features.promptsChangeConsumers());
}
notificationHandlers.put(McpSchema.METHOD_NOTIFICATION_PROMPTS_LIST_CHANGED,
asyncPromptsChangeNotificationHandler(promptsChangeConsumersFinal));
// Utility Logging Notification
List<Function<LoggingMessageNotification, Mono<Void>>> loggingConsumersFinal = new ArrayList<>();
loggingConsumersFinal.add((notification) -> Mono.fromRunnable(() -> logger.info("Logging: {}", notification)));
if (!Utils.isEmpty(features.loggingConsumers())) {
loggingConsumersFinal.addAll(features.loggingConsumers());
}
notificationHandlers.put(McpSchema.METHOD_NOTIFICATION_MESSAGE,
asyncLoggingNotificationHandler(loggingConsumersFinal));
this.mcpSession = new DefaultMcpSession(requestTimeout, transport, requestHandlers, notificationHandlers);
}
/**
* Create a new McpAsyncClient with the given transport and session request-response
* timeout.
@ -129,7 +242,9 @@ public class McpAsyncClient {
* @param promptsChangeConsumers the prompts change consumers.
* @param loggingConsumers the logging consumers.
* @param samplingHandler the sampling handler.
* @deprecated Use {@link McpClient#async(ClientMcpTransport)} to obtain an instance.
*/
@Deprecated
public McpAsyncClient(ClientMcpTransport transport, Duration requestTimeout, Implementation clientInfo,
ClientCapabilities clientCapabilities, Map<String, Root> roots,
List<Consumer<List<McpSchema.Tool>>> toolsChangeConsumers,
@ -153,11 +268,11 @@ public class McpAsyncClient {
this.roots = roots != null ? new ConcurrentHashMap<>(roots) : new ConcurrentHashMap<>();
// Request Handlers
Map<String, RequestHandler> requestHanlers = new HashMap<>();
Map<String, RequestHandler<?>> requestHandlers = new HashMap<>();
// Roots List Request Handler
if (this.roots != null && this.clientCapabilities.roots() != null) {
requestHanlers.put(McpSchema.METHOD_ROOTS_LIST, rootsListRequestHandler());
if (this.clientCapabilities.roots() != null) {
requestHandlers.put(McpSchema.METHOD_ROOTS_LIST, rootsListRequestHandler());
}
// Sampling Handler
@ -165,8 +280,9 @@ public class McpAsyncClient {
if (samplingHandler == null) {
throw new McpError("Sampling handler must not be null when client capabilities include sampling");
}
this.samplingHandler = samplingHandler;
requestHanlers.put(McpSchema.METHOD_SAMPLING_CREATE_MESSAGE, samplingCreateMessageHandler());
this.samplingHandler = r -> Mono.fromCallable(() -> samplingHandler.apply(r))
.subscribeOn(Schedulers.boundedElastic());
requestHandlers.put(McpSchema.METHOD_SAMPLING_CREATE_MESSAGE, samplingCreateMessageHandler());
}
// Notification Handlers
@ -208,7 +324,7 @@ public class McpAsyncClient {
notificationHandlers.put(McpSchema.METHOD_NOTIFICATION_MESSAGE,
loggingNotificationHandler(loggingConsumersFinal));
this.mcpSession = new DefaultMcpSession(requestTimeout, transport, requestHanlers, notificationHandlers);
this.mcpSession = new DefaultMcpSession(requestTimeout, transport, requestHandlers, notificationHandlers);
}
@ -416,15 +532,13 @@ public class McpAsyncClient {
// --------------------------
// Sampling
// --------------------------
private RequestHandler samplingCreateMessageHandler() {
private RequestHandler<CreateMessageResult> samplingCreateMessageHandler() {
return params -> {
McpSchema.CreateMessageRequest request = transport.unmarshalFrom(params,
new TypeReference<McpSchema.CreateMessageRequest>() {
});
CreateMessageResult response = this.samplingHandler.apply(request);
return Mono.just(response);
return this.samplingHandler.apply(request);
};
}
@ -500,6 +614,26 @@ public class McpAsyncClient {
}).then(); // Convert to Mono<Void>
}
/**
* Creates a notification handler for tools/list_changed notifications from the
* server. When the server's available tools change, it sends a notification to inform
* connected clients. This handler automatically fetches the updated tool list and
* distributes it to all registered consumers.
* @param toolsChangeConsumers List of consumers that will be notified when the tools
* list changes. Each consumer receives the complete updated list of tools.
* @return A NotificationHandler that processes tools/list_changed notifications by:
* 1. Fetching the current list of tools from the server 2. Distributing the updated
* list to all registered consumers 3. Handling any errors that occur during this
* process
*/
private NotificationHandler asyncToolsChangeNotificationHandler(
List<Function<List<McpSchema.Tool>, Mono<Void>>> toolsChangeConsumers) {
// TODO: params are not used yet
return params -> listTools().flatMap(listToolsResult -> Flux.fromIterable(toolsChangeConsumers)
.flatMap(consumer -> consumer.apply(listToolsResult.tools()))
.then());
}
// --------------------------
// Resources
// --------------------------
@ -621,6 +755,13 @@ public class McpAsyncClient {
}).then();
}
private NotificationHandler asyncResourcesChangeNotificationHandler(
List<Function<List<McpSchema.Resource>, Mono<Void>>> resourcesChangeConsumers) {
return params -> listResources().flatMap(listResourcesResult -> Flux.fromIterable(resourcesChangeConsumers)
.flatMap(consumer -> consumer.apply(listResourcesResult.resources()))
.then());
}
// --------------------------
// Prompts
// --------------------------
@ -682,6 +823,13 @@ public class McpAsyncClient {
}; // @formatter:on
}
private NotificationHandler asyncPromptsChangeNotificationHandler(
List<Function<List<McpSchema.Prompt>, Mono<Void>>> promptsChangeConsumers) {
return params -> listPrompts().flatMap(listPromptsResult -> Flux.fromIterable(promptsChangeConsumers)
.flatMap(consumer -> consumer.apply(listPromptsResult.prompts()))
.then());
}
// --------------------------
// Logging
// --------------------------
@ -711,6 +859,28 @@ public class McpAsyncClient {
};
}
/**
* Create a notification handler for logging notifications from the server. This
* handler automatically distributes logging messages to all registered consumers.
* @param loggingConsumers List of consumers that will be notified when a logging
* message is received. Each consumer receives the logging message notification.
* @return A NotificationHandler that processes log notifications by distributing the
* message to all registered consumers
*/
private NotificationHandler asyncLoggingNotificationHandler(
List<Function<LoggingMessageNotification, Mono<Void>>> loggingConsumers) {
return params -> {
McpSchema.LoggingMessageNotification loggingMessageNotification = transport.unmarshalFrom(params,
new TypeReference<McpSchema.LoggingMessageNotification>() {
});
return Flux.fromIterable(loggingConsumers)
.flatMap(consumer -> consumer.apply(loggingMessageNotification))
.then();
};
}
/**
* Client can set the minimum logging level it wants to receive from the server.
* @param loggingLevel the min logging level

View file

@ -24,6 +24,8 @@ import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Function;
import reactor.core.publisher.Mono;
import org.springframework.ai.mcp.spec.ClientMcpTransport;
import org.springframework.ai.mcp.spec.McpSchema;
import org.springframework.ai.mcp.spec.McpSchema.ClientCapabilities;
@ -58,24 +60,30 @@ import org.springframework.ai.mcp.util.Assert;
* </ul>
*
* <p>
* Example of creating a basic client: <pre>{@code
* McpClient.using(transport)
* Example of creating a basic synchronous client: <pre>{@code
* McpClient.sync(transport)
* .requestTimeout(Duration.ofSeconds(5))
* .sync(); // or .async()
* .build();
* }</pre>
*
* Example of creating a basic asynchronous client: <pre>{@code
* McpClient.async(transport)
* .requestTimeout(Duration.ofSeconds(5))
* .build();
* }</pre>
*
* <p>
* Example with advanced configuration: <pre>{@code
* McpClient.using(transport)
* Example with advanced asynchronous configuration: <pre>{@code
* McpClient.async(transport)
* .requestTimeout(Duration.ofSeconds(10))
* .capabilities(new ClientCapabilities(...))
* .clientInfo(new Implementation("My Client", "1.0.0"))
* .roots(new Root("file://workspace", "Workspace Files"))
* .toolsChangeConsumer(tools -> System.out.println("Tools updated: " + tools))
* .resourcesChangeConsumer(resources -> System.out.println("Resources updated: " + resources))
* .promptsChangeConsumer(prompts -> System.out.println("Prompts updated: " + prompts))
* .loggingConsumer(message -> System.out.println("Log message: " + message))
* .async();
* .toolsChangeConsumer(tools -> Mono.fromRunnable(() -> System.out.println("Tools updated: " + tools)))
* .resourcesChangeConsumer(resources -> Mono.fromRunnable(() -> System.out.println("Resources updated: " + resources)))
* .promptsChangeConsumer(prompts -> Mono.fromRunnable(() -> System.out.println("Prompts updated: " + prompts)))
* .loggingConsumer(message -> Mono.fromRunnable(() -> System.out.println("Log message: " + message)))
* .build();
* }</pre>
*
* <p>
@ -115,7 +123,11 @@ public interface McpClient {
* and {@code SseClientTransport} for SSE-based communication.
* @return A new builder instance for configuring the client
* @throws IllegalArgumentException if transport is null
* @deprecated Use {@link #sync(ClientMcpTransport)} or
* {@link #async(ClientMcpTransport)} specification builder to configure the client
* and build an instance.
*/
@Deprecated
public static Builder using(ClientMcpTransport transport) {
return new Builder(transport);
}
@ -136,7 +148,12 @@ public interface McpClient {
* <li>Change notification handlers for tools, resources, and prompts
* <li>Custom message sampling logic
* </ul>
*
* @deprecated Use {@link #sync(ClientMcpTransport)} or
* {@link #async(ClientMcpTransport)} specification builder to instantiate an
* instance.
*/
@Deprecated
public static class Builder {
private final ClientMcpTransport transport;
@ -353,4 +370,431 @@ public interface McpClient {
}
/**
* Synchronous client specification.
*/
class SyncSpec {
private final ClientMcpTransport transport;
private Duration requestTimeout = Duration.ofSeconds(20); // Default timeout
private ClientCapabilities capabilities;
private Implementation clientInfo = new Implementation("Spring AI MCP Client", "0.3.1");
private Map<String, Root> roots = new HashMap<>();
private List<Consumer<List<McpSchema.Tool>>> toolsChangeConsumers = new ArrayList<>();
private List<Consumer<List<McpSchema.Resource>>> resourcesChangeConsumers = new ArrayList<>();
private List<Consumer<List<McpSchema.Prompt>>> promptsChangeConsumers = new ArrayList<>();
private List<Consumer<McpSchema.LoggingMessageNotification>> loggingConsumers = new ArrayList<>();
private Function<CreateMessageRequest, CreateMessageResult> samplingHandler;
private SyncSpec(ClientMcpTransport transport) {
this.transport = transport;
}
/**
* Sets the duration to wait for server responses before timing out requests. This
* timeout applies to all requests made through the client, including tool calls,
* resource access, and prompt operations.
* @param requestTimeout The duration to wait before timing out requests. Must not
* be null.
* @return This builder instance for method chaining
* @throws IllegalArgumentException if requestTimeout is null
*/
public SyncSpec requestTimeout(Duration requestTimeout) {
Assert.notNull(requestTimeout, "Request timeout must not be null");
this.requestTimeout = requestTimeout;
return this;
}
/**
* Sets the client capabilities that will be advertised to the server during
* connection initialization. Capabilities define what features the client
* supports, such as tool execution, resource access, and prompt handling.
* @param capabilities The client capabilities configuration. Must not be null.
* @return This builder instance for method chaining
* @throws IllegalArgumentException if capabilities is null
*/
public SyncSpec capabilities(ClientCapabilities capabilities) {
Assert.notNull(capabilities, "Capabilities must not be null");
this.capabilities = capabilities;
return this;
}
/**
* Sets the client implementation information that will be shared with the server
* during connection initialization. This helps with version compatibility and
* debugging.
* @param clientInfo The client implementation details including name and version.
* Must not be null.
* @return This builder instance for method chaining
* @throws IllegalArgumentException if clientInfo is null
*/
public SyncSpec clientInfo(Implementation clientInfo) {
Assert.notNull(clientInfo, "Client info must not be null");
this.clientInfo = clientInfo;
return this;
}
/**
* Sets the root URIs that this client can access. Roots define the base URIs for
* resources that the client can request from the server. For example, a root
* might be "file://workspace" for accessing workspace files.
* @param roots A list of root definitions. Must not be null.
* @return This builder instance for method chaining
* @throws IllegalArgumentException if roots is null
*/
public SyncSpec roots(List<Root> roots) {
Assert.notNull(roots, "Roots must not be null");
for (Root root : roots) {
this.roots.put(root.uri(), root);
}
return this;
}
/**
* Sets the root URIs that this client can access, using a varargs parameter for
* convenience. This is an alternative to {@link #roots(List)}.
* @param roots An array of root definitions. Must not be null.
* @return This builder instance for method chaining
* @throws IllegalArgumentException if roots is null
* @see #roots(List)
*/
public SyncSpec roots(Root... roots) {
Assert.notNull(roots, "Roots must not be null");
for (Root root : roots) {
this.roots.put(root.uri(), root);
}
return this;
}
/**
* Sets a custom sampling handler for processing message creation requests. The
* sampling handler can modify or validate messages before they are sent to the
* server, enabling custom processing logic.
* @param samplingHandler A function that processes message requests and returns
* results. Must not be null.
* @return This builder instance for method chaining
* @throws IllegalArgumentException if samplingHandler is null
*/
public SyncSpec sampling(Function<CreateMessageRequest, CreateMessageResult> samplingHandler) {
Assert.notNull(samplingHandler, "Sampling handler must not be null");
this.samplingHandler = samplingHandler;
return this;
}
/**
* Adds a consumer to be notified when the available tools change. This allows the
* client to react to changes in the server's tool capabilities, such as tools
* being added or removed.
* @param toolsChangeConsumer A consumer that receives the updated list of
* available tools. Must not be null.
* @return This builder instance for method chaining
* @throws IllegalArgumentException if toolsChangeConsumer is null
*/
public SyncSpec toolsChangeConsumer(Consumer<List<McpSchema.Tool>> toolsChangeConsumer) {
Assert.notNull(toolsChangeConsumer, "Tools change consumer must not be null");
this.toolsChangeConsumers.add(toolsChangeConsumer);
return this;
}
/**
* Adds a consumer to be notified when the available resources change. This allows
* the client to react to changes in the server's resource availability, such as
* files being added or removed.
* @param resourcesChangeConsumer A consumer that receives the updated list of
* available resources. Must not be null.
* @return This builder instance for method chaining
* @throws IllegalArgumentException if resourcesChangeConsumer is null
*/
public SyncSpec resourcesChangeConsumer(Consumer<List<McpSchema.Resource>> resourcesChangeConsumer) {
Assert.notNull(resourcesChangeConsumer, "Resources change consumer must not be null");
this.resourcesChangeConsumers.add(resourcesChangeConsumer);
return this;
}
/**
* Adds a consumer to be notified when the available prompts change. This allows
* the client to react to changes in the server's prompt templates, such as new
* templates being added or existing ones being modified.
* @param promptsChangeConsumer A consumer that receives the updated list of
* available prompts. Must not be null.
* @return This builder instance for method chaining
* @throws IllegalArgumentException if promptsChangeConsumer is null
*/
public SyncSpec promptsChangeConsumer(Consumer<List<McpSchema.Prompt>> promptsChangeConsumer) {
Assert.notNull(promptsChangeConsumer, "Prompts change consumer must not be null");
this.promptsChangeConsumers.add(promptsChangeConsumer);
return this;
}
/**
* Adds a consumer to be notified when logging messages are received from the
* server. This allows the client to react to log messages, such as warnings or
* errors, that are sent by the server.
* @param loggingConsumer A consumer that receives logging messages. Must not be
* null.
* @return This builder instance for method chaining
*/
public SyncSpec loggingConsumer(Consumer<McpSchema.LoggingMessageNotification> loggingConsumer) {
Assert.notNull(loggingConsumer, "Logging consumer must not be null");
this.loggingConsumers.add(loggingConsumer);
return this;
}
/**
* Adds multiple consumers to be notified when logging messages are received from
* the server. This allows the client to react to log messages, such as warnings
* or errors, that are sent by the server.
* @param loggingConsumers A list of consumers that receive logging messages. Must
* not be null.
* @return This builder instance for method chaining
*/
public SyncSpec loggingConsumers(List<Consumer<McpSchema.LoggingMessageNotification>> loggingConsumers) {
Assert.notNull(loggingConsumers, "Logging consumers must not be null");
this.loggingConsumers.addAll(loggingConsumers);
return this;
}
/**
* Create an instance of {@link McpSyncClient} with the provided configurations or
* sensible defaults.
* @return a new instance of {@link McpSyncClient}.
*/
public McpSyncClient build() {
McpClientFeatures.Sync syncFeatures = new McpClientFeatures.Sync(this.clientInfo, this.capabilities,
this.roots, this.toolsChangeConsumers, this.resourcesChangeConsumers, this.promptsChangeConsumers,
this.loggingConsumers, this.samplingHandler);
McpClientFeatures.Async asyncFeatures = McpClientFeatures.Async.fromSync(syncFeatures);
return new McpSyncClient(new McpAsyncClient(transport, this.requestTimeout, asyncFeatures));
}
}
/**
* Asynchronous client specification.
*/
class AsyncSpec {
private final ClientMcpTransport transport;
private Duration requestTimeout = Duration.ofSeconds(20); // Default timeout
private ClientCapabilities capabilities;
private Implementation clientInfo = new Implementation("Spring AI MCP Client", "0.3.1");
private Map<String, Root> roots = new HashMap<>();
private List<Function<List<McpSchema.Tool>, Mono<Void>>> toolsChangeConsumers = new ArrayList<>();
private List<Function<List<McpSchema.Resource>, Mono<Void>>> resourcesChangeConsumers = new ArrayList<>();
private List<Function<List<McpSchema.Prompt>, Mono<Void>>> promptsChangeConsumers = new ArrayList<>();
private List<Function<McpSchema.LoggingMessageNotification, Mono<Void>>> loggingConsumers = new ArrayList<>();
private Function<CreateMessageRequest, Mono<CreateMessageResult>> samplingHandler;
private AsyncSpec(ClientMcpTransport transport) {
this.transport = transport;
}
/**
* Sets the duration to wait for server responses before timing out requests. This
* timeout applies to all requests made through the client, including tool calls,
* resource access, and prompt operations.
* @param requestTimeout The duration to wait before timing out requests. Must not
* be null.
* @return This builder instance for method chaining
* @throws IllegalArgumentException if requestTimeout is null
*/
public AsyncSpec requestTimeout(Duration requestTimeout) {
Assert.notNull(requestTimeout, "Request timeout must not be null");
this.requestTimeout = requestTimeout;
return this;
}
/**
* Sets the client capabilities that will be advertised to the server during
* connection initialization. Capabilities define what features the client
* supports, such as tool execution, resource access, and prompt handling.
* @param capabilities The client capabilities configuration. Must not be null.
* @return This builder instance for method chaining
* @throws IllegalArgumentException if capabilities is null
*/
public AsyncSpec capabilities(ClientCapabilities capabilities) {
Assert.notNull(capabilities, "Capabilities must not be null");
this.capabilities = capabilities;
return this;
}
/**
* Sets the client implementation information that will be shared with the server
* during connection initialization. This helps with version compatibility and
* debugging.
* @param clientInfo The client implementation details including name and version.
* Must not be null.
* @return This builder instance for method chaining
* @throws IllegalArgumentException if clientInfo is null
*/
public AsyncSpec clientInfo(Implementation clientInfo) {
Assert.notNull(clientInfo, "Client info must not be null");
this.clientInfo = clientInfo;
return this;
}
/**
* Sets the root URIs that this client can access. Roots define the base URIs for
* resources that the client can request from the server. For example, a root
* might be "file://workspace" for accessing workspace files.
* @param roots A list of root definitions. Must not be null.
* @return This builder instance for method chaining
* @throws IllegalArgumentException if roots is null
*/
public AsyncSpec roots(List<Root> roots) {
Assert.notNull(roots, "Roots must not be null");
for (Root root : roots) {
this.roots.put(root.uri(), root);
}
return this;
}
/**
* Sets the root URIs that this client can access, using a varargs parameter for
* convenience. This is an alternative to {@link #roots(List)}.
* @param roots An array of root definitions. Must not be null.
* @return This builder instance for method chaining
* @throws IllegalArgumentException if roots is null
* @see #roots(List)
*/
public AsyncSpec roots(Root... roots) {
Assert.notNull(roots, "Roots must not be null");
for (Root root : roots) {
this.roots.put(root.uri(), root);
}
return this;
}
/**
* Sets a custom sampling handler for processing message creation requests. The
* sampling handler can modify or validate messages before they are sent to the
* server, enabling custom processing logic.
* @param samplingHandler A function that processes message requests and returns
* results. Must not be null.
* @return This builder instance for method chaining
* @throws IllegalArgumentException if samplingHandler is null
*/
public AsyncSpec sampling(Function<CreateMessageRequest, Mono<CreateMessageResult>> samplingHandler) {
Assert.notNull(samplingHandler, "Sampling handler must not be null");
this.samplingHandler = samplingHandler;
return this;
}
/**
* Adds a consumer to be notified when the available tools change. This allows the
* client to react to changes in the server's tool capabilities, such as tools
* being added or removed.
* @param toolsChangeConsumer A consumer that receives the updated list of
* available tools. Must not be null.
* @return This builder instance for method chaining
* @throws IllegalArgumentException if toolsChangeConsumer is null
*/
public AsyncSpec toolsChangeConsumer(Function<List<McpSchema.Tool>, Mono<Void>> toolsChangeConsumer) {
Assert.notNull(toolsChangeConsumer, "Tools change consumer must not be null");
this.toolsChangeConsumers.add(toolsChangeConsumer);
return this;
}
/**
* Adds a consumer to be notified when the available resources change. This allows
* the client to react to changes in the server's resource availability, such as
* files being added or removed.
* @param resourcesChangeConsumer A consumer that receives the updated list of
* available resources. Must not be null.
* @return This builder instance for method chaining
* @throws IllegalArgumentException if resourcesChangeConsumer is null
*/
public AsyncSpec resourcesChangeConsumer(
Function<List<McpSchema.Resource>, Mono<Void>> resourcesChangeConsumer) {
Assert.notNull(resourcesChangeConsumer, "Resources change consumer must not be null");
this.resourcesChangeConsumers.add(resourcesChangeConsumer);
return this;
}
/**
* Adds a consumer to be notified when the available prompts change. This allows
* the client to react to changes in the server's prompt templates, such as new
* templates being added or existing ones being modified.
* @param promptsChangeConsumer A consumer that receives the updated list of
* available prompts. Must not be null.
* @return This builder instance for method chaining
* @throws IllegalArgumentException if promptsChangeConsumer is null
*/
public AsyncSpec promptsChangeConsumer(Function<List<McpSchema.Prompt>, Mono<Void>> promptsChangeConsumer) {
Assert.notNull(promptsChangeConsumer, "Prompts change consumer must not be null");
this.promptsChangeConsumers.add(promptsChangeConsumer);
return this;
}
/**
* Adds a consumer to be notified when logging messages are received from the
* server. This allows the client to react to log messages, such as warnings or
* errors, that are sent by the server.
* @param loggingConsumer A consumer that receives logging messages. Must not be
* null.
* @return This builder instance for method chaining
*/
public AsyncSpec loggingConsumer(Function<McpSchema.LoggingMessageNotification, Mono<Void>> loggingConsumer) {
Assert.notNull(loggingConsumer, "Logging consumer must not be null");
this.loggingConsumers.add(loggingConsumer);
return this;
}
/**
* Adds multiple consumers to be notified when logging messages are received from
* the server. This allows the client to react to log messages, such as warnings
* or errors, that are sent by the server.
* @param loggingConsumers A list of consumers that receive logging messages. Must
* not be null.
* @return This builder instance for method chaining
*/
public AsyncSpec loggingConsumers(
List<Function<McpSchema.LoggingMessageNotification, Mono<Void>>> loggingConsumers) {
Assert.notNull(loggingConsumers, "Logging consumers must not be null");
this.loggingConsumers.addAll(loggingConsumers);
return this;
}
/**
* Create an instance of {@link McpAsyncClient} with the provided configurations
* or sensible defaults.
* @return a new instance of {@link McpAsyncClient}.
*/
public McpAsyncClient build() {
return new McpAsyncClient(this.transport, this.requestTimeout,
new McpClientFeatures.Async(this.clientInfo, this.capabilities, this.roots,
this.toolsChangeConsumers, this.resourcesChangeConsumers, this.promptsChangeConsumers,
this.loggingConsumers, this.samplingHandler));
}
}
static SyncSpec sync(ClientMcpTransport transport) {
return new SyncSpec(transport);
}
static AsyncSpec async(ClientMcpTransport transport) {
return new AsyncSpec(transport);
}
}

View file

@ -0,0 +1,200 @@
package org.springframework.ai.mcp.client;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
import java.util.function.Function;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
import org.springframework.ai.mcp.spec.McpSchema;
import org.springframework.ai.mcp.util.Assert;
import org.springframework.ai.mcp.util.Utils;
/**
* Internal representation of features and capabilities for Model Context Protocol (MCP)
* clients. This class provides two record types for managing client features:
* <ul>
* <li>{@link Async} for non-blocking operations with Project Reactor's Mono responses
* <li>{@link Sync} for blocking operations with direct responses
* </ul>
*
* <p>
* Each feature specification includes:
* <ul>
* <li>Client implementation information and capabilities
* <li>Root URI mappings for resource access
* <li>Change notification handlers for tools, resources, and prompts
* <li>Logging message consumers
* <li>Message sampling handlers for request processing
* </ul>
*
* <p>
* The class supports conversion between synchronous and asynchronous specifications
* through the {@link Async#fromSync} method, which ensures proper handling of blocking
* operations in non-blocking contexts by scheduling them on a bounded elastic scheduler.
*
* @see McpClient
* @see McpSchema.Implementation
* @see McpSchema.ClientCapabilities
*/
class McpClientFeatures {
/**
* Asynchronous client features specification providing the capabilities and request
* and notification handlers.
*
* @param clientInfo the client implementation information.
* @param clientCapabilities the client capabilities.
* @param roots the roots.
* @param toolsChangeConsumers the tools change consumers.
* @param resourcesChangeConsumers the resources change consumers.
* @param promptsChangeConsumers the prompts change consumers.
* @param loggingConsumers the logging consumers.
* @param samplingHandler the sampling handler.
*/
record Async(McpSchema.Implementation clientInfo, McpSchema.ClientCapabilities clientCapabilities,
Map<String, McpSchema.Root> roots, List<Function<List<McpSchema.Tool>, Mono<Void>>> toolsChangeConsumers,
List<Function<List<McpSchema.Resource>, Mono<Void>>> resourcesChangeConsumers,
List<Function<List<McpSchema.Prompt>, Mono<Void>>> promptsChangeConsumers,
List<Function<McpSchema.LoggingMessageNotification, Mono<Void>>> loggingConsumers,
Function<McpSchema.CreateMessageRequest, Mono<McpSchema.CreateMessageResult>> samplingHandler) {
/**
* Create an instance and validate the arguments.
* @param clientInfo the client implementation information.
* @param clientCapabilities the client capabilities.
* @param roots the roots.
* @param toolsChangeConsumers the tools change consumers.
* @param resourcesChangeConsumers the resources change consumers.
* @param promptsChangeConsumers the prompts change consumers.
* @param loggingConsumers the logging consumers.
* @param samplingHandler the sampling handler.
*/
public Async(McpSchema.Implementation clientInfo, McpSchema.ClientCapabilities clientCapabilities,
Map<String, McpSchema.Root> roots,
List<Function<List<McpSchema.Tool>, Mono<Void>>> toolsChangeConsumers,
List<Function<List<McpSchema.Resource>, Mono<Void>>> resourcesChangeConsumers,
List<Function<List<McpSchema.Prompt>, Mono<Void>>> promptsChangeConsumers,
List<Function<McpSchema.LoggingMessageNotification, Mono<Void>>> loggingConsumers,
Function<McpSchema.CreateMessageRequest, Mono<McpSchema.CreateMessageResult>> samplingHandler) {
Assert.notNull(clientInfo, "Client info must not be null");
this.clientInfo = clientInfo;
this.clientCapabilities = (clientCapabilities != null) ? clientCapabilities
: new McpSchema.ClientCapabilities(null,
!Utils.isEmpty(roots) ? new McpSchema.ClientCapabilities.RootCapabilities(false) : null,
samplingHandler != null ? new McpSchema.ClientCapabilities.Sampling() : null);
this.roots = roots != null ? new ConcurrentHashMap<>(roots) : new ConcurrentHashMap<>();
this.toolsChangeConsumers = toolsChangeConsumers;
this.resourcesChangeConsumers = resourcesChangeConsumers;
this.promptsChangeConsumers = promptsChangeConsumers;
this.loggingConsumers = loggingConsumers;
this.samplingHandler = samplingHandler;
}
/**
* Convert a synchronous specification into an asynchronous one and provide
* blocking code offloading to prevent accidental blocking of the non-blocking
* transport.
* @param syncSpec a potentially blocking, synchronous specification.
* @return a specification which is protected from blocking calls specified by the
* user.
*/
public static Async fromSync(Sync syncSpec) {
List<Function<List<McpSchema.Tool>, Mono<Void>>> toolsChangeConsumers = new ArrayList<>();
for (Consumer<List<McpSchema.Tool>> consumer : syncSpec.toolsChangeConsumers()) {
toolsChangeConsumers.add(t -> Mono.<Void>fromRunnable(() -> consumer.accept(t))
.subscribeOn(Schedulers.boundedElastic()));
}
List<Function<List<McpSchema.Resource>, Mono<Void>>> resourcesChangeConsumers = new ArrayList<>();
for (Consumer<List<McpSchema.Resource>> consumer : syncSpec.resourcesChangeConsumers()) {
resourcesChangeConsumers.add(r -> Mono.<Void>fromRunnable(() -> consumer.accept(r))
.subscribeOn(Schedulers.boundedElastic()));
}
List<Function<List<McpSchema.Prompt>, Mono<Void>>> promptsChangeConsumers = new ArrayList<>();
for (Consumer<List<McpSchema.Prompt>> consumer : syncSpec.promptsChangeConsumers()) {
promptsChangeConsumers.add(p -> Mono.<Void>fromRunnable(() -> consumer.accept(p))
.subscribeOn(Schedulers.boundedElastic()));
}
List<Function<McpSchema.LoggingMessageNotification, Mono<Void>>> loggingConsumers = new ArrayList<>();
for (Consumer<McpSchema.LoggingMessageNotification> consumer : syncSpec.loggingConsumers()) {
loggingConsumers.add(l -> Mono.<Void>fromRunnable(() -> consumer.accept(l))
.subscribeOn(Schedulers.boundedElastic()));
}
Function<McpSchema.CreateMessageRequest, Mono<McpSchema.CreateMessageResult>> samplingHandler = r -> Mono
.fromCallable(() -> syncSpec.samplingHandler().apply(r))
.subscribeOn(Schedulers.boundedElastic());
return new Async(syncSpec.clientInfo(), syncSpec.clientCapabilities(), syncSpec.roots(),
toolsChangeConsumers, resourcesChangeConsumers, promptsChangeConsumers, loggingConsumers,
samplingHandler);
}
}
/**
* Synchronous client features specification providing the capabilities and request
* and notification handlers.
*
* @param clientInfo the client implementation information.
* @param clientCapabilities the client capabilities.
* @param roots the roots.
* @param toolsChangeConsumers the tools change consumers.
* @param resourcesChangeConsumers the resources change consumers.
* @param promptsChangeConsumers the prompts change consumers.
* @param loggingConsumers the logging consumers.
* @param samplingHandler the sampling handler.
*/
public record Sync(McpSchema.Implementation clientInfo, McpSchema.ClientCapabilities clientCapabilities,
Map<String, McpSchema.Root> roots, List<Consumer<List<McpSchema.Tool>>> toolsChangeConsumers,
List<Consumer<List<McpSchema.Resource>>> resourcesChangeConsumers,
List<Consumer<List<McpSchema.Prompt>>> promptsChangeConsumers,
List<Consumer<McpSchema.LoggingMessageNotification>> loggingConsumers,
Function<McpSchema.CreateMessageRequest, McpSchema.CreateMessageResult> samplingHandler) {
/**
* Create an instance and validate the arguments.
* @param clientInfo the client implementation information.
* @param clientCapabilities the client capabilities.
* @param roots the roots.
* @param toolsChangeConsumers the tools change consumers.
* @param resourcesChangeConsumers the resources change consumers.
* @param promptsChangeConsumers the prompts change consumers.
* @param loggingConsumers the logging consumers.
* @param samplingHandler the sampling handler.
*/
public Sync(McpSchema.Implementation clientInfo, McpSchema.ClientCapabilities clientCapabilities,
Map<String, McpSchema.Root> roots, List<Consumer<List<McpSchema.Tool>>> toolsChangeConsumers,
List<Consumer<List<McpSchema.Resource>>> resourcesChangeConsumers,
List<Consumer<List<McpSchema.Prompt>>> promptsChangeConsumers,
List<Consumer<McpSchema.LoggingMessageNotification>> loggingConsumers,
Function<McpSchema.CreateMessageRequest, McpSchema.CreateMessageResult> samplingHandler) {
Assert.notNull(clientInfo, "Client info must not be null");
this.clientInfo = clientInfo;
this.clientCapabilities = (clientCapabilities != null) ? clientCapabilities
: new McpSchema.ClientCapabilities(null,
!Utils.isEmpty(roots) ? new McpSchema.ClientCapabilities.RootCapabilities(false) : null,
samplingHandler != null ? new McpSchema.ClientCapabilities.Sampling() : null);
this.roots = roots != null ? new HashMap<>(roots) : new HashMap<>();
this.toolsChangeConsumers = toolsChangeConsumers;
this.resourcesChangeConsumers = resourcesChangeConsumers;
this.promptsChangeConsumers = promptsChangeConsumers;
this.loggingConsumers = loggingConsumers;
this.samplingHandler = samplingHandler;
}
}
}

View file

@ -29,10 +29,39 @@ import org.springframework.ai.mcp.spec.McpSchema.ListPromptsResult;
import org.springframework.ai.mcp.util.Assert;
/**
* A synchronous client for the Model Context Protocol (MCP).
* A synchronous client implementation for the Model Context Protocol (MCP) that wraps an
* {@link McpAsyncClient} to provide blocking operations.
*
* <p>
* This client implements the MCP specification by delegating to an asynchronous client
* and blocking on the results. Key features include:
* <ul>
* <li>Synchronous, blocking API for simpler integration in non-reactive applications
* <li>Tool discovery and invocation for server-provided functionality
* <li>Resource access and management with URI-based addressing
* <li>Prompt template handling for standardized AI interactions
* <li>Real-time notifications for tools, resources, and prompts changes
* <li>Structured logging with configurable severity levels
* </ul>
*
* <p>
* The client follows the same lifecycle as its async counterpart:
* <ol>
* <li>Initialization - Establishes connection and negotiates capabilities
* <li>Normal Operation - Handles requests and notifications
* <li>Graceful Shutdown - Ensures clean connection termination
* </ol>
*
* <p>
* This implementation implements {@link AutoCloseable} for resource cleanup and provides
* both immediate and graceful shutdown options. All operations block until completion or
* timeout, making it suitable for traditional synchronous programming models.
*
* @author Dariusz Jędrzejczyk
* @author Christian Tzolov
* @see McpClient
* @see McpAsyncClient
* @see McpSchema
*/
public class McpSyncClient implements AutoCloseable {

View file

@ -24,10 +24,12 @@ import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.Consumer;
import java.util.function.Function;
import com.fasterxml.jackson.core.type.TypeReference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
@ -48,9 +50,43 @@ import org.springframework.ai.mcp.util.Utils;
/**
* The Model Context Protocol (MCP) server implementation that provides asynchronous
* communication.
* communication using Project Reactor's Mono and Flux types.
*
* <p>
* This server implements the MCP specification, enabling AI models to expose tools,
* resources, and prompts through a standardized interface. Key features include:
* <ul>
* <li>Asynchronous communication using reactive programming patterns
* <li>Dynamic tool registration and management
* <li>Resource handling with URI-based addressing
* <li>Prompt template management
* <li>Real-time client notifications for state changes
* <li>Structured logging with configurable severity levels
* <li>Support for client-side AI model sampling
* </ul>
*
* <p>
* The server follows a lifecycle:
* <ol>
* <li>Initialization - Accepts client connections and negotiates capabilities
* <li>Normal Operation - Handles client requests and sends notifications
* <li>Graceful Shutdown - Ensures clean connection termination
* </ol>
*
* <p>
* This implementation uses Project Reactor for non-blocking operations, making it
* suitable for high-throughput scenarios and reactive applications. All operations return
* Mono or Flux types that can be composed into reactive pipelines.
*
* <p>
* The server supports runtime modification of its capabilities through methods like
* {@link #addTool}, {@link #addResource}, and {@link #addPrompt}, automatically notifying
* connected clients of changes when configured to do so.
*
* @author Christian Tzolov
* @see McpServer
* @see McpSchema
* @see DefaultMcpSession
*/
public class McpAsyncServer {
@ -75,16 +111,84 @@ public class McpAsyncServer {
/**
* Thread-safe list of tool handlers that can be modified at runtime.
*/
private final CopyOnWriteArrayList<ToolRegistration> tools;
private final CopyOnWriteArrayList<McpServerFeatures.AsyncToolRegistration> tools = new CopyOnWriteArrayList<>();
private final CopyOnWriteArrayList<McpSchema.ResourceTemplate> resourceTemplates;
private final CopyOnWriteArrayList<McpSchema.ResourceTemplate> resourceTemplates = new CopyOnWriteArrayList<>();
private final ConcurrentHashMap<String, ResourceRegistration> resources;
private final ConcurrentHashMap<String, McpServerFeatures.AsyncResourceRegistration> resources = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, PromptRegistration> prompts;
private final ConcurrentHashMap<String, McpServerFeatures.AsyncPromptRegistration> prompts = new ConcurrentHashMap<>();
private LoggingLevel minLoggingLevel = LoggingLevel.DEBUG;
/**
* Create a new McpAsyncServer with the given transport and capabilities.
* @param mcpTransport The transport layer implementation for MCP communication.
* @param features The MCP server supported features.
*/
McpAsyncServer(ServerMcpTransport mcpTransport, McpServerFeatures.Async features) {
this.serverInfo = features.serverInfo();
this.serverCapabilities = features.serverCapabilities();
this.tools.addAll(features.tools());
this.resources.putAll(features.resources());
this.resourceTemplates.addAll(features.resourceTemplates());
this.prompts.putAll(features.prompts());
Map<String, DefaultMcpSession.RequestHandler<?>> requestHandlers = new HashMap<>();
// Initialize request handlers for standard MCP methods
requestHandlers.put(McpSchema.METHOD_INITIALIZE, initializeRequestHandler());
// Ping MUST respond with an empty data, but not NULL response.
requestHandlers.put(McpSchema.METHOD_PING, (params) -> Mono.just(""));
// Add tools API handlers if the tool capability is enabled
if (this.serverCapabilities.tools() != null) {
requestHandlers.put(McpSchema.METHOD_TOOLS_LIST, toolsListRequestHandler());
requestHandlers.put(McpSchema.METHOD_TOOLS_CALL, toolsCallRequestHandler());
}
// Add resources API handlers if provided
if (!Utils.isEmpty(this.resources)) {
requestHandlers.put(McpSchema.METHOD_RESOURCES_LIST, resourcesListRequestHandler());
requestHandlers.put(McpSchema.METHOD_RESOURCES_READ, resourcesReadRequestHandler());
}
// Add resource templates API handlers if provided.
if (!Utils.isEmpty(this.resourceTemplates)) {
requestHandlers.put(McpSchema.METHOD_RESOURCES_TEMPLATES_LIST, resourceTemplateListRequestHandler());
}
// Add prompts API handlers if provider exists
if (!Utils.isEmpty(this.prompts)) {
requestHandlers.put(McpSchema.METHOD_PROMPT_LIST, promptsListRequestHandler());
requestHandlers.put(McpSchema.METHOD_PROMPT_GET, promptsGetRequestHandler());
}
// Add logging API handlers if the logging capability is enabled
if (this.serverCapabilities.logging() != null) {
requestHandlers.put(McpSchema.METHOD_LOGGING_SET_LEVEL, setLoggerRequestHandler());
}
Map<String, NotificationHandler> notificationHandlers = new HashMap<>();
notificationHandlers.put(McpSchema.METHOD_NOTIFICATION_INITIALIZED, (params) -> Mono.empty());
List<Function<List<McpSchema.Root>, Mono<Void>>> rootsChangeConsumers = features.rootsChangeConsumers();
if (Utils.isEmpty(rootsChangeConsumers)) {
rootsChangeConsumers = List.of((roots) -> Mono.fromRunnable(() -> logger
.warn("Roots list changed notification, but no consumers provided. Roots list changed: {}", roots)));
}
notificationHandlers.put(McpSchema.METHOD_NOTIFICATION_ROOTS_LIST_CHANGED,
asyncRootsListChangedNotificationHandler(rootsChangeConsumers));
this.transport = mcpTransport;
this.mcpSession = new DefaultMcpSession(Duration.ofSeconds(10), mcpTransport, requestHandlers,
notificationHandlers);
}
/**
* Create a new McpAsyncServer with the given transport and capabilities.
* @param mcpTransport The transport layer implementation for MCP communication
@ -96,18 +200,28 @@ public class McpAsyncServer {
* @param prompts The map of prompt registrations
* @param rootsChangeConsumers The list of consumers that will be notified when the
* roots list changes
* @deprecated Use {@link McpServer#sync(ServerMcpTransport)} or
* {@link McpServer#async(ServerMcpTransport)} to create a new server instance.
*/
@Deprecated
public McpAsyncServer(ServerMcpTransport mcpTransport, McpSchema.Implementation serverInfo,
McpSchema.ServerCapabilities serverCapabilities, List<ToolRegistration> tools,
Map<String, ResourceRegistration> resources, List<McpSchema.ResourceTemplate> resourceTemplates,
Map<String, PromptRegistration> prompts, List<Consumer<List<McpSchema.Root>>> rootsChangeConsumers) {
this.serverInfo = serverInfo;
this.tools = new CopyOnWriteArrayList<>(tools != null ? tools : List.of());
this.resources = !Utils.isEmpty(resources) ? new ConcurrentHashMap<>(resources) : new ConcurrentHashMap<>();
this.resourceTemplates = !Utils.isEmpty(resourceTemplates) ? new CopyOnWriteArrayList<>(resourceTemplates)
: new CopyOnWriteArrayList<>();
this.prompts = !Utils.isEmpty(prompts) ? new ConcurrentHashMap<>(prompts) : new ConcurrentHashMap<>();
if (!Utils.isEmpty(tools)) {
this.tools.addAll(McpServer.mapDeprecatedTools(tools));
}
if (!Utils.isEmpty(resources)) {
this.resources.putAll(McpServer.mapDeprecatedResources(resources));
}
if (!Utils.isEmpty(resourceTemplates)) {
this.resourceTemplates.addAll(resourceTemplates);
}
if (!Utils.isEmpty(prompts)) {
this.prompts.putAll(McpServer.mapDeprecatedPrompts(prompts));
}
this.serverCapabilities = (serverCapabilities != null) ? serverCapabilities : new McpSchema.ServerCapabilities(
null, // experimental
@ -118,13 +232,13 @@ public class McpAsyncServer {
: null,
!Utils.isEmpty(this.tools) ? new McpSchema.ServerCapabilities.ToolCapabilities(false) : null);
Map<String, DefaultMcpSession.RequestHandler> requestHandlers = new HashMap<>();
Map<String, DefaultMcpSession.RequestHandler<?>> requestHandlers = new HashMap<>();
// Initialize request handlers for standard MCP methods
requestHandlers.put(McpSchema.METHOD_INITIALIZE, initializeRequestHandler());
// Ping MUST respond with an empty data, but not NULL response.
requestHandlers.put(McpSchema.METHOD_PING, (params) -> Mono.<Object>just(""));
requestHandlers.put(McpSchema.METHOD_PING, (params) -> Mono.just(""));
// Add tools API handlers if the tool capability is enabled
if (this.serverCapabilities.tools() != null) {
@ -173,7 +287,7 @@ public class McpAsyncServer {
// ---------------------------------------
// Lifecycle Management
// ---------------------------------------
private DefaultMcpSession.RequestHandler initializeRequestHandler() {
private DefaultMcpSession.RequestHandler<McpSchema.InitializeResult> initializeRequestHandler() {
return params -> {
McpSchema.InitializeRequest initializeRequest = transport.unmarshalFrom(params,
new TypeReference<McpSchema.InitializeRequest>() {
@ -187,14 +301,13 @@ public class McpAsyncServer {
initializeRequest.clientInfo());
if (!McpSchema.LATEST_PROTOCOL_VERSION.equals(initializeRequest.protocolVersion())) {
return Mono
.<Object>error(new McpError(
"Unsupported protocol version from client: " + initializeRequest.protocolVersion()))
return Mono.<McpSchema.InitializeResult>error(new McpError(
"Unsupported protocol version from client: " + initializeRequest.protocolVersion()))
.publishOn(Schedulers.boundedElastic());
}
return Mono
.<Object>just(new McpSchema.InitializeResult(McpSchema.LATEST_PROTOCOL_VERSION, this.serverCapabilities,
.just(new McpSchema.InitializeResult(McpSchema.LATEST_PROTOCOL_VERSION, this.serverCapabilities,
this.serverInfo, null))
.publishOn(Schedulers.boundedElastic());
};
@ -268,6 +381,7 @@ public class McpAsyncServer {
LIST_ROOTS_RESULT_TYPE_REF);
}
@Deprecated
private NotificationHandler rootsListChnagedNotificationHandler(
List<Consumer<List<McpSchema.Root>>> rootsChangeConsumers) {
@ -281,6 +395,13 @@ public class McpAsyncServer {
};
}
private NotificationHandler asyncRootsListChangedNotificationHandler(
List<Function<List<McpSchema.Root>, Mono<Void>>> rootsChangeConsumers) {
return params -> listRoots().flatMap(listRootsResult -> Flux.fromIterable(rootsChangeConsumers)
.flatMap(consumer -> consumer.apply(listRootsResult.roots()))
.then());
}
// ---------------------------------------
// Tool Management
// ---------------------------------------
@ -290,6 +411,44 @@ public class McpAsyncServer {
* @param toolRegistration The tool registration to add
* @return Mono that completes when clients have been notified of the change
*/
public Mono<Void> addTool(McpServerFeatures.AsyncToolRegistration toolRegistration) {
if (toolRegistration == null) {
return Mono.error(new McpError("Tool registration must not be null"));
}
if (toolRegistration.tool() == null) {
return Mono.error(new McpError("Tool must not be null"));
}
if (toolRegistration.call() == null) {
return Mono.error(new McpError("Tool call handler must not be null"));
}
if (this.serverCapabilities.tools() == null) {
return Mono.error(new McpError("Server must be configured with tool capabilities"));
}
return Mono.defer(() -> {
// Check for duplicate tool names
if (this.tools.stream().anyMatch(th -> th.tool().name().equals(toolRegistration.tool().name()))) {
return Mono
.error(new McpError("Tool with name '" + toolRegistration.tool().name() + "' already exists"));
}
this.tools.add(toolRegistration);
logger.info("Added tool handler: {}", toolRegistration.tool().name());
if (this.serverCapabilities.tools().listChanged()) {
return notifyToolsListChanged();
}
return Mono.empty();
});
}
/**
* Add a new tool registration at runtime.
* @param toolRegistration The tool registration to add
* @return Mono that completes when clients have been notified of the change
* @deprecated Use {@link #addTool(McpServerFeatures.AsyncToolRegistration)}.
*/
@Deprecated
public Mono<Void> addTool(ToolRegistration toolRegistration) {
if (toolRegistration == null) {
return Mono.error(new McpError("Tool registration must not be null"));
@ -309,7 +468,7 @@ public class McpAsyncServer {
return Mono.error(new McpError("Tool with name '" + toolRegistration.tool().name() + "' already exists"));
}
this.tools.add(toolRegistration);
this.tools.add(McpServer.mapDeprecatedTool(toolRegistration));
logger.info("Added tool handler: {}", toolRegistration.tool().name());
if (this.serverCapabilities.tools().listChanged()) {
return notifyToolsListChanged();
@ -330,15 +489,17 @@ public class McpAsyncServer {
return Mono.error(new McpError("Server must be configured with tool capabilities"));
}
boolean removed = this.tools.removeIf(toolRegistration -> toolRegistration.tool().name().equals(toolName));
if (removed) {
logger.info("Removed tool handler: {}", toolName);
if (this.serverCapabilities.tools().listChanged()) {
return notifyToolsListChanged();
return Mono.defer(() -> {
boolean removed = this.tools.removeIf(toolRegistration -> toolRegistration.tool().name().equals(toolName));
if (removed) {
logger.info("Removed tool handler: {}", toolName);
if (this.serverCapabilities.tools().listChanged()) {
return notifyToolsListChanged();
}
return Mono.empty();
}
return Mono.empty();
}
return Mono.error(new McpError("Tool with name '" + toolName + "' not found"));
return Mono.error(new McpError("Tool with name '" + toolName + "' not found"));
});
}
/**
@ -349,32 +510,30 @@ public class McpAsyncServer {
return this.mcpSession.sendNotification(McpSchema.METHOD_NOTIFICATION_TOOLS_LIST_CHANGED, null);
}
private DefaultMcpSession.RequestHandler toolsListRequestHandler() {
private DefaultMcpSession.RequestHandler<McpSchema.ListToolsResult> toolsListRequestHandler() {
return params -> {
List<Tool> tools = this.tools.stream().map(ToolRegistration::tool).toList();
List<Tool> tools = this.tools.stream().map(McpServerFeatures.AsyncToolRegistration::tool).toList();
return Mono.just(new McpSchema.ListToolsResult(tools, null));
};
}
private DefaultMcpSession.RequestHandler toolsCallRequestHandler() {
private DefaultMcpSession.RequestHandler<CallToolResult> toolsCallRequestHandler() {
return params -> {
McpSchema.CallToolRequest callToolRequest = transport.unmarshalFrom(params,
new TypeReference<McpSchema.CallToolRequest>() {
});
Optional<ToolRegistration> toolRegistration = this.tools.stream()
Optional<McpServerFeatures.AsyncToolRegistration> toolRegistration = this.tools.stream()
.filter(tr -> callToolRequest.name().equals(tr.tool().name()))
.findAny();
if (toolRegistration.isEmpty()) {
return Mono.<Object>error(new McpError("Tool not found: " + callToolRequest.name()));
return Mono.error(new McpError("Tool not found: " + callToolRequest.name()));
}
return Mono.fromCallable(() -> toolRegistration.get().call().apply(callToolRequest.arguments()))
.map(result -> (Object) result)
.subscribeOn(Schedulers.boundedElastic());
return toolRegistration.map(tool -> tool.call().apply(callToolRequest.arguments()))
.orElse(Mono.error(new McpError("Tool not found: " + callToolRequest.name())));
};
}
@ -387,6 +546,35 @@ public class McpAsyncServer {
* @param resourceHandler The resource handler to add
* @return Mono that completes when clients have been notified of the change
*/
public Mono<Void> addResource(McpServerFeatures.AsyncResourceRegistration resourceHandler) {
if (resourceHandler == null || resourceHandler.resource() == null) {
return Mono.error(new McpError("Resource must not be null"));
}
if (this.serverCapabilities.resources() == null) {
return Mono.error(new McpError("Server must be configured with resource capabilities"));
}
return Mono.defer(() -> {
if (this.resources.putIfAbsent(resourceHandler.resource().uri(), resourceHandler) != null) {
return Mono
.error(new McpError("Resource with URI '" + resourceHandler.resource().uri() + "' already exists"));
}
logger.info("Added resource handler: {}", resourceHandler.resource().uri());
if (this.serverCapabilities.resources().listChanged()) {
return notifyResourcesListChanged();
}
return Mono.empty();
});
}
/**
* Add a new resource handler at runtime.
* @param resourceHandler The resource handler to add
* @return Mono that completes when clients have been notified of the change
* @deprecated Use {@link #addResource(McpServerFeatures.AsyncResourceRegistration)}.
*/
@Deprecated
public Mono<Void> addResource(ResourceRegistration resourceHandler) {
if (resourceHandler == null || resourceHandler.resource() == null) {
return Mono.error(new McpError("Resource must not be null"));
@ -401,7 +589,7 @@ public class McpAsyncServer {
.error(new McpError("Resource with URI '" + resourceHandler.resource().uri() + "' already exists"));
}
this.resources.put(resourceHandler.resource().uri(), resourceHandler);
this.resources.put(resourceHandler.resource().uri(), McpServer.mapDeprecatedResource(resourceHandler));
logger.info("Added resource handler: {}", resourceHandler.resource().uri());
if (this.serverCapabilities.resources().listChanged()) {
return notifyResourcesListChanged();
@ -422,15 +610,17 @@ public class McpAsyncServer {
return Mono.error(new McpError("Server must be configured with resource capabilities"));
}
ResourceRegistration removed = this.resources.remove(resourceUri);
if (removed != null) {
logger.info("Removed resource handler: {}", resourceUri);
if (this.serverCapabilities.resources().listChanged()) {
return notifyResourcesListChanged();
return Mono.defer(() -> {
McpServerFeatures.AsyncResourceRegistration removed = this.resources.remove(resourceUri);
if (removed != null) {
logger.info("Removed resource handler: {}", resourceUri);
if (this.serverCapabilities.resources().listChanged()) {
return notifyResourcesListChanged();
}
return Mono.empty();
}
return Mono.empty();
}
return Mono.error(new McpError("Resource with URI '" + resourceUri + "' not found"));
return Mono.error(new McpError("Resource with URI '" + resourceUri + "' not found"));
});
}
/**
@ -441,28 +631,30 @@ public class McpAsyncServer {
return this.mcpSession.sendNotification(McpSchema.METHOD_NOTIFICATION_RESOURCES_LIST_CHANGED, null);
}
private DefaultMcpSession.RequestHandler resourcesListRequestHandler() {
private DefaultMcpSession.RequestHandler<McpSchema.ListResourcesResult> resourcesListRequestHandler() {
return params -> {
var resourceList = this.resources.values().stream().map(ResourceRegistration::resource).toList();
var resourceList = this.resources.values()
.stream()
.map(McpServerFeatures.AsyncResourceRegistration::resource)
.toList();
return Mono.just(new McpSchema.ListResourcesResult(resourceList, null));
};
}
private DefaultMcpSession.RequestHandler resourceTemplateListRequestHandler() {
private DefaultMcpSession.RequestHandler<McpSchema.ListResourceTemplatesResult> resourceTemplateListRequestHandler() {
return params -> Mono.just(new McpSchema.ListResourceTemplatesResult(this.resourceTemplates, null));
}
private DefaultMcpSession.RequestHandler resourcesReadRequestHandler() {
private DefaultMcpSession.RequestHandler<McpSchema.ReadResourceResult> resourcesReadRequestHandler() {
return params -> {
McpSchema.ReadResourceRequest resourceRequest = transport.unmarshalFrom(params,
new TypeReference<McpSchema.ReadResourceRequest>() {
});
var resourceUri = resourceRequest.uri();
if (this.resources.containsKey(resourceUri)) {
return Mono.fromCallable(() -> this.resources.get(resourceUri).readHandler().apply(resourceRequest))
.map(result -> (Object) result)
.subscribeOn(Schedulers.boundedElastic());
McpServerFeatures.AsyncResourceRegistration registration = this.resources.get(resourceUri);
if (registration != null) {
return this.resources.get(resourceUri).readHandler().apply(resourceRequest);
}
return Mono.error(new McpError("Resource not found: " + resourceUri));
};
@ -477,6 +669,41 @@ public class McpAsyncServer {
* @param promptRegistration The prompt handler to add
* @return Mono that completes when clients have been notified of the change
*/
public Mono<Void> addPrompt(McpServerFeatures.AsyncPromptRegistration promptRegistration) {
if (promptRegistration == null) {
return Mono.error(new McpError("Prompt registration must not be null"));
}
if (this.serverCapabilities.prompts() == null) {
return Mono.error(new McpError("Server must be configured with prompt capabilities"));
}
return Mono.defer(() -> {
McpServerFeatures.AsyncPromptRegistration registration = this.prompts
.putIfAbsent(promptRegistration.prompt().name(), promptRegistration);
if (registration != null) {
return Mono.error(
new McpError("Prompt with name '" + promptRegistration.prompt().name() + "' already exists"));
}
logger.info("Added prompt handler: {}", promptRegistration.prompt().name());
// Servers that declared the listChanged capability SHOULD send a
// notification,
// when the list of available prompts changes
if (this.serverCapabilities.prompts().listChanged()) {
return notifyPromptsListChanged();
}
return Mono.empty();
});
}
/**
* Add a new prompt handler at runtime.
* @param promptRegistration The prompt handler to add
* @return Mono that completes when clients have been notified of the change
* @deprecated Use {@link #addPrompt(McpServerFeatures.AsyncPromptRegistration)}.
*/
@Deprecated
public Mono<Void> addPrompt(PromptRegistration promptRegistration) {
if (promptRegistration == null) {
return Mono.error(new McpError("Prompt registration must not be null"));
@ -490,7 +717,7 @@ public class McpAsyncServer {
.error(new McpError("Prompt with name '" + promptRegistration.prompt().name() + "' already exists"));
}
this.prompts.put(promptRegistration.prompt().name(), promptRegistration);
this.prompts.put(promptRegistration.prompt().name(), McpServer.mapDeprecatedPrompt(promptRegistration));
logger.info("Added prompt handler: {}", promptRegistration.prompt().name());
@ -515,18 +742,20 @@ public class McpAsyncServer {
return Mono.error(new McpError("Server must be configured with prompt capabilities"));
}
PromptRegistration removed = this.prompts.remove(promptName);
return Mono.defer(() -> {
McpServerFeatures.AsyncPromptRegistration removed = this.prompts.remove(promptName);
if (removed != null) {
logger.info("Removed prompt handler: {}", promptName);
// Servers that declared the listChanged capability SHOULD send a
// notification, when the list of available prompts changes
if (this.serverCapabilities.prompts().listChanged()) {
return this.notifyPromptsListChanged();
if (removed != null) {
logger.info("Removed prompt handler: {}", promptName);
// Servers that declared the listChanged capability SHOULD send a
// notification, when the list of available prompts changes
if (this.serverCapabilities.prompts().listChanged()) {
return this.notifyPromptsListChanged();
}
return Mono.empty();
}
return Mono.empty();
}
return Mono.error(new McpError("Prompt with name '" + promptName + "' not found"));
return Mono.error(new McpError("Prompt with name '" + promptName + "' not found"));
});
}
/**
@ -537,34 +766,35 @@ public class McpAsyncServer {
return this.mcpSession.sendNotification(McpSchema.METHOD_NOTIFICATION_PROMPTS_LIST_CHANGED, null);
}
private DefaultMcpSession.RequestHandler promptsListRequestHandler() {
private DefaultMcpSession.RequestHandler<McpSchema.ListPromptsResult> promptsListRequestHandler() {
return params -> {
// TODO: Implement pagination
// McpSchema.PaginatedRequest request = transport.unmarshalFrom(params,
// new TypeReference<McpSchema.PaginatedRequest>() {
// });
var promptList = this.prompts.values().stream().map(PromptRegistration::prompt).toList();
var promptList = this.prompts.values()
.stream()
.map(McpServerFeatures.AsyncPromptRegistration::prompt)
.toList();
return Mono.just(new McpSchema.ListPromptsResult(promptList, null));
};
}
private DefaultMcpSession.RequestHandler promptsGetRequestHandler() {
private DefaultMcpSession.RequestHandler<McpSchema.GetPromptResult> promptsGetRequestHandler() {
return params -> {
McpSchema.GetPromptRequest promptRequest = transport.unmarshalFrom(params,
new TypeReference<McpSchema.GetPromptRequest>() {
});
// Implement prompt retrieval logic here
if (this.prompts.containsKey(promptRequest.name())) {
return Mono
.fromCallable(() -> this.prompts.get(promptRequest.name()).promptHandler().apply(promptRequest))
.map(result -> (Object) result)
.subscribeOn(Schedulers.boundedElastic());
McpServerFeatures.AsyncPromptRegistration registration = this.prompts.get(promptRequest.name());
if (registration == null) {
return Mono.error(new McpError("Prompt not found: " + promptRequest.name()));
}
return Mono.error(new McpError("Prompt not found: " + promptRequest.name()));
return registration.promptHandler().apply(promptRequest);
};
}
@ -600,13 +830,10 @@ public class McpAsyncServer {
* not be sent.
* @return A handler that processes logging level change requests
*/
private DefaultMcpSession.RequestHandler setLoggerRequestHandler() {
private DefaultMcpSession.RequestHandler<Void> setLoggerRequestHandler() {
return params -> {
McpSchema.LoggingLevel setLoggerRequest = transport.unmarshalFrom(params,
new TypeReference<McpSchema.LoggingLevel>() {
});
this.minLoggingLevel = setLoggerRequest;
this.minLoggingLevel = transport.unmarshalFrom(params, new TypeReference<LoggingLevel>() {
});
return Mono.empty();
};
@ -615,7 +842,7 @@ public class McpAsyncServer {
// ---------------------------------------
// Sampling
// ---------------------------------------
private static TypeReference<McpSchema.CreateMessageResult> CREATE_MESSAGE_RESULT_TYPE_REF = new TypeReference<>() {
private static final TypeReference<McpSchema.CreateMessageResult> CREATE_MESSAGE_RESULT_TYPE_REF = new TypeReference<>() {
};
/**

View file

@ -0,0 +1,399 @@
package org.springframework.ai.mcp.server;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Function;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
import org.springframework.ai.mcp.spec.McpSchema;
import org.springframework.ai.mcp.util.Assert;
import org.springframework.ai.mcp.util.Utils;
/**
* MCP server features specification that a particular server can choose to support.
*/
public class McpServerFeatures {
/**
* Asynchronous server features specification.
*
* @param serverInfo The server implementation details
* @param serverCapabilities The server capabilities
* @param tools The list of tool registrations
* @param resources The map of resource registrations
* @param resourceTemplates The list of resource templates
* @param prompts The map of prompt registrations
* @param rootsChangeConsumers The list of consumers that will be notified when the
* roots list changes
*/
record Async(McpSchema.Implementation serverInfo, McpSchema.ServerCapabilities serverCapabilities,
List<McpServerFeatures.AsyncToolRegistration> tools, Map<String, AsyncResourceRegistration> resources,
List<McpSchema.ResourceTemplate> resourceTemplates,
Map<String, McpServerFeatures.AsyncPromptRegistration> prompts,
List<Function<List<McpSchema.Root>, Mono<Void>>> rootsChangeConsumers) {
/**
* Create an instance and validate the arguments.
* @param serverInfo The server implementation details
* @param serverCapabilities The server capabilities
* @param tools The list of tool registrations
* @param resources The map of resource registrations
* @param resourceTemplates The list of resource templates
* @param prompts The map of prompt registrations
* @param rootsChangeConsumers The list of consumers that will be notified when
* the roots list changes
*/
Async(McpSchema.Implementation serverInfo, McpSchema.ServerCapabilities serverCapabilities,
List<McpServerFeatures.AsyncToolRegistration> tools, Map<String, AsyncResourceRegistration> resources,
List<McpSchema.ResourceTemplate> resourceTemplates,
Map<String, McpServerFeatures.AsyncPromptRegistration> prompts,
List<Function<List<McpSchema.Root>, Mono<Void>>> rootsChangeConsumers) {
Assert.notNull(serverInfo, "Server info must not be null");
this.serverInfo = serverInfo;
this.serverCapabilities = (serverCapabilities != null) ? serverCapabilities
: new McpSchema.ServerCapabilities(null, // experimental
new McpSchema.ServerCapabilities.LoggingCapabilities(), // Enable
// logging
// by
// default
!Utils.isEmpty(prompts) ? new McpSchema.ServerCapabilities.PromptCapabilities(false) : null,
!Utils.isEmpty(resources)
? new McpSchema.ServerCapabilities.ResourceCapabilities(false, false) : null,
!Utils.isEmpty(tools) ? new McpSchema.ServerCapabilities.ToolCapabilities(false) : null);
this.tools = (tools != null) ? tools : List.of();
this.resources = (resources != null) ? resources : Map.of();
this.resourceTemplates = (resourceTemplates != null) ? resourceTemplates : List.of();
this.prompts = (prompts != null) ? prompts : Map.of();
this.rootsChangeConsumers = (rootsChangeConsumers != null) ? rootsChangeConsumers : List.of();
}
/**
* Convert a synchronous specification into an asynchronous one and provide
* blocking code offloading to prevent accidental blocking of the non-blocking
* transport.
* @param syncSpec a potentially blocking, synchronous specification.
* @return a specification which is protected from blocking calls specified by the
* user.
*/
static Async fromSync(Sync syncSpec) {
List<McpServerFeatures.AsyncToolRegistration> tools = new ArrayList<>();
for (var tool : syncSpec.tools()) {
tools.add(AsyncToolRegistration.fromSync(tool));
}
Map<String, AsyncResourceRegistration> resources = new HashMap<>();
syncSpec.resources().forEach((key, resource) -> {
resources.put(key, AsyncResourceRegistration.fromSync(resource));
});
Map<String, AsyncPromptRegistration> prompts = new HashMap<>();
syncSpec.prompts().forEach((key, prompt) -> {
prompts.put(key, AsyncPromptRegistration.fromSync(prompt));
});
List<Function<List<McpSchema.Root>, Mono<Void>>> rootChangeConsumers = new ArrayList<>();
for (var rootChangeConsumer : syncSpec.rootsChangeConsumers()) {
rootChangeConsumers.add(list -> Mono.<Void>fromRunnable(() -> rootChangeConsumer.accept(list))
.subscribeOn(Schedulers.boundedElastic()));
}
return new Async(syncSpec.serverInfo(), syncSpec.serverCapabilities(), tools, resources,
syncSpec.resourceTemplates(), prompts, rootChangeConsumers);
}
}
/**
* Synchronous server features specification.
*
* @param serverInfo The server implementation details
* @param serverCapabilities The server capabilities
* @param tools The list of tool registrations
* @param resources The map of resource registrations
* @param resourceTemplates The list of resource templates
* @param prompts The map of prompt registrations
* @param rootsChangeConsumers The list of consumers that will be notified when the
* roots list changes
*/
record Sync(McpSchema.Implementation serverInfo, McpSchema.ServerCapabilities serverCapabilities,
List<McpServerFeatures.SyncToolRegistration> tools,
Map<String, McpServerFeatures.SyncResourceRegistration> resources,
List<McpSchema.ResourceTemplate> resourceTemplates,
Map<String, McpServerFeatures.SyncPromptRegistration> prompts,
List<Consumer<List<McpSchema.Root>>> rootsChangeConsumers) {
/**
* Create an instance and validate the arguments.
* @param serverInfo The server implementation details
* @param serverCapabilities The server capabilities
* @param tools The list of tool registrations
* @param resources The map of resource registrations
* @param resourceTemplates The list of resource templates
* @param prompts The map of prompt registrations
* @param rootsChangeConsumers The list of consumers that will be notified when
* the roots list changes
*/
Sync(McpSchema.Implementation serverInfo, McpSchema.ServerCapabilities serverCapabilities,
List<McpServerFeatures.SyncToolRegistration> tools,
Map<String, McpServerFeatures.SyncResourceRegistration> resources,
List<McpSchema.ResourceTemplate> resourceTemplates,
Map<String, McpServerFeatures.SyncPromptRegistration> prompts,
List<Consumer<List<McpSchema.Root>>> rootsChangeConsumers) {
Assert.notNull(serverInfo, "Server info must not be null");
this.serverInfo = serverInfo;
this.serverCapabilities = (serverCapabilities != null) ? serverCapabilities
: new McpSchema.ServerCapabilities(null, // experimental
new McpSchema.ServerCapabilities.LoggingCapabilities(), // Enable
// logging
// by
// default
!Utils.isEmpty(prompts) ? new McpSchema.ServerCapabilities.PromptCapabilities(false) : null,
!Utils.isEmpty(resources)
? new McpSchema.ServerCapabilities.ResourceCapabilities(false, false) : null,
!Utils.isEmpty(tools) ? new McpSchema.ServerCapabilities.ToolCapabilities(false) : null);
this.tools = (tools != null) ? tools : new ArrayList<>();
this.resources = (resources != null) ? resources : new HashMap<>();
this.resourceTemplates = (resourceTemplates != null) ? resourceTemplates : new ArrayList<>();
this.prompts = (prompts != null) ? prompts : new HashMap<>();
this.rootsChangeConsumers = (rootsChangeConsumers != null) ? rootsChangeConsumers : new ArrayList<>();
}
}
/**
* Registration of a tool with its asynchronous handler function. Tools are the
* primary way for MCP servers to expose functionality to AI models. Each tool
* represents a specific capability, such as:
* <ul>
* <li>Performing calculations
* <li>Accessing external APIs
* <li>Querying databases
* <li>Manipulating files
* <li>Executing system commands
* </ul>
*
* <p>
* Example tool registration: <pre>{@code
* new McpServerFeatures.AsyncToolRegistration(
* new Tool(
* "calculator",
* "Performs mathematical calculations",
* new JsonSchemaObject()
* .required("expression")
* .property("expression", JsonSchemaType.STRING)
* ),
* args -> {
* String expr = (String) args.get("expression");
* return Mono.just(new CallToolResult("Result: " + evaluate(expr)));
* }
* )
* }</pre>
*
* @param tool The tool definition including name, description, and parameter schema
* @param call The function that implements the tool's logic, receiving arguments and
* returning results
*/
public record AsyncToolRegistration(McpSchema.Tool tool,
Function<Map<String, Object>, Mono<McpSchema.CallToolResult>> call) {
static AsyncToolRegistration fromSync(SyncToolRegistration tool) {
// FIXME: This is temporary, proper validation should be implemented
if (tool == null) {
return null;
}
return new AsyncToolRegistration(tool.tool(),
map -> Mono.fromCallable(() -> tool.call().apply(map)).subscribeOn(Schedulers.boundedElastic()));
}
}
/**
* Registration of a resource with its asynchronous handler function. Resources
* provide context to AI models by exposing data such as:
* <ul>
* <li>File contents
* <li>Database records
* <li>API responses
* <li>System information
* <li>Application state
* </ul>
*
* <p>
* Example resource registration: <pre>{@code
* new McpServerFeatures.AsyncResourceRegistration(
* new Resource("docs", "Documentation files", "text/markdown"),
* request -> {
* String content = readFile(request.getPath());
* return Mono.just(new ReadResourceResult(content));
* }
* )
* }</pre>
*
* @param resource The resource definition including name, description, and MIME type
* @param readHandler The function that handles resource read requests
*/
public record AsyncResourceRegistration(McpSchema.Resource resource,
Function<McpSchema.ReadResourceRequest, Mono<McpSchema.ReadResourceResult>> readHandler) {
static AsyncResourceRegistration fromSync(SyncResourceRegistration resource) {
// FIXME: This is temporary, proper validation should be implemented
if (resource == null) {
return null;
}
return new AsyncResourceRegistration(resource.resource(),
req -> Mono.fromCallable(() -> resource.readHandler().apply(req))
.subscribeOn(Schedulers.boundedElastic()));
}
}
/**
* Registration of a prompt template with its asynchronous handler function. Prompts
* provide structured templates for AI model interactions, supporting:
* <ul>
* <li>Consistent message formatting
* <li>Parameter substitution
* <li>Context injection
* <li>Response formatting
* <li>Instruction templating
* </ul>
*
* <p>
* Example prompt registration: <pre>{@code
* new McpServerFeatures.AsyncPromptRegistration(
* new Prompt("analyze", "Code analysis template"),
* request -> {
* String code = request.getArguments().get("code");
* return Mono.just(new GetPromptResult(
* "Analyze this code:\n\n" + code + "\n\nProvide feedback on:"
* ));
* }
* )
* }</pre>
*
* @param prompt The prompt definition including name and description
* @param promptHandler The function that processes prompt requests and returns
* formatted templates
*/
public record AsyncPromptRegistration(McpSchema.Prompt prompt,
Function<McpSchema.GetPromptRequest, Mono<McpSchema.GetPromptResult>> promptHandler) {
static AsyncPromptRegistration fromSync(SyncPromptRegistration prompt) {
// FIXME: This is temporary, proper validation should be implemented
if (prompt == null) {
return null;
}
return new AsyncPromptRegistration(prompt.prompt(),
req -> Mono.fromCallable(() -> prompt.promptHandler().apply(req))
.subscribeOn(Schedulers.boundedElastic()));
}
}
/**
* Registration of a tool with its synchronous handler function. Tools are the primary
* way for MCP servers to expose functionality to AI models. Each tool represents a
* specific capability, such as:
* <ul>
* <li>Performing calculations
* <li>Accessing external APIs
* <li>Querying databases
* <li>Manipulating files
* <li>Executing system commands
* </ul>
*
* <p>
* Example tool registration: <pre>{@code
* new McpServerFeatures.SyncToolRegistration(
* new Tool(
* "calculator",
* "Performs mathematical calculations",
* new JsonSchemaObject()
* .required("expression")
* .property("expression", JsonSchemaType.STRING)
* ),
* args -> {
* String expr = (String) args.get("expression");
* return new CallToolResult("Result: " + evaluate(expr));
* }
* )
* }</pre>
*
* @param tool The tool definition including name, description, and parameter schema
* @param call The function that implements the tool's logic, receiving arguments and
* returning results
*/
public record SyncToolRegistration(McpSchema.Tool tool,
Function<Map<String, Object>, McpSchema.CallToolResult> call) {
}
/**
* Registration of a resource with its synchronous handler function. Resources provide
* context to AI models by exposing data such as:
* <ul>
* <li>File contents
* <li>Database records
* <li>API responses
* <li>System information
* <li>Application state
* </ul>
*
* <p>
* Example resource registration: <pre>{@code
* new McpServerFeatures.SyncResourceRegistration(
* new Resource("docs", "Documentation files", "text/markdown"),
* request -> {
* String content = readFile(request.getPath());
* return new ReadResourceResult(content);
* }
* )
* }</pre>
*
* @param resource The resource definition including name, description, and MIME type
* @param readHandler The function that handles resource read requests
*/
public record SyncResourceRegistration(McpSchema.Resource resource,
Function<McpSchema.ReadResourceRequest, McpSchema.ReadResourceResult> readHandler) {
}
/**
* Registration of a prompt template with its synchronous handler function. Prompts
* provide structured templates for AI model interactions, supporting:
* <ul>
* <li>Consistent message formatting
* <li>Parameter substitution
* <li>Context injection
* <li>Response formatting
* <li>Instruction templating
* </ul>
*
* <p>
* Example prompt registration: <pre>{@code
* new McpServerFeatures.SyncPromptRegistration(
* new Prompt("analyze", "Code analysis template"),
* request -> {
* String code = request.getArguments().get("code");
* return new GetPromptResult(
* "Analyze this code:\n\n" + code + "\n\nProvide feedback on:"
* );
* }
* )
* }</pre>
*
* @param prompt The prompt definition including name and description
* @param promptHandler The function that processes prompt requests and returns
* formatted templates
*/
public record SyncPromptRegistration(McpSchema.Prompt prompt,
Function<McpSchema.GetPromptRequest, McpSchema.GetPromptResult> promptHandler) {
}
}

View file

@ -26,11 +26,42 @@ import org.springframework.ai.mcp.spec.McpSchema.LoggingMessageNotification;
import org.springframework.ai.mcp.util.Assert;
/**
* Synchronous wrapper around {@link McpAsyncServer} that provides blocking operations.
* This class delegates all operations to an underlying async server instance while
* providing a synchronous API.
* A synchronous implementation of the Model Context Protocol (MCP) server that wraps
* {@link McpAsyncServer} to provide blocking operations. This class delegates all
* operations to an underlying async server instance while providing a simpler,
* synchronous API for scenarios where reactive programming is not required.
*
* <p>
* The MCP server enables AI models to expose tools, resources, and prompts through a
* standardized interface. Key features available through this synchronous API include:
* <ul>
* <li>Tool registration and management for extending AI model capabilities
* <li>Resource handling with URI-based addressing for providing context
* <li>Prompt template management for standardized interactions
* <li>Real-time client notifications for state changes
* <li>Structured logging with configurable severity levels
* <li>Support for client-side AI model sampling
* </ul>
*
* <p>
* While {@link McpAsyncServer} uses Project Reactor's Mono and Flux types for
* non-blocking operations, this class converts those into blocking calls, making it more
* suitable for:
* <ul>
* <li>Traditional synchronous applications
* <li>Simple scripting scenarios
* <li>Testing and debugging
* <li>Cases where reactive programming adds unnecessary complexity
* </ul>
*
* <p>
* The server supports runtime modification of its capabilities through methods like
* {@link #addTool}, {@link #addResource}, and {@link #addPrompt}, automatically notifying
* connected clients of changes when configured to do so.
*
* @author Christian Tzolov
* @see McpAsyncServer
* @see McpSchema
*/
public class McpSyncServer {
@ -69,6 +100,16 @@ public class McpSyncServer {
* Add a new tool handler.
* @param toolHandler The tool handler to add
*/
public void addTool(McpServerFeatures.SyncToolRegistration toolHandler) {
this.asyncServer.addTool(McpServerFeatures.AsyncToolRegistration.fromSync(toolHandler)).block();
}
/**
* Add a new tool handler.
* @param toolHandler The tool handler to add
* @deprecated Use {@link #addTool(McpServerFeatures.SyncToolRegistration)}.
*/
@Deprecated
public void addTool(ToolRegistration toolHandler) {
this.asyncServer.addTool(toolHandler).block();
}
@ -85,6 +126,16 @@ public class McpSyncServer {
* Add a new resource handler.
* @param resourceHandler The resource handler to add
*/
public void addResource(McpServerFeatures.SyncResourceRegistration resourceHandler) {
this.asyncServer.addResource(McpServerFeatures.AsyncResourceRegistration.fromSync(resourceHandler)).block();
}
/**
* Add a new resource handler.
* @param resourceHandler The resource handler to add
* @deprecated Use {@link #addResource(McpServerFeatures.SyncResourceRegistration)}.
*/
@Deprecated
public void addResource(ResourceRegistration resourceHandler) {
this.asyncServer.addResource(resourceHandler).block();
}
@ -101,6 +152,16 @@ public class McpSyncServer {
* Add a new prompt handler.
* @param promptRegistration The prompt registration to add
*/
public void addPrompt(McpServerFeatures.SyncPromptRegistration promptRegistration) {
this.asyncServer.addPrompt(McpServerFeatures.AsyncPromptRegistration.fromSync(promptRegistration)).block();
}
/**
* Add a new prompt handler.
* @param promptRegistration The prompt registration to add
* @deprecated Use {@link #addPrompt(McpServerFeatures.SyncPromptRegistration)}.
*/
@Deprecated
public void addPrompt(PromptRegistration promptRegistration) {
this.asyncServer.addPrompt(promptRegistration).block();
}
@ -120,28 +181,6 @@ public class McpSyncServer {
this.asyncServer.notifyToolsListChanged().block();
}
/**
* Notify clients that the list of available resources has changed.
*/
public void notifyResourcesListChanged() {
this.asyncServer.notifyResourcesListChanged().block();
}
/**
* Notify clients that the list of available prompts has changed.
*/
public void notifyPromptsListChanged() {
this.asyncServer.notifyPromptsListChanged().block();
}
/**
* Send a logging message notification to all clients.
* @param loggingMessageNotification The logging message notification to send
*/
public void loggingNotification(LoggingMessageNotification loggingMessageNotification) {
this.asyncServer.loggingNotification(loggingMessageNotification).block();
}
/**
* Get the server capabilities that define the supported features and functionality.
* @return The server capabilities
@ -174,6 +213,28 @@ public class McpSyncServer {
return this.asyncServer.getClientInfo();
}
/**
* Notify clients that the list of available resources has changed.
*/
public void notifyResourcesListChanged() {
this.asyncServer.notifyResourcesListChanged().block();
}
/**
* Notify clients that the list of available prompts has changed.
*/
public void notifyPromptsListChanged() {
this.asyncServer.notifyPromptsListChanged().block();
}
/**
* Send a logging message notification to all clients.
* @param loggingMessageNotification The logging message notification to send
*/
public void loggingNotification(LoggingMessageNotification loggingMessageNotification) {
this.asyncServer.loggingNotification(loggingMessageNotification).block();
}
/**
* Close the server gracefully.
*/
@ -199,13 +260,19 @@ public class McpSyncServer {
/**
* Create a new message using the sampling capabilities of the client. The Model
* Context Protocol (MCP) provides a standardized way for servers to request LLM
* sampling (“completions” or “generations”) from language models via clients. This
* flow allows clients to maintain control over model access, selection, and
* sampling ("completions" or "generations") from language models via clients.
*
* <p>
* This flow allows clients to maintain control over model access, selection, and
* permissions while enabling servers to leverage AI capabilities—with no server API
* keys necessary. Servers can request text or image-based interactions and optionally
* include context from MCP servers in their prompts.
*
* <p>
* Unlike its async counterpart, this method blocks until the message creation is
* complete, making it easier to use in synchronous code paths.
* @param createMessageRequest The request to create a new message
* @return A Mono that completes when the message has been created
* @return The result of the message creation
* @throws McpError if the client has not been initialized or does not support
* sampling capabilities
* @throws McpError if the client does not support the createMessage method

View file

@ -63,7 +63,7 @@ public class DefaultMcpSession implements McpSession {
private final ConcurrentHashMap<Object, MonoSink<McpSchema.JSONRPCResponse>> pendingResponses = new ConcurrentHashMap<>();
/** Map of request handlers keyed by method name */
private final ConcurrentHashMap<String, RequestHandler> requestHandlers = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, RequestHandler<?>> requestHandlers = new ConcurrentHashMap<>();
/** Map of notification handlers keyed by method name */
private final ConcurrentHashMap<String, NotificationHandler> notificationHandlers = new ConcurrentHashMap<>();
@ -79,16 +79,18 @@ public class DefaultMcpSession implements McpSession {
/**
* Functional interface for handling incoming JSON-RPC requests. Implementations
* should process the request parameters and return a response.
*
* @param <T> Response type
*/
@FunctionalInterface
public interface RequestHandler {
public interface RequestHandler<T> {
/**
* Handles an incoming request with the given parameters.
* @param params The request parameters
* @return A Mono containing the response object
*/
Mono<Object> handle(Object params);
Mono<T> handle(Object params);
}
@ -116,7 +118,7 @@ public class DefaultMcpSession implements McpSession {
* @param notificationHandlers Map of method names to notification handlers
*/
public DefaultMcpSession(Duration requestTimeout, McpTransport transport,
Map<String, RequestHandler> requestHandlers, Map<String, NotificationHandler> notificationHandlers) {
Map<String, RequestHandler<?>> requestHandlers, Map<String, NotificationHandler> notificationHandlers) {
Assert.notNull(requestTimeout, "The requstTimeout can not be null");
Assert.notNull(transport, "The transport can not be null");