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
2 changed files with 535 additions and 8 deletions
|
|
@ -12,8 +12,10 @@ import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
|||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
|
@ -49,6 +51,7 @@ import io.modelcontextprotocol.spec.McpSchema.UnsubscribeRequest;
|
|||
import io.modelcontextprotocol.spec.McpTransport;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.publisher.Sinks;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
/**
|
||||
|
|
@ -420,7 +423,7 @@ public abstract class AbstractMcpAsyncClientTests {
|
|||
.consumeNextWith(result -> {
|
||||
assertThat(result.prompts()).isNotNull();
|
||||
// Verify that the returned list is immutable
|
||||
assertThatThrownBy(() -> result.prompts().add(new Prompt("test", "Test", "test", null)))
|
||||
assertThatThrownBy(() -> result.prompts().add(new Prompt("test", "test", "test", null)))
|
||||
.isInstanceOf(UnsupportedOperationException.class);
|
||||
})
|
||||
.verifyComplete();
|
||||
|
|
@ -792,4 +795,39 @@ public abstract class AbstractMcpAsyncClientTests {
|
|||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------
|
||||
// Progress Notification Tests
|
||||
// ---------------------------------------
|
||||
|
||||
@Test
|
||||
void testProgressConsumer() {
|
||||
Sinks.Many<McpSchema.ProgressNotification> sink = Sinks.many().unicast().onBackpressureBuffer();
|
||||
List<McpSchema.ProgressNotification> receivedNotifications = new CopyOnWriteArrayList<>();
|
||||
|
||||
withClient(createMcpTransport(), builder -> builder.progressConsumer(notification -> {
|
||||
receivedNotifications.add(notification);
|
||||
sink.tryEmitNext(notification);
|
||||
return Mono.empty();
|
||||
}), client -> {
|
||||
StepVerifier.create(client.initialize()).expectNextMatches(Objects::nonNull).verifyComplete();
|
||||
|
||||
// Call a tool that sends progress notifications
|
||||
CallToolRequest request = CallToolRequest.builder()
|
||||
.name("longRunningOperation")
|
||||
.arguments(Map.of("duration", 1, "steps", 2))
|
||||
.progressToken("test-token")
|
||||
.build();
|
||||
|
||||
StepVerifier.create(client.callTool(request)).consumeNextWith(result -> {
|
||||
assertThat(result).isNotNull();
|
||||
}).verifyComplete();
|
||||
|
||||
// Use StepVerifier to verify the progress notifications via the sink
|
||||
StepVerifier.create(sink.asFlux()).expectNextCount(2).thenCancel().verify(Duration.ofSeconds(3));
|
||||
|
||||
assertThat(receivedNotifications).hasSize(2);
|
||||
assertThat(receivedNotifications.get(0).progressToken()).isEqualTo("test-token");
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,9 @@ import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
|||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
|
@ -648,4 +651,48 @@ public abstract class AbstractMcpSyncClientTests {
|
|||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------
|
||||
// Progress Notification Tests
|
||||
// ---------------------------------------
|
||||
|
||||
@Test
|
||||
void testProgressConsumer() {
|
||||
AtomicInteger progressNotificationCount = new AtomicInteger(0);
|
||||
List<McpSchema.ProgressNotification> receivedNotifications = new CopyOnWriteArrayList<>();
|
||||
CountDownLatch latch = new CountDownLatch(2);
|
||||
|
||||
withClient(createMcpTransport(), builder -> builder.progressConsumer(notification -> {
|
||||
System.out.println("Received progress notification: " + notification);
|
||||
receivedNotifications.add(notification);
|
||||
progressNotificationCount.incrementAndGet();
|
||||
latch.countDown();
|
||||
}), client -> {
|
||||
client.initialize();
|
||||
|
||||
// Call a tool that sends progress notifications
|
||||
CallToolRequest request = CallToolRequest.builder()
|
||||
.name("longRunningOperation")
|
||||
.arguments(Map.of("duration", 1, "steps", 2))
|
||||
.progressToken("test-token")
|
||||
.build();
|
||||
|
||||
CallToolResult result = client.callTool(request);
|
||||
|
||||
assertThat(result).isNotNull();
|
||||
|
||||
try {
|
||||
// Wait for progress notifications to be processed
|
||||
latch.await(3, TimeUnit.SECONDS);
|
||||
}
|
||||
catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
assertThat(progressNotificationCount.get()).isEqualTo(2);
|
||||
|
||||
assertThat(receivedNotifications).isNotEmpty();
|
||||
assertThat(receivedNotifications.get(0).progressToken()).isEqualTo("test-token");
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue