feat: Add progress notification support for MCP operations (#407)
Implement progress tracking for long-running operations with: - New ProgressNotification schema and client/server support - Progress consumer handlers in sync/async client builders - Server exchange methods for sending progress updates - Comprehensive integration tests - Backwards compatibility maintained - Add additional tests Signed-off-by: Christian Tzolov <christian.tzolov@broadcom.com>
This commit is contained in:
parent
87cdaf883a
commit
b04c30b60e
7 changed files with 535 additions and 8 deletions
|
|
@ -100,6 +100,9 @@ public class McpAsyncClient {
|
|||
public static final TypeReference<LoggingMessageNotification> LOGGING_MESSAGE_NOTIFICATION_TYPE_REF = new TypeReference<>() {
|
||||
};
|
||||
|
||||
public static final TypeReference<McpSchema.ProgressNotification> PROGRESS_NOTIFICATION_TYPE_REF = new TypeReference<>() {
|
||||
};
|
||||
|
||||
/**
|
||||
* Client capabilities.
|
||||
*/
|
||||
|
|
@ -253,6 +256,16 @@ public class McpAsyncClient {
|
|||
notificationHandlers.put(McpSchema.METHOD_NOTIFICATION_MESSAGE,
|
||||
asyncLoggingNotificationHandler(loggingConsumersFinal));
|
||||
|
||||
// Utility Progress Notification
|
||||
List<Function<McpSchema.ProgressNotification, Mono<Void>>> progressConsumersFinal = new ArrayList<>();
|
||||
progressConsumersFinal
|
||||
.add((notification) -> Mono.fromRunnable(() -> logger.debug("Progress: {}", notification)));
|
||||
if (!Utils.isEmpty(features.progressConsumers())) {
|
||||
progressConsumersFinal.addAll(features.progressConsumers());
|
||||
}
|
||||
notificationHandlers.put(McpSchema.METHOD_NOTIFICATION_PROGRESS,
|
||||
asyncProgressNotificationHandler(progressConsumersFinal));
|
||||
|
||||
this.initializer = new LifecycleInitializer(clientCapabilities, clientInfo,
|
||||
List.of(McpSchema.LATEST_PROTOCOL_VERSION), initializationTimeout,
|
||||
ctx -> new McpClientSession(requestTimeout, transport, requestHandlers, notificationHandlers,
|
||||
|
|
@ -846,6 +859,28 @@ public class McpAsyncClient {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a notification handler for progress notifications from the server. This
|
||||
* handler automatically distributes progress notifications to all registered
|
||||
* consumers.
|
||||
* @param progressConsumers List of consumers that will be notified when a progress
|
||||
* message is received. Each consumer receives the progress notification.
|
||||
* @return A NotificationHandler that processes progress notifications by distributing
|
||||
* the message to all registered consumers
|
||||
*/
|
||||
private NotificationHandler asyncProgressNotificationHandler(
|
||||
List<Function<McpSchema.ProgressNotification, Mono<Void>>> progressConsumers) {
|
||||
|
||||
return params -> {
|
||||
McpSchema.ProgressNotification progressNotification = transport.unmarshalFrom(params,
|
||||
PROGRESS_NOTIFICATION_TYPE_REF);
|
||||
|
||||
return Flux.fromIterable(progressConsumers)
|
||||
.flatMap(consumer -> consumer.apply(progressNotification))
|
||||
.then();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is package-private and used for test only. Should not be called by user
|
||||
* code.
|
||||
|
|
|
|||
|
|
@ -177,6 +177,8 @@ public interface McpClient {
|
|||
|
||||
private final List<Consumer<McpSchema.LoggingMessageNotification>> loggingConsumers = new ArrayList<>();
|
||||
|
||||
private final List<Consumer<McpSchema.ProgressNotification>> progressConsumers = new ArrayList<>();
|
||||
|
||||
private Function<CreateMessageRequest, CreateMessageResult> samplingHandler;
|
||||
|
||||
private Function<ElicitRequest, ElicitResult> elicitationHandler;
|
||||
|
|
@ -377,6 +379,36 @@ public interface McpClient {
|
|||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a consumer to be notified of progress notifications from the server. This
|
||||
* allows the client to track long-running operations and provide feedback to
|
||||
* users.
|
||||
* @param progressConsumer A consumer that receives progress notifications. Must
|
||||
* not be null.
|
||||
* @return This builder instance for method chaining
|
||||
* @throws IllegalArgumentException if progressConsumer is null
|
||||
*/
|
||||
public SyncSpec progressConsumer(Consumer<McpSchema.ProgressNotification> progressConsumer) {
|
||||
Assert.notNull(progressConsumer, "Progress consumer must not be null");
|
||||
this.progressConsumers.add(progressConsumer);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a multiple consumers to be notified of progress notifications from the
|
||||
* server. This allows the client to track long-running operations and provide
|
||||
* feedback to users.
|
||||
* @param progressConsumers A list of consumers that receives progress
|
||||
* notifications. Must not be null.
|
||||
* @return This builder instance for method chaining
|
||||
* @throws IllegalArgumentException if progressConsumer is null
|
||||
*/
|
||||
public SyncSpec progressConsumers(List<Consumer<McpSchema.ProgressNotification>> progressConsumers) {
|
||||
Assert.notNull(progressConsumers, "Progress consumers must not be null");
|
||||
this.progressConsumers.addAll(progressConsumers);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an instance of {@link McpSyncClient} with the provided configurations or
|
||||
* sensible defaults.
|
||||
|
|
@ -385,7 +417,8 @@ public interface McpClient {
|
|||
public McpSyncClient build() {
|
||||
McpClientFeatures.Sync syncFeatures = new McpClientFeatures.Sync(this.clientInfo, this.capabilities,
|
||||
this.roots, this.toolsChangeConsumers, this.resourcesChangeConsumers, this.resourcesUpdateConsumers,
|
||||
this.promptsChangeConsumers, this.loggingConsumers, this.samplingHandler, this.elicitationHandler);
|
||||
this.promptsChangeConsumers, this.loggingConsumers, this.progressConsumers, this.samplingHandler,
|
||||
this.elicitationHandler);
|
||||
|
||||
McpClientFeatures.Async asyncFeatures = McpClientFeatures.Async.fromSync(syncFeatures);
|
||||
|
||||
|
|
@ -435,6 +468,8 @@ public interface McpClient {
|
|||
|
||||
private final List<Function<McpSchema.LoggingMessageNotification, Mono<Void>>> loggingConsumers = new ArrayList<>();
|
||||
|
||||
private final List<Function<McpSchema.ProgressNotification, Mono<Void>>> progressConsumers = new ArrayList<>();
|
||||
|
||||
private Function<CreateMessageRequest, Mono<CreateMessageResult>> samplingHandler;
|
||||
|
||||
private Function<ElicitRequest, Mono<ElicitResult>> elicitationHandler;
|
||||
|
|
@ -654,6 +689,37 @@ public interface McpClient {
|
|||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a consumer to be notified of progress notifications from the server. This
|
||||
* allows the client to track long-running operations and provide feedback to
|
||||
* users.
|
||||
* @param progressConsumer A consumer that receives progress notifications. Must
|
||||
* not be null.
|
||||
* @return This builder instance for method chaining
|
||||
* @throws IllegalArgumentException if progressConsumer is null
|
||||
*/
|
||||
public AsyncSpec progressConsumer(Function<McpSchema.ProgressNotification, Mono<Void>> progressConsumer) {
|
||||
Assert.notNull(progressConsumer, "Progress consumer must not be null");
|
||||
this.progressConsumers.add(progressConsumer);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a multiple consumers to be notified of progress notifications from the
|
||||
* server. This allows the client to track long-running operations and provide
|
||||
* feedback to users.
|
||||
* @param progressConsumers A list of consumers that receives progress
|
||||
* notifications. Must not be null.
|
||||
* @return This builder instance for method chaining
|
||||
* @throws IllegalArgumentException if progressConsumer is null
|
||||
*/
|
||||
public AsyncSpec progressConsumers(
|
||||
List<Function<McpSchema.ProgressNotification, Mono<Void>>> progressConsumers) {
|
||||
Assert.notNull(progressConsumers, "Progress consumers must not be null");
|
||||
this.progressConsumers.addAll(progressConsumers);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an instance of {@link McpAsyncClient} with the provided configurations
|
||||
* or sensible defaults.
|
||||
|
|
@ -663,8 +729,8 @@ public interface McpClient {
|
|||
return new McpAsyncClient(this.transport, this.requestTimeout, this.initializationTimeout,
|
||||
new McpClientFeatures.Async(this.clientInfo, this.capabilities, this.roots,
|
||||
this.toolsChangeConsumers, this.resourcesChangeConsumers, this.resourcesUpdateConsumers,
|
||||
this.promptsChangeConsumers, this.loggingConsumers, this.samplingHandler,
|
||||
this.elicitationHandler));
|
||||
this.promptsChangeConsumers, this.loggingConsumers, this.progressConsumers,
|
||||
this.samplingHandler, this.elicitationHandler));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ class McpClientFeatures {
|
|||
* @param resourcesChangeConsumers the resources change consumers.
|
||||
* @param promptsChangeConsumers the prompts change consumers.
|
||||
* @param loggingConsumers the logging consumers.
|
||||
* @param progressConsumers the progress consumers.
|
||||
* @param samplingHandler the sampling handler.
|
||||
* @param elicitationHandler the elicitation handler.
|
||||
*/
|
||||
|
|
@ -68,6 +69,7 @@ class McpClientFeatures {
|
|||
List<Function<List<McpSchema.ResourceContents>, Mono<Void>>> resourcesUpdateConsumers,
|
||||
List<Function<List<McpSchema.Prompt>, Mono<Void>>> promptsChangeConsumers,
|
||||
List<Function<McpSchema.LoggingMessageNotification, Mono<Void>>> loggingConsumers,
|
||||
List<Function<McpSchema.ProgressNotification, Mono<Void>>> progressConsumers,
|
||||
Function<McpSchema.CreateMessageRequest, Mono<McpSchema.CreateMessageResult>> samplingHandler,
|
||||
Function<McpSchema.ElicitRequest, Mono<McpSchema.ElicitResult>> elicitationHandler) {
|
||||
|
||||
|
|
@ -79,6 +81,7 @@ class McpClientFeatures {
|
|||
* @param resourcesChangeConsumers the resources change consumers.
|
||||
* @param promptsChangeConsumers the prompts change consumers.
|
||||
* @param loggingConsumers the logging consumers.
|
||||
* @param progressConsumers the progress consumers.
|
||||
* @param samplingHandler the sampling handler.
|
||||
* @param elicitationHandler the elicitation handler.
|
||||
*/
|
||||
|
|
@ -89,6 +92,7 @@ class McpClientFeatures {
|
|||
List<Function<List<McpSchema.ResourceContents>, Mono<Void>>> resourcesUpdateConsumers,
|
||||
List<Function<List<McpSchema.Prompt>, Mono<Void>>> promptsChangeConsumers,
|
||||
List<Function<McpSchema.LoggingMessageNotification, Mono<Void>>> loggingConsumers,
|
||||
List<Function<McpSchema.ProgressNotification, Mono<Void>>> progressConsumers,
|
||||
Function<McpSchema.CreateMessageRequest, Mono<McpSchema.CreateMessageResult>> samplingHandler,
|
||||
Function<McpSchema.ElicitRequest, Mono<McpSchema.ElicitResult>> elicitationHandler) {
|
||||
|
||||
|
|
@ -106,10 +110,28 @@ class McpClientFeatures {
|
|||
this.resourcesUpdateConsumers = resourcesUpdateConsumers != null ? resourcesUpdateConsumers : List.of();
|
||||
this.promptsChangeConsumers = promptsChangeConsumers != null ? promptsChangeConsumers : List.of();
|
||||
this.loggingConsumers = loggingConsumers != null ? loggingConsumers : List.of();
|
||||
this.progressConsumers = progressConsumers != null ? progressConsumers : List.of();
|
||||
this.samplingHandler = samplingHandler;
|
||||
this.elicitationHandler = elicitationHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Only exists for backwards-compatibility purposes.
|
||||
*/
|
||||
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.ResourceContents>, Mono<Void>>> resourcesUpdateConsumers,
|
||||
List<Function<List<McpSchema.Prompt>, Mono<Void>>> promptsChangeConsumers,
|
||||
List<Function<McpSchema.LoggingMessageNotification, Mono<Void>>> loggingConsumers,
|
||||
Function<McpSchema.CreateMessageRequest, Mono<McpSchema.CreateMessageResult>> samplingHandler,
|
||||
Function<McpSchema.ElicitRequest, Mono<McpSchema.ElicitResult>> elicitationHandler) {
|
||||
this(clientInfo, clientCapabilities, roots, toolsChangeConsumers, resourcesChangeConsumers,
|
||||
resourcesUpdateConsumers, promptsChangeConsumers, loggingConsumers, List.of(), samplingHandler,
|
||||
elicitationHandler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a synchronous specification into an asynchronous one and provide
|
||||
* blocking code offloading to prevent accidental blocking of the non-blocking
|
||||
|
|
@ -149,6 +171,12 @@ class McpClientFeatures {
|
|||
.subscribeOn(Schedulers.boundedElastic()));
|
||||
}
|
||||
|
||||
List<Function<McpSchema.ProgressNotification, Mono<Void>>> progressConsumers = new ArrayList<>();
|
||||
for (Consumer<McpSchema.ProgressNotification> consumer : syncSpec.progressConsumers()) {
|
||||
progressConsumers.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());
|
||||
|
|
@ -159,7 +187,7 @@ class McpClientFeatures {
|
|||
|
||||
return new Async(syncSpec.clientInfo(), syncSpec.clientCapabilities(), syncSpec.roots(),
|
||||
toolsChangeConsumers, resourcesChangeConsumers, resourcesUpdateConsumers, promptsChangeConsumers,
|
||||
loggingConsumers, samplingHandler, elicitationHandler);
|
||||
loggingConsumers, progressConsumers, samplingHandler, elicitationHandler);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -174,6 +202,7 @@ class McpClientFeatures {
|
|||
* @param resourcesChangeConsumers the resources change consumers.
|
||||
* @param promptsChangeConsumers the prompts change consumers.
|
||||
* @param loggingConsumers the logging consumers.
|
||||
* @param progressConsumers the progress consumers.
|
||||
* @param samplingHandler the sampling handler.
|
||||
* @param elicitationHandler the elicitation handler.
|
||||
*/
|
||||
|
|
@ -183,6 +212,7 @@ class McpClientFeatures {
|
|||
List<Consumer<List<McpSchema.ResourceContents>>> resourcesUpdateConsumers,
|
||||
List<Consumer<List<McpSchema.Prompt>>> promptsChangeConsumers,
|
||||
List<Consumer<McpSchema.LoggingMessageNotification>> loggingConsumers,
|
||||
List<Consumer<McpSchema.ProgressNotification>> progressConsumers,
|
||||
Function<McpSchema.CreateMessageRequest, McpSchema.CreateMessageResult> samplingHandler,
|
||||
Function<McpSchema.ElicitRequest, McpSchema.ElicitResult> elicitationHandler) {
|
||||
|
||||
|
|
@ -196,6 +226,7 @@ class McpClientFeatures {
|
|||
* @param resourcesUpdateConsumers the resource update consumers.
|
||||
* @param promptsChangeConsumers the prompts change consumers.
|
||||
* @param loggingConsumers the logging consumers.
|
||||
* @param progressConsumers the progress consumers.
|
||||
* @param samplingHandler the sampling handler.
|
||||
* @param elicitationHandler the elicitation handler.
|
||||
*/
|
||||
|
|
@ -205,6 +236,7 @@ class McpClientFeatures {
|
|||
List<Consumer<List<McpSchema.ResourceContents>>> resourcesUpdateConsumers,
|
||||
List<Consumer<List<McpSchema.Prompt>>> promptsChangeConsumers,
|
||||
List<Consumer<McpSchema.LoggingMessageNotification>> loggingConsumers,
|
||||
List<Consumer<McpSchema.ProgressNotification>> progressConsumers,
|
||||
Function<McpSchema.CreateMessageRequest, McpSchema.CreateMessageResult> samplingHandler,
|
||||
Function<McpSchema.ElicitRequest, McpSchema.ElicitResult> elicitationHandler) {
|
||||
|
||||
|
|
@ -222,9 +254,26 @@ class McpClientFeatures {
|
|||
this.resourcesUpdateConsumers = resourcesUpdateConsumers != null ? resourcesUpdateConsumers : List.of();
|
||||
this.promptsChangeConsumers = promptsChangeConsumers != null ? promptsChangeConsumers : List.of();
|
||||
this.loggingConsumers = loggingConsumers != null ? loggingConsumers : List.of();
|
||||
this.progressConsumers = progressConsumers != null ? progressConsumers : List.of();
|
||||
this.samplingHandler = samplingHandler;
|
||||
this.elicitationHandler = elicitationHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Only exists for backwards-compatibility purposes.
|
||||
*/
|
||||
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.ResourceContents>>> resourcesUpdateConsumers,
|
||||
List<Consumer<List<McpSchema.Prompt>>> promptsChangeConsumers,
|
||||
List<Consumer<McpSchema.LoggingMessageNotification>> loggingConsumers,
|
||||
Function<McpSchema.CreateMessageRequest, McpSchema.CreateMessageResult> samplingHandler,
|
||||
Function<McpSchema.ElicitRequest, McpSchema.ElicitResult> elicitationHandler) {
|
||||
this(clientInfo, clientCapabilities, roots, toolsChangeConsumers, resourcesChangeConsumers,
|
||||
resourcesUpdateConsumers, promptsChangeConsumers, loggingConsumers, List.of(), samplingHandler,
|
||||
elicitationHandler);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -177,6 +177,19 @@ public class McpAsyncServerExchange {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a notification to the client that the current progress status has changed for
|
||||
* long-running operations.
|
||||
* @param progressNotification The progress notification to send
|
||||
* @return A Mono that completes when the notification has been sent
|
||||
*/
|
||||
public Mono<Void> progressNotification(McpSchema.ProgressNotification progressNotification) {
|
||||
if (progressNotification == null) {
|
||||
return Mono.error(new McpError("Progress notification must not be null"));
|
||||
}
|
||||
return this.session.sendNotification(McpSchema.METHOD_NOTIFICATION_PROGRESS, progressNotification);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a ping request to the client.
|
||||
* @return A Mono that completes with clients's ping response
|
||||
|
|
|
|||
|
|
@ -209,7 +209,7 @@ public class McpServerFeatures {
|
|||
* represents a specific capability.
|
||||
*
|
||||
* @param tool The tool definition including name, description, and parameter schema
|
||||
* @param call Deprecated. Uset he {@link AsyncToolSpecification#callHandler} instead.
|
||||
* @param call Deprecated. Use the {@link AsyncToolSpecification#callHandler} instead.
|
||||
* @param callHandler The function that implements the tool's logic, receiving a
|
||||
* {@link McpAsyncServerExchange} and a
|
||||
* {@link io.modelcontextprotocol.spec.McpSchema.CallToolRequest} and returning
|
||||
|
|
|
|||
|
|
@ -107,6 +107,15 @@ public class McpSyncServerExchange {
|
|||
this.exchange.loggingNotification(loggingMessageNotification).block();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a notification to the client that the current progress status has changed for
|
||||
* long-running operations.
|
||||
* @param progressNotification The progress notification to send
|
||||
*/
|
||||
public void progressNotification(McpSchema.ProgressNotification progressNotification) {
|
||||
this.exchange.progressNotification(progressNotification).block();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a synchronous ping request to the client.
|
||||
* @return
|
||||
|
|
|
|||
|
|
@ -61,6 +61,8 @@ public final class McpSchema {
|
|||
|
||||
public static final String METHOD_PING = "ping";
|
||||
|
||||
public static final String METHOD_NOTIFICATION_PROGRESS = "notifications/progress";
|
||||
|
||||
// Tool Methods
|
||||
public static final String METHOD_TOOLS_LIST = "tools/list";
|
||||
|
||||
|
|
@ -1388,7 +1390,8 @@ public final class McpSchema {
|
|||
* tools/list.
|
||||
* @param arguments Arguments to pass to the tool. These must conform to the tool's
|
||||
* input schema.
|
||||
* @param meta See specification for notes on _meta usage
|
||||
* @param meta Optional metadata about the request. This can include additional
|
||||
* information like `progressToken`
|
||||
*/
|
||||
@JsonInclude(JsonInclude.Include.NON_ABSENT)
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
|
|
|
|||
Loading…
Reference in a new issue