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:
parent
e1bdc98813
commit
6839d3b740
9 changed files with 3494 additions and 753 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
||||
|
|
|
|||
|
|
@ -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<>() {
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -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) {
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
Loading…
Reference in a new issue