Signed-off-by: Dariusz Jędrzejczyk <dariusz.jedrzejczyk@broadcom.com>
This commit is contained in:
Dariusz Jędrzejczyk 2025-01-23 13:33:11 +01:00 • committed by Christian Tzolov
commit 919433437c
8 changed files with 181 additions and 69 deletions

View file

@ -517,7 +517,7 @@ public class McpAsyncClient {
return this.mcpSession.sendNotification(McpSchema.METHOD_NOTIFICATION_ROOTS_LIST_CHANGED);
}
private RequestHandler rootsListRequestHandler() {
private RequestHandler<McpSchema.ListRootsResult> rootsListRequestHandler() {
return params -> {
McpSchema.PaginatedRequest request = transport.unmarshalFrom(params,
new TypeReference<McpSchema.PaginatedRequest>() {
@ -545,10 +545,10 @@ public class McpAsyncClient {
// --------------------------
// Tools
// --------------------------
private static TypeReference<McpSchema.CallToolResult> CALL_TOOL_RESULT_TYPE_REF = new TypeReference<>() {
private static final TypeReference<McpSchema.CallToolResult> CALL_TOOL_RESULT_TYPE_REF = new TypeReference<>() {
};
private static TypeReference<McpSchema.ListToolsResult> LIST_TOOLS_RESULT_TYPE_REF = new TypeReference<>() {
private static final TypeReference<McpSchema.ListToolsResult> LIST_TOOLS_RESULT_TYPE_REF = new TypeReference<>() {
};
/**
@ -601,6 +601,7 @@ public class McpAsyncClient {
* list to all registered consumers 3. Handling any errors that occur during this
* process
*/
@Deprecated
private NotificationHandler toolsChangeNotificationHandler(
List<Consumer<List<McpSchema.Tool>>> toolsChangeConsumers) {
@ -631,6 +632,10 @@ public class McpAsyncClient {
// TODO: params are not used yet
return params -> listTools().flatMap(listToolsResult -> Flux.fromIterable(toolsChangeConsumers)
.flatMap(consumer -> consumer.apply(listToolsResult.tools()))
.onErrorResume(error -> {
logger.error("Error handling tools list change notification", error);
return Mono.empty();
})
.then());
}
@ -638,13 +643,13 @@ public class McpAsyncClient {
// Resources
// --------------------------
private static TypeReference<McpSchema.ListResourcesResult> LIST_RESOURCES_RESULT_TYPE_REF = new TypeReference<>() {
private static final TypeReference<McpSchema.ListResourcesResult> LIST_RESOURCES_RESULT_TYPE_REF = new TypeReference<>() {
};
private static TypeReference<McpSchema.ReadResourceResult> READ_RESOURCE_RESULT_TYPE_REF = new TypeReference<>() {
private static final TypeReference<McpSchema.ReadResourceResult> READ_RESOURCE_RESULT_TYPE_REF = new TypeReference<>() {
};
private static TypeReference<McpSchema.ListResourceTemplatesResult> LIST_RESOURCE_TEMPLATES_RESULT_TYPE_REF = new TypeReference<>() {
private static final TypeReference<McpSchema.ListResourceTemplatesResult> LIST_RESOURCE_TEMPLATES_RESULT_TYPE_REF = new TypeReference<>() {
};
/**
@ -742,6 +747,7 @@ public class McpAsyncClient {
VOID_TYPE_REFERENCE);
}
@Deprecated
private NotificationHandler resourcesChangeNotificationHandler(
List<Consumer<List<McpSchema.Resource>>> resourcesChangeConsumers) {
@ -759,16 +765,20 @@ public class McpAsyncClient {
List<Function<List<McpSchema.Resource>, Mono<Void>>> resourcesChangeConsumers) {
return params -> listResources().flatMap(listResourcesResult -> Flux.fromIterable(resourcesChangeConsumers)
.flatMap(consumer -> consumer.apply(listResourcesResult.resources()))
.onErrorResume(error -> {
logger.error("Error handling resources list change notification", error);
return Mono.empty();
})
.then());
}
// --------------------------
// Prompts
// --------------------------
private static TypeReference<McpSchema.ListPromptsResult> LIST_PROMPTS_RESULT_TYPE_REF = new TypeReference<>() {
private static final TypeReference<McpSchema.ListPromptsResult> LIST_PROMPTS_RESULT_TYPE_REF = new TypeReference<>() {
};
private static TypeReference<McpSchema.GetPromptResult> GET_PROMPT_RESULT_TYPE_REF = new TypeReference<>() {
private static final TypeReference<McpSchema.GetPromptResult> GET_PROMPT_RESULT_TYPE_REF = new TypeReference<>() {
};
/**
@ -808,6 +818,7 @@ public class McpAsyncClient {
return this.mcpSession.sendNotification(McpSchema.METHOD_NOTIFICATION_PROMPTS_LIST_CHANGED);
}
@Deprecated
private NotificationHandler promptsChangeNotificationHandler(
List<Consumer<List<McpSchema.Prompt>>> promptsChangeConsumers) {
@ -827,6 +838,10 @@ public class McpAsyncClient {
List<Function<List<McpSchema.Prompt>, Mono<Void>>> promptsChangeConsumers) {
return params -> listPrompts().flatMap(listPromptsResult -> Flux.fromIterable(promptsChangeConsumers)
.flatMap(consumer -> consumer.apply(listPromptsResult.prompts()))
.onErrorResume(error -> {
logger.error("Error handling prompts list change notification", error);
return Mono.empty();
})
.then());
}

View file

@ -65,7 +65,7 @@ import org.springframework.ai.mcp.util.Assert;
* .requestTimeout(Duration.ofSeconds(5))
* .build();
* }</pre>
*
*
* Example of creating a basic asynchronous client: <pre>{@code
* McpClient.async(transport)
* .requestTimeout(Duration.ofSeconds(5))
@ -114,6 +114,40 @@ import org.springframework.ai.mcp.util.Assert;
*/
public interface McpClient {
/**
* Start building a synchronous MCP client with the specified transport layer. The
* synchronous MCP client provides blocking operations. Synchronous clients wait for
* each operation to complete before returning, making them simpler to use but
* potentially less performant for concurrent operations. The transport layer handles
* the low-level communication between client and server using protocols like stdio or
* Server-Sent Events (SSE).
* @param transport The transport layer implementation for MCP communication. Common
* implementations include {@code StdioClientTransport} for stdio-based communication
* and {@code SseClientTransport} for SSE-based communication.
* @return A new builder instance for configuring the client
* @throws IllegalArgumentException if transport is null
*/
static SyncSpec sync(ClientMcpTransport transport) {
return new SyncSpec(transport);
}
/**
* Start building an asynchronous MCP client with the specified transport layer. The
* asynchronous MCP client provides non-blocking operations. Asynchronous clients
* return reactive primitives (Mono/Flux) immediately, allowing for concurrent
* operations and reactive programming patterns. The transport layer handles the
* low-level communication between client and server using protocols like stdio or
* Server-Sent Events (SSE).
* @param transport The transport layer implementation for MCP communication. Common
* implementations include {@code StdioClientTransport} for stdio-based communication
* and {@code SseClientTransport} for SSE-based communication.
* @return A new builder instance for configuring the client
* @throws IllegalArgumentException if transport is null
*/
static AsyncSpec async(ClientMcpTransport transport) {
return new AsyncSpec(transport);
}
/**
* Start building an MCP client with the specified transport layer. The transport
* layer handles the low-level communication between client and server using protocols
@ -371,7 +405,20 @@ public interface McpClient {
}
/**
* Synchronous client specification.
* Synchronous client specification. This class follows the builder pattern to provide
* a fluent API for setting up clients with custom configurations.
*
* <p>
* The builder supports configuration of:
* <ul>
* <li>Transport layer for client-server communication
* <li>Request timeouts for operation boundaries
* <li>Client capabilities for feature negotiation
* <li>Client implementation details for version tracking
* <li>Root URIs for resource access
* <li>Change notification handlers for tools, resources, and prompts
* <li>Custom message sampling logic
* </ul>
*/
class SyncSpec {
@ -381,21 +428,22 @@ public interface McpClient {
private ClientCapabilities capabilities;
private Implementation clientInfo = new Implementation("Spring AI MCP Client", "0.3.1");
private Implementation clientInfo = new Implementation("Spring AI MCP Client", "1.0.0");
private Map<String, Root> roots = new HashMap<>();
private final Map<String, Root> roots = new HashMap<>();
private List<Consumer<List<McpSchema.Tool>>> toolsChangeConsumers = new ArrayList<>();
private final List<Consumer<List<McpSchema.Tool>>> toolsChangeConsumers = new ArrayList<>();
private List<Consumer<List<McpSchema.Resource>>> resourcesChangeConsumers = new ArrayList<>();
private final List<Consumer<List<McpSchema.Resource>>> resourcesChangeConsumers = new ArrayList<>();
private List<Consumer<List<McpSchema.Prompt>>> promptsChangeConsumers = new ArrayList<>();
private final List<Consumer<List<McpSchema.Prompt>>> promptsChangeConsumers = new ArrayList<>();
private List<Consumer<McpSchema.LoggingMessageNotification>> loggingConsumers = new ArrayList<>();
private final List<Consumer<McpSchema.LoggingMessageNotification>> loggingConsumers = new ArrayList<>();
private Function<CreateMessageRequest, CreateMessageResult> samplingHandler;
private SyncSpec(ClientMcpTransport transport) {
Assert.notNull(transport, "Transport must not be null");
this.transport = transport;
}
@ -581,7 +629,20 @@ public interface McpClient {
}
/**
* Asynchronous client specification.
* Asynchronous client specification. This class follows the builder pattern to
* provide a fluent API for setting up clients with custom configurations.
*
* <p>
* The builder supports configuration of:
* <ul>
* <li>Transport layer for client-server communication
* <li>Request timeouts for operation boundaries
* <li>Client capabilities for feature negotiation
* <li>Client implementation details for version tracking
* <li>Root URIs for resource access
* <li>Change notification handlers for tools, resources, and prompts
* <li>Custom message sampling logic
* </ul>
*/
class AsyncSpec {
@ -593,19 +654,20 @@ public interface McpClient {
private Implementation clientInfo = new Implementation("Spring AI MCP Client", "0.3.1");
private Map<String, Root> roots = new HashMap<>();
private final Map<String, Root> roots = new HashMap<>();
private List<Function<List<McpSchema.Tool>, Mono<Void>>> toolsChangeConsumers = new ArrayList<>();
private final List<Function<List<McpSchema.Tool>, Mono<Void>>> toolsChangeConsumers = new ArrayList<>();
private List<Function<List<McpSchema.Resource>, Mono<Void>>> resourcesChangeConsumers = new ArrayList<>();
private final List<Function<List<McpSchema.Resource>, Mono<Void>>> resourcesChangeConsumers = new ArrayList<>();
private List<Function<List<McpSchema.Prompt>, Mono<Void>>> promptsChangeConsumers = new ArrayList<>();
private final List<Function<List<McpSchema.Prompt>, Mono<Void>>> promptsChangeConsumers = new ArrayList<>();
private List<Function<McpSchema.LoggingMessageNotification, Mono<Void>>> loggingConsumers = new ArrayList<>();
private final List<Function<McpSchema.LoggingMessageNotification, Mono<Void>>> loggingConsumers = new ArrayList<>();
private Function<CreateMessageRequest, Mono<CreateMessageResult>> samplingHandler;
private AsyncSpec(ClientMcpTransport transport) {
Assert.notNull(transport, "Transport must not be null");
this.transport = transport;
}
@ -789,12 +851,4 @@ public interface McpClient {
}
static SyncSpec sync(ClientMcpTransport transport) {
return new SyncSpec(transport);
}
static AsyncSpec async(ClientMcpTransport transport) {
return new AsyncSpec(transport);
}
}

View file

@ -16,8 +16,8 @@ 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:
* 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
@ -38,6 +38,7 @@ import org.springframework.ai.mcp.util.Utils;
* 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.
*
* @author Dariusz Jędrzejczyk
* @see McpClient
* @see McpSchema.Implementation
* @see McpSchema.ClientCapabilities
@ -92,10 +93,10 @@ class McpClientFeatures {
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.toolsChangeConsumers = toolsChangeConsumers != null ? toolsChangeConsumers : List.of();
this.resourcesChangeConsumers = resourcesChangeConsumers != null ? resourcesChangeConsumers : List.of();
this.promptsChangeConsumers = promptsChangeConsumers != null ? promptsChangeConsumers : List.of();
this.loggingConsumers = loggingConsumers != null ? loggingConsumers : List.of();
this.samplingHandler = samplingHandler;
}
@ -189,10 +190,10 @@ class McpClientFeatures {
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.toolsChangeConsumers = toolsChangeConsumers != null ? toolsChangeConsumers : List.of();
this.resourcesChangeConsumers = resourcesChangeConsumers != null ? resourcesChangeConsumers : List.of();
this.promptsChangeConsumers = promptsChangeConsumers != null ? promptsChangeConsumers : List.of();
this.loggingConsumers = loggingConsumers != null ? loggingConsumers : List.of();
this.samplingHandler = samplingHandler;
}
}

View file

@ -21,6 +21,7 @@ import java.time.Duration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.mcp.spec.ClientMcpTransport;
import org.springframework.ai.mcp.spec.McpSchema;
import org.springframework.ai.mcp.spec.McpSchema.ClientCapabilities;
import org.springframework.ai.mcp.spec.McpSchema.GetPromptRequest;
@ -74,6 +75,14 @@ public class McpSyncClient implements AutoCloseable {
private final McpAsyncClient delegate;
/**
* Create a new McpSyncClient with the given delegate.
* @param delegate the asynchronous kernel on top of which this synchronous client
* provides a blocking API.
* @deprecated Use {@link McpClient#sync(ClientMcpTransport)} to obtain an instance.
*/
@Deprecated
// TODO make the constructor package private post-deprecation
public McpSyncClient(McpAsyncClient delegate) {
Assert.notNull(delegate, "The delegate can not be null");
this.delegate = delegate;

View file

@ -84,6 +84,7 @@ import org.springframework.ai.mcp.util.Utils;
* connected clients of changes when configured to do so.
*
* @author Christian Tzolov
* @author Dariusz Jędrzejczyk
* @see McpServer
* @see McpSchema
* @see DefaultMcpSession
@ -137,7 +138,7 @@ public class McpAsyncServer {
Map<String, DefaultMcpSession.RequestHandler<?>> requestHandlers = new HashMap<>();
// Initialize request handlers for standard MCP methods
requestHandlers.put(McpSchema.METHOD_INITIALIZE, initializeRequestHandler());
requestHandlers.put(McpSchema.METHOD_INITIALIZE, asyncInitializeRequestHandler());
// Ping MUST respond with an empty data, but not NULL response.
requestHandlers.put(McpSchema.METHOD_PING, (params) -> Mono.just(""));
@ -287,6 +288,28 @@ public class McpAsyncServer {
// ---------------------------------------
// Lifecycle Management
// ---------------------------------------
private DefaultMcpSession.RequestHandler<McpSchema.InitializeResult> asyncInitializeRequestHandler() {
return params -> {
McpSchema.InitializeRequest initializeRequest = transport.unmarshalFrom(params,
new TypeReference<McpSchema.InitializeRequest>() {
});
this.clientCapabilities = initializeRequest.capabilities();
this.clientInfo = initializeRequest.clientInfo();
logger.info("Client initialize request - Protocol: {}, Capabilities: {}, Info: {}",
initializeRequest.protocolVersion(), initializeRequest.capabilities(),
initializeRequest.clientInfo());
if (!McpSchema.LATEST_PROTOCOL_VERSION.equals(initializeRequest.protocolVersion())) {
return Mono.error(new McpError(
"Unsupported protocol version from client: " + initializeRequest.protocolVersion()));
}
return Mono.just(new McpSchema.InitializeResult(McpSchema.LATEST_PROTOCOL_VERSION, this.serverCapabilities,
this.serverInfo, null));
};
}
@Deprecated
private DefaultMcpSession.RequestHandler<McpSchema.InitializeResult> initializeRequestHandler() {
return params -> {
McpSchema.InitializeRequest initializeRequest = transport.unmarshalFrom(params,
@ -360,7 +383,7 @@ public class McpAsyncServer {
this.mcpSession.close();
}
private static TypeReference<McpSchema.ListRootsResult> LIST_ROOTS_RESULT_TYPE_REF = new TypeReference<>() {
private static final TypeReference<McpSchema.ListRootsResult> LIST_ROOTS_RESULT_TYPE_REF = new TypeReference<>() {
};
/**
@ -399,6 +422,10 @@ public class McpAsyncServer {
List<Function<List<McpSchema.Root>, Mono<Void>>> rootsChangeConsumers) {
return params -> listRoots().flatMap(listRootsResult -> Flux.fromIterable(rootsChangeConsumers)
.flatMap(consumer -> consumer.apply(listRootsResult.roots()))
.onErrorResume(error -> {
logger.error("Error handling roots list change notification", error);
return Mono.empty();
})
.then());
}
@ -654,7 +681,7 @@ public class McpAsyncServer {
var resourceUri = resourceRequest.uri();
McpServerFeatures.AsyncResourceRegistration registration = this.resources.get(resourceUri);
if (registration != null) {
return this.resources.get(resourceUri).readHandler().apply(resourceRequest);
return registration.readHandler().apply(resourceRequest);
}
return Mono.error(new McpError("Resource not found: " + resourceUri));
};

View file

@ -120,12 +120,37 @@ import org.springframework.ai.mcp.util.Assert;
* }</pre>
*
* @author Christian Tzolov
* @author Dariusz Jędrzejczyk
* @see McpAsyncServer
* @see McpSyncServer
* @see McpTransport
*/
public interface McpServer {
/**
* Starts building a synchronous MCP server that provides blocking operations.
* Synchronous servers process each request to completion before handling the next
* one, making them simpler to implement but potentially less performant for
* concurrent operations.
* @param transport The transport layer implementation for MCP communication
* @return A new instance of {@link SyncSpec} for configuring the server.
*/
static SyncSpec sync(ServerMcpTransport transport) {
return new SyncSpec(transport);
}
/**
* Starts building an asynchronous MCP server that provides blocking operations.
* Asynchronous servers can handle multiple requests concurrently using a functional
* paradigm with non-blocking server transports, making them more efficient for
* high-concurrency scenarios but more complex to implement.
* @param transport The transport layer implementation for MCP communication
* @return A new instance of {@link SyncSpec} for configuring the server.
*/
static AsyncSpec async(ServerMcpTransport transport) {
return new AsyncSpec(transport);
}
/**
* Start building an MCP server with the specified transport.
* @param transport The transport layer implementation for MCP communication
@ -170,7 +195,7 @@ public interface McpServer {
*/
private final Map<String, McpServerFeatures.AsyncResourceRegistration> resources = new HashMap<>();
private List<ResourceTemplate> resourceTemplates = new ArrayList<>();
private final List<ResourceTemplate> resourceTemplates = new ArrayList<>();
/**
* The Model Context Protocol (MCP) provides a standardized way for servers to
@ -547,7 +572,7 @@ public interface McpServer {
*/
private final Map<String, McpServerFeatures.SyncResourceRegistration> resources = new HashMap<>();
private List<ResourceTemplate> resourceTemplates = new ArrayList<>();
private final List<ResourceTemplate> resourceTemplates = new ArrayList<>();
/**
* The Model Context Protocol (MCP) provides a standardized way for servers to
@ -556,9 +581,9 @@ public interface McpServer {
* discover available prompts, retrieve their contents, and provide arguments to
* customize them.
*/
private Map<String, McpServerFeatures.SyncPromptRegistration> prompts = new HashMap<>();
private final Map<String, McpServerFeatures.SyncPromptRegistration> prompts = new HashMap<>();
private List<Consumer<List<McpSchema.Root>>> rootsChangeConsumers = new ArrayList<>();
private final List<Consumer<List<McpSchema.Root>>> rootsChangeConsumers = new ArrayList<>();
private SyncSpec(ServerMcpTransport transport) {
Assert.notNull(transport, "Transport must not be null");
@ -895,28 +920,6 @@ public interface McpServer {
}
/**
* Builds a synchronous MCP server that provides blocking operations. Synchronous
* servers process each request to completion before handling the next one, making
* them simpler to implement but potentially less performant for concurrent
* operations.
* @return A new instance of {@link SyncSpec} for configuring the server.
*/
static SyncSpec sync(ServerMcpTransport transport) {
return new SyncSpec(transport);
}
/**
* Builds an asynchronous MCP server that provides non-blocking operations.
* Asynchronous servers can handle multiple requests concurrently using a functional
* paradigm with non-blocking server transports, making them more efficient for
* high-concurrency scenarios but more complex to implement.
* @return A new instance of {@link SyncSpec} for configuring the server.
*/
static AsyncSpec async(ServerMcpTransport transport) {
return new AsyncSpec(transport);
}
/**
* Builder class for creating MCP servers with custom configuration.
*

View file

@ -16,6 +16,8 @@ import org.springframework.ai.mcp.util.Utils;
/**
* MCP server features specification that a particular server can choose to support.
*
* @author Dariusz Jędrzejczyk
*/
public class McpServerFeatures {

View file

@ -60,6 +60,7 @@ import org.springframework.ai.mcp.util.Assert;
* connected clients of changes when configured to do so.
*
* @author Christian Tzolov
* @author Dariusz Jędrzejczyk
* @see McpAsyncServer
* @see McpSchema
*/