feat: add ping functionality to MCP server exchanges (#347)
- Add ping() method to McpAsyncServerExchange that sends ping requests to clients - Add ping() method to McpSyncServerExchange as synchronous wrapper - Add OBJECT_TYPE_REF constant for ping response type handling - Create McpSyncServerExchangeTests.java with full test coverage - Add ping integration tests across WebFlux, WebMVC, and HttpServlet transports Follow up of #203 Signed-off-by: Christian Tzolov <christian.tzolov@broadcom.com>
This commit is contained in:
parent
d9006ecb78
commit
d7bcb237b8
5 changed files with 1040 additions and 6 deletions
|
|
@ -42,6 +42,9 @@ public class McpAsyncServerExchange {
|
|||
private static final TypeReference<McpSchema.ElicitResult> ELICITATION_RESULT_TYPE_REF = new TypeReference<>() {
|
||||
};
|
||||
|
||||
public static final TypeReference<Object> OBJECT_TYPE_REF = new TypeReference<>() {
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a new asynchronous exchange with the client.
|
||||
* @param session The server session representing a 1-1 interaction.
|
||||
|
|
@ -132,9 +135,9 @@ public class McpAsyncServerExchange {
|
|||
|
||||
// @formatter:off
|
||||
return this.listRoots(McpSchema.FIRST_PAGE)
|
||||
.expand(result -> (result.nextCursor() != null) ?
|
||||
.expand(result -> (result.nextCursor() != null) ?
|
||||
this.listRoots(result.nextCursor()) : Mono.empty())
|
||||
.reduce(new McpSchema.ListRootsResult(new ArrayList<>(), null),
|
||||
.reduce(new McpSchema.ListRootsResult(new ArrayList<>(), null),
|
||||
(allRootsResult, result) -> {
|
||||
allRootsResult.roots().addAll(result.roots());
|
||||
return allRootsResult;
|
||||
|
|
@ -174,6 +177,14 @@ public class McpAsyncServerExchange {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a ping request to the client.
|
||||
* @return A Mono that completes with clients's ping response
|
||||
*/
|
||||
public Mono<Object> ping() {
|
||||
return this.session.sendRequest(McpSchema.METHOD_PING, null, OBJECT_TYPE_REF);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the minimum logging level for the client. Messages below this level will be
|
||||
* filtered out.
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@
|
|||
package io.modelcontextprotocol.server;
|
||||
|
||||
import io.modelcontextprotocol.spec.McpSchema;
|
||||
import io.modelcontextprotocol.spec.McpSchema.LoggingLevel;
|
||||
import io.modelcontextprotocol.spec.McpSchema.LoggingMessageNotification;
|
||||
|
||||
/**
|
||||
|
|
@ -108,4 +107,12 @@ public class McpSyncServerExchange {
|
|||
this.exchange.loggingNotification(loggingMessageNotification).block();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a ping request to the client.
|
||||
* @return A Mono that completes with clients's ping response
|
||||
*/
|
||||
public void ping() {
|
||||
this.exchange.ping().block();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ package io.modelcontextprotocol.server;
|
|||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import io.modelcontextprotocol.spec.McpError;
|
||||
|
|
@ -740,4 +741,61 @@ class McpAsyncServerExchangeTests {
|
|||
}).verifyComplete();
|
||||
}
|
||||
|
||||
// ---------------------------------------
|
||||
// Ping Tests
|
||||
// ---------------------------------------
|
||||
|
||||
@Test
|
||||
void testPingWithSuccessfulResponse() {
|
||||
|
||||
java.util.Map<String, Object> expectedResponse = java.util.Map.of();
|
||||
|
||||
when(mockSession.sendRequest(eq(McpSchema.METHOD_PING), eq(null), any(TypeReference.class)))
|
||||
.thenReturn(Mono.just(expectedResponse));
|
||||
|
||||
StepVerifier.create(exchange.ping()).assertNext(result -> {
|
||||
assertThat(result).isEqualTo(expectedResponse);
|
||||
assertThat(result).isInstanceOf(java.util.Map.class);
|
||||
}).verifyComplete();
|
||||
|
||||
// Verify that sendRequest was called with correct parameters
|
||||
verify(mockSession, times(1)).sendRequest(eq(McpSchema.METHOD_PING), eq(null), any(TypeReference.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPingWithMcpError() {
|
||||
// Given - Mock an MCP-specific error during ping
|
||||
McpError mcpError = new McpError("Server unavailable");
|
||||
when(mockSession.sendRequest(eq(McpSchema.METHOD_PING), eq(null), any(TypeReference.class)))
|
||||
.thenReturn(Mono.error(mcpError));
|
||||
|
||||
// When & Then
|
||||
StepVerifier.create(exchange.ping()).verifyErrorSatisfies(error -> {
|
||||
assertThat(error).isInstanceOf(McpError.class).hasMessage("Server unavailable");
|
||||
});
|
||||
|
||||
verify(mockSession, times(1)).sendRequest(eq(McpSchema.METHOD_PING), eq(null), any(TypeReference.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPingMultipleCalls() {
|
||||
|
||||
when(mockSession.sendRequest(eq(McpSchema.METHOD_PING), eq(null), any(TypeReference.class)))
|
||||
.thenReturn(Mono.just(Map.of()))
|
||||
.thenReturn(Mono.just(Map.of()));
|
||||
|
||||
// First call
|
||||
StepVerifier.create(exchange.ping()).assertNext(result -> {
|
||||
assertThat(result).isInstanceOf(Map.class);
|
||||
}).verifyComplete();
|
||||
|
||||
// Second call
|
||||
StepVerifier.create(exchange.ping()).assertNext(result -> {
|
||||
assertThat(result).isInstanceOf(Map.class);
|
||||
}).verifyComplete();
|
||||
|
||||
// Verify that sendRequest was called twice
|
||||
verify(mockSession, times(2)).sendRequest(eq(McpSchema.METHOD_PING), eq(null), any(TypeReference.class));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,793 @@
|
|||
/*
|
||||
* Copyright 2024-2024 the original author or authors.
|
||||
*/
|
||||
|
||||
package io.modelcontextprotocol.server;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import io.modelcontextprotocol.spec.McpError;
|
||||
import io.modelcontextprotocol.spec.McpSchema;
|
||||
import io.modelcontextprotocol.spec.McpServerSession;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.reset;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* Tests for {@link McpSyncServerExchange}.
|
||||
*
|
||||
* @author Christian Tzolov
|
||||
*/
|
||||
class McpSyncServerExchangeTests {
|
||||
|
||||
@Mock
|
||||
private McpServerSession mockSession;
|
||||
|
||||
private McpSchema.ClientCapabilities clientCapabilities;
|
||||
|
||||
private McpSchema.Implementation clientInfo;
|
||||
|
||||
private McpAsyncServerExchange asyncExchange;
|
||||
|
||||
private McpSyncServerExchange exchange;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
MockitoAnnotations.openMocks(this);
|
||||
|
||||
clientCapabilities = McpSchema.ClientCapabilities.builder().roots(true).build();
|
||||
|
||||
clientInfo = new McpSchema.Implementation("test-client", "1.0.0");
|
||||
|
||||
asyncExchange = new McpAsyncServerExchange(mockSession, clientCapabilities, clientInfo);
|
||||
exchange = new McpSyncServerExchange(asyncExchange);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testListRootsWithSinglePage() {
|
||||
|
||||
List<McpSchema.Root> roots = Arrays.asList(new McpSchema.Root("file:///home/user/project1", "Project 1"),
|
||||
new McpSchema.Root("file:///home/user/project2", "Project 2"));
|
||||
McpSchema.ListRootsResult singlePageResult = new McpSchema.ListRootsResult(roots, null);
|
||||
|
||||
when(mockSession.sendRequest(eq(McpSchema.METHOD_ROOTS_LIST), any(McpSchema.PaginatedRequest.class),
|
||||
any(TypeReference.class)))
|
||||
.thenReturn(Mono.just(singlePageResult));
|
||||
|
||||
McpSchema.ListRootsResult result = exchange.listRoots();
|
||||
|
||||
assertThat(result.roots()).hasSize(2);
|
||||
assertThat(result.roots().get(0).uri()).isEqualTo("file:///home/user/project1");
|
||||
assertThat(result.roots().get(0).name()).isEqualTo("Project 1");
|
||||
assertThat(result.roots().get(1).uri()).isEqualTo("file:///home/user/project2");
|
||||
assertThat(result.roots().get(1).name()).isEqualTo("Project 2");
|
||||
assertThat(result.nextCursor()).isNull();
|
||||
|
||||
// Verify that the returned list is unmodifiable
|
||||
assertThatThrownBy(() -> result.roots().add(new McpSchema.Root("file:///test", "Test")))
|
||||
.isInstanceOf(UnsupportedOperationException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testListRootsWithMultiplePages() {
|
||||
|
||||
List<McpSchema.Root> page1Roots = Arrays.asList(new McpSchema.Root("file:///home/user/project1", "Project 1"),
|
||||
new McpSchema.Root("file:///home/user/project2", "Project 2"));
|
||||
List<McpSchema.Root> page2Roots = Arrays.asList(new McpSchema.Root("file:///home/user/project3", "Project 3"));
|
||||
|
||||
McpSchema.ListRootsResult page1Result = new McpSchema.ListRootsResult(page1Roots, "cursor1");
|
||||
McpSchema.ListRootsResult page2Result = new McpSchema.ListRootsResult(page2Roots, null);
|
||||
|
||||
when(mockSession.sendRequest(eq(McpSchema.METHOD_ROOTS_LIST), eq(new McpSchema.PaginatedRequest(null)),
|
||||
any(TypeReference.class)))
|
||||
.thenReturn(Mono.just(page1Result));
|
||||
|
||||
when(mockSession.sendRequest(eq(McpSchema.METHOD_ROOTS_LIST), eq(new McpSchema.PaginatedRequest("cursor1")),
|
||||
any(TypeReference.class)))
|
||||
.thenReturn(Mono.just(page2Result));
|
||||
|
||||
McpSchema.ListRootsResult result = exchange.listRoots();
|
||||
|
||||
assertThat(result.roots()).hasSize(3);
|
||||
assertThat(result.roots().get(0).uri()).isEqualTo("file:///home/user/project1");
|
||||
assertThat(result.roots().get(1).uri()).isEqualTo("file:///home/user/project2");
|
||||
assertThat(result.roots().get(2).uri()).isEqualTo("file:///home/user/project3");
|
||||
assertThat(result.nextCursor()).isNull();
|
||||
|
||||
// Verify that the returned list is unmodifiable
|
||||
assertThatThrownBy(() -> result.roots().add(new McpSchema.Root("file:///test", "Test")))
|
||||
.isInstanceOf(UnsupportedOperationException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testListRootsWithEmptyResult() {
|
||||
|
||||
McpSchema.ListRootsResult emptyResult = new McpSchema.ListRootsResult(new ArrayList<>(), null);
|
||||
|
||||
when(mockSession.sendRequest(eq(McpSchema.METHOD_ROOTS_LIST), any(McpSchema.PaginatedRequest.class),
|
||||
any(TypeReference.class)))
|
||||
.thenReturn(Mono.just(emptyResult));
|
||||
|
||||
McpSchema.ListRootsResult result = exchange.listRoots();
|
||||
|
||||
assertThat(result.roots()).isEmpty();
|
||||
assertThat(result.nextCursor()).isNull();
|
||||
|
||||
// Verify that the returned list is unmodifiable
|
||||
assertThatThrownBy(() -> result.roots().add(new McpSchema.Root("file:///test", "Test")))
|
||||
.isInstanceOf(UnsupportedOperationException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testListRootsWithSpecificCursor() {
|
||||
|
||||
List<McpSchema.Root> roots = Arrays.asList(new McpSchema.Root("file:///home/user/project3", "Project 3"));
|
||||
McpSchema.ListRootsResult result = new McpSchema.ListRootsResult(roots, "nextCursor");
|
||||
|
||||
when(mockSession.sendRequest(eq(McpSchema.METHOD_ROOTS_LIST), eq(new McpSchema.PaginatedRequest("someCursor")),
|
||||
any(TypeReference.class)))
|
||||
.thenReturn(Mono.just(result));
|
||||
|
||||
McpSchema.ListRootsResult listResult = exchange.listRoots("someCursor");
|
||||
|
||||
assertThat(listResult.roots()).hasSize(1);
|
||||
assertThat(listResult.roots().get(0).uri()).isEqualTo("file:///home/user/project3");
|
||||
assertThat(listResult.nextCursor()).isEqualTo("nextCursor");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testListRootsWithError() {
|
||||
|
||||
when(mockSession.sendRequest(eq(McpSchema.METHOD_ROOTS_LIST), any(McpSchema.PaginatedRequest.class),
|
||||
any(TypeReference.class)))
|
||||
.thenReturn(Mono.error(new RuntimeException("Network error")));
|
||||
|
||||
// When & Then
|
||||
assertThatThrownBy(() -> exchange.listRoots()).isInstanceOf(RuntimeException.class).hasMessage("Network error");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testListRootsUnmodifiabilityAfterAccumulation() {
|
||||
|
||||
List<McpSchema.Root> page1Roots = new ArrayList<>(
|
||||
Arrays.asList(new McpSchema.Root("file:///home/user/project1", "Project 1")));
|
||||
List<McpSchema.Root> page2Roots = new ArrayList<>(
|
||||
Arrays.asList(new McpSchema.Root("file:///home/user/project2", "Project 2")));
|
||||
|
||||
McpSchema.ListRootsResult page1Result = new McpSchema.ListRootsResult(page1Roots, "cursor1");
|
||||
McpSchema.ListRootsResult page2Result = new McpSchema.ListRootsResult(page2Roots, null);
|
||||
|
||||
when(mockSession.sendRequest(eq(McpSchema.METHOD_ROOTS_LIST), eq(new McpSchema.PaginatedRequest(null)),
|
||||
any(TypeReference.class)))
|
||||
.thenReturn(Mono.just(page1Result));
|
||||
|
||||
when(mockSession.sendRequest(eq(McpSchema.METHOD_ROOTS_LIST), eq(new McpSchema.PaginatedRequest("cursor1")),
|
||||
any(TypeReference.class)))
|
||||
.thenReturn(Mono.just(page2Result));
|
||||
|
||||
McpSchema.ListRootsResult result = exchange.listRoots();
|
||||
|
||||
// Verify the accumulated result is correct
|
||||
assertThat(result.roots()).hasSize(2);
|
||||
|
||||
// Verify that the returned list is unmodifiable
|
||||
assertThatThrownBy(() -> result.roots().add(new McpSchema.Root("file:///test", "Test")))
|
||||
.isInstanceOf(UnsupportedOperationException.class);
|
||||
|
||||
// Verify that clear() also throws UnsupportedOperationException
|
||||
assertThatThrownBy(() -> result.roots().clear()).isInstanceOf(UnsupportedOperationException.class);
|
||||
|
||||
// Verify that remove() also throws UnsupportedOperationException
|
||||
assertThatThrownBy(() -> result.roots().remove(0)).isInstanceOf(UnsupportedOperationException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetClientCapabilities() {
|
||||
assertThat(exchange.getClientCapabilities()).isEqualTo(clientCapabilities);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetClientInfo() {
|
||||
assertThat(exchange.getClientInfo()).isEqualTo(clientInfo);
|
||||
}
|
||||
|
||||
// ---------------------------------------
|
||||
// Logging Notification Tests
|
||||
// ---------------------------------------
|
||||
|
||||
@Test
|
||||
void testLoggingNotificationWithNullMessage() {
|
||||
assertThatThrownBy(() -> exchange.loggingNotification(null)).isInstanceOf(McpError.class)
|
||||
.hasMessage("Logging message must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLoggingNotificationWithAllowedLevel() {
|
||||
|
||||
McpSchema.LoggingMessageNotification notification = McpSchema.LoggingMessageNotification.builder()
|
||||
.level(McpSchema.LoggingLevel.ERROR)
|
||||
.logger("test-logger")
|
||||
.data("Test error message")
|
||||
.build();
|
||||
|
||||
when(mockSession.sendNotification(eq(McpSchema.METHOD_NOTIFICATION_MESSAGE), eq(notification)))
|
||||
.thenReturn(Mono.empty());
|
||||
|
||||
exchange.loggingNotification(notification);
|
||||
|
||||
// Verify that sendNotification was called exactly once
|
||||
verify(mockSession, times(1)).sendNotification(eq(McpSchema.METHOD_NOTIFICATION_MESSAGE), eq(notification));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLoggingNotificationWithFilteredLevel() {
|
||||
// Given - Set minimum level to WARNING, send DEBUG message
|
||||
asyncExchange.setMinLoggingLevel(McpSchema.LoggingLevel.WARNING);
|
||||
|
||||
McpSchema.LoggingMessageNotification debugNotification = McpSchema.LoggingMessageNotification.builder()
|
||||
.level(McpSchema.LoggingLevel.DEBUG)
|
||||
.logger("test-logger")
|
||||
.data("Debug message that should be filtered")
|
||||
.build();
|
||||
|
||||
// When & Then - Should complete without sending notification
|
||||
exchange.loggingNotification(debugNotification);
|
||||
|
||||
// Verify that sendNotification was never called for filtered DEBUG level
|
||||
verify(mockSession, never()).sendNotification(eq(McpSchema.METHOD_NOTIFICATION_MESSAGE), eq(debugNotification));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLoggingNotificationLevelFiltering() {
|
||||
// Given - Set minimum level to WARNING
|
||||
asyncExchange.setMinLoggingLevel(McpSchema.LoggingLevel.WARNING);
|
||||
|
||||
// Test DEBUG (should be filtered)
|
||||
McpSchema.LoggingMessageNotification debugNotification = McpSchema.LoggingMessageNotification.builder()
|
||||
.level(McpSchema.LoggingLevel.DEBUG)
|
||||
.logger("test-logger")
|
||||
.data("Debug message")
|
||||
.build();
|
||||
|
||||
exchange.loggingNotification(debugNotification);
|
||||
|
||||
// Verify that sendNotification was never called for DEBUG level
|
||||
verify(mockSession, never()).sendNotification(eq(McpSchema.METHOD_NOTIFICATION_MESSAGE), eq(debugNotification));
|
||||
|
||||
// Test INFO (should be filtered)
|
||||
McpSchema.LoggingMessageNotification infoNotification = McpSchema.LoggingMessageNotification.builder()
|
||||
.level(McpSchema.LoggingLevel.INFO)
|
||||
.logger("test-logger")
|
||||
.data("Info message")
|
||||
.build();
|
||||
|
||||
exchange.loggingNotification(infoNotification);
|
||||
|
||||
// Verify that sendNotification was never called for INFO level
|
||||
verify(mockSession, never()).sendNotification(eq(McpSchema.METHOD_NOTIFICATION_MESSAGE), eq(infoNotification));
|
||||
|
||||
reset(mockSession);
|
||||
|
||||
// Test WARNING (should be sent)
|
||||
McpSchema.LoggingMessageNotification warningNotification = McpSchema.LoggingMessageNotification.builder()
|
||||
.level(McpSchema.LoggingLevel.WARNING)
|
||||
.logger("test-logger")
|
||||
.data("Warning message")
|
||||
.build();
|
||||
|
||||
when(mockSession.sendNotification(eq(McpSchema.METHOD_NOTIFICATION_MESSAGE), eq(warningNotification)))
|
||||
.thenReturn(Mono.empty());
|
||||
|
||||
exchange.loggingNotification(warningNotification);
|
||||
|
||||
// Verify that sendNotification was called exactly once for WARNING level
|
||||
verify(mockSession, times(1)).sendNotification(eq(McpSchema.METHOD_NOTIFICATION_MESSAGE),
|
||||
eq(warningNotification));
|
||||
|
||||
// Test ERROR (should be sent)
|
||||
McpSchema.LoggingMessageNotification errorNotification = McpSchema.LoggingMessageNotification.builder()
|
||||
.level(McpSchema.LoggingLevel.ERROR)
|
||||
.logger("test-logger")
|
||||
.data("Error message")
|
||||
.build();
|
||||
|
||||
when(mockSession.sendNotification(eq(McpSchema.METHOD_NOTIFICATION_MESSAGE), eq(errorNotification)))
|
||||
.thenReturn(Mono.empty());
|
||||
|
||||
exchange.loggingNotification(errorNotification);
|
||||
|
||||
// Verify that sendNotification was called exactly once for ERROR level
|
||||
verify(mockSession, times(1)).sendNotification(eq(McpSchema.METHOD_NOTIFICATION_MESSAGE),
|
||||
eq(errorNotification));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLoggingNotificationWithDefaultLevel() {
|
||||
|
||||
McpSchema.LoggingMessageNotification infoNotification = McpSchema.LoggingMessageNotification.builder()
|
||||
.level(McpSchema.LoggingLevel.INFO)
|
||||
.logger("test-logger")
|
||||
.data("Info message")
|
||||
.build();
|
||||
|
||||
when(mockSession.sendNotification(eq(McpSchema.METHOD_NOTIFICATION_MESSAGE), eq(infoNotification)))
|
||||
.thenReturn(Mono.empty());
|
||||
|
||||
exchange.loggingNotification(infoNotification);
|
||||
|
||||
// Verify that sendNotification was called exactly once for default level
|
||||
verify(mockSession, times(1)).sendNotification(eq(McpSchema.METHOD_NOTIFICATION_MESSAGE), eq(infoNotification));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLoggingNotificationWithSessionError() {
|
||||
|
||||
McpSchema.LoggingMessageNotification notification = McpSchema.LoggingMessageNotification.builder()
|
||||
.level(McpSchema.LoggingLevel.ERROR)
|
||||
.logger("test-logger")
|
||||
.data("Test error message")
|
||||
.build();
|
||||
|
||||
when(mockSession.sendNotification(eq(McpSchema.METHOD_NOTIFICATION_MESSAGE), eq(notification)))
|
||||
.thenReturn(Mono.error(new RuntimeException("Session error")));
|
||||
|
||||
assertThatThrownBy(() -> exchange.loggingNotification(notification)).isInstanceOf(RuntimeException.class)
|
||||
.hasMessage("Session error");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLoggingLevelHierarchy() {
|
||||
// Test all logging levels to ensure proper hierarchy
|
||||
McpSchema.LoggingLevel[] levels = { McpSchema.LoggingLevel.DEBUG, McpSchema.LoggingLevel.INFO,
|
||||
McpSchema.LoggingLevel.NOTICE, McpSchema.LoggingLevel.WARNING, McpSchema.LoggingLevel.ERROR,
|
||||
McpSchema.LoggingLevel.CRITICAL, McpSchema.LoggingLevel.ALERT, McpSchema.LoggingLevel.EMERGENCY };
|
||||
|
||||
// Set minimum level to WARNING
|
||||
asyncExchange.setMinLoggingLevel(McpSchema.LoggingLevel.WARNING);
|
||||
|
||||
for (McpSchema.LoggingLevel level : levels) {
|
||||
McpSchema.LoggingMessageNotification notification = McpSchema.LoggingMessageNotification.builder()
|
||||
.level(level)
|
||||
.logger("test-logger")
|
||||
.data("Test message for " + level)
|
||||
.build();
|
||||
|
||||
if (level.level() >= McpSchema.LoggingLevel.WARNING.level()) {
|
||||
// Should be sent
|
||||
when(mockSession.sendNotification(eq(McpSchema.METHOD_NOTIFICATION_MESSAGE), eq(notification)))
|
||||
.thenReturn(Mono.empty());
|
||||
|
||||
exchange.loggingNotification(notification);
|
||||
}
|
||||
else {
|
||||
// Should be filtered (completes without sending)
|
||||
exchange.loggingNotification(notification);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------
|
||||
// Create Elicitation Tests
|
||||
// ---------------------------------------
|
||||
|
||||
@Test
|
||||
void testCreateElicitationWithNullCapabilities() {
|
||||
// Given - Create exchange with null capabilities
|
||||
McpAsyncServerExchange asyncExchangeWithNullCapabilities = new McpAsyncServerExchange(mockSession, null,
|
||||
clientInfo);
|
||||
McpSyncServerExchange exchangeWithNullCapabilities = new McpSyncServerExchange(
|
||||
asyncExchangeWithNullCapabilities);
|
||||
|
||||
McpSchema.ElicitRequest elicitRequest = McpSchema.ElicitRequest.builder()
|
||||
.message("Please provide your name")
|
||||
.build();
|
||||
|
||||
assertThatThrownBy(() -> exchangeWithNullCapabilities.createElicitation(elicitRequest))
|
||||
.isInstanceOf(McpError.class)
|
||||
.hasMessage("Client must be initialized. Call the initialize method first!");
|
||||
|
||||
// Verify that sendRequest was never called due to null capabilities
|
||||
verify(mockSession, never()).sendRequest(eq(McpSchema.METHOD_ELICITATION_CREATE), any(),
|
||||
any(TypeReference.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreateElicitationWithoutElicitationCapabilities() {
|
||||
// Given - Create exchange without elicitation capabilities
|
||||
McpSchema.ClientCapabilities capabilitiesWithoutElicitation = McpSchema.ClientCapabilities.builder()
|
||||
.roots(true)
|
||||
.build();
|
||||
|
||||
McpAsyncServerExchange asyncExchangeWithoutElicitation = new McpAsyncServerExchange(mockSession,
|
||||
capabilitiesWithoutElicitation, clientInfo);
|
||||
McpSyncServerExchange exchangeWithoutElicitation = new McpSyncServerExchange(asyncExchangeWithoutElicitation);
|
||||
|
||||
McpSchema.ElicitRequest elicitRequest = McpSchema.ElicitRequest.builder()
|
||||
.message("Please provide your name")
|
||||
.build();
|
||||
|
||||
assertThatThrownBy(() -> exchangeWithoutElicitation.createElicitation(elicitRequest))
|
||||
.isInstanceOf(McpError.class)
|
||||
.hasMessage("Client must be configured with elicitation capabilities");
|
||||
|
||||
// Verify that sendRequest was never called due to missing elicitation
|
||||
// capabilities
|
||||
verify(mockSession, never()).sendRequest(eq(McpSchema.METHOD_ELICITATION_CREATE), any(),
|
||||
any(TypeReference.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreateElicitationWithComplexRequest() {
|
||||
|
||||
McpSchema.ClientCapabilities capabilitiesWithElicitation = McpSchema.ClientCapabilities.builder()
|
||||
.elicitation()
|
||||
.build();
|
||||
|
||||
McpAsyncServerExchange asyncExchangeWithElicitation = new McpAsyncServerExchange(mockSession,
|
||||
capabilitiesWithElicitation, clientInfo);
|
||||
McpSyncServerExchange exchangeWithElicitation = new McpSyncServerExchange(asyncExchangeWithElicitation);
|
||||
|
||||
// Create a complex elicit request with schema
|
||||
java.util.Map<String, Object> requestedSchema = new java.util.HashMap<>();
|
||||
requestedSchema.put("type", "object");
|
||||
requestedSchema.put("properties", java.util.Map.of("name", java.util.Map.of("type", "string"), "age",
|
||||
java.util.Map.of("type", "number")));
|
||||
requestedSchema.put("required", java.util.List.of("name"));
|
||||
|
||||
McpSchema.ElicitRequest elicitRequest = McpSchema.ElicitRequest.builder()
|
||||
.message("Please provide your personal information")
|
||||
.requestedSchema(requestedSchema)
|
||||
.build();
|
||||
|
||||
java.util.Map<String, Object> responseContent = new java.util.HashMap<>();
|
||||
responseContent.put("name", "John Doe");
|
||||
responseContent.put("age", 30);
|
||||
|
||||
McpSchema.ElicitResult expectedResult = McpSchema.ElicitResult.builder()
|
||||
.message(McpSchema.ElicitResult.Action.ACCEPT)
|
||||
.content(responseContent)
|
||||
.build();
|
||||
|
||||
when(mockSession.sendRequest(eq(McpSchema.METHOD_ELICITATION_CREATE), eq(elicitRequest),
|
||||
any(TypeReference.class)))
|
||||
.thenReturn(Mono.just(expectedResult));
|
||||
|
||||
McpSchema.ElicitResult result = exchangeWithElicitation.createElicitation(elicitRequest);
|
||||
|
||||
assertThat(result).isEqualTo(expectedResult);
|
||||
assertThat(result.action()).isEqualTo(McpSchema.ElicitResult.Action.ACCEPT);
|
||||
assertThat(result.content()).isNotNull();
|
||||
assertThat(result.content().get("name")).isEqualTo("John Doe");
|
||||
assertThat(result.content().get("age")).isEqualTo(30);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreateElicitationWithDeclineAction() {
|
||||
|
||||
McpSchema.ClientCapabilities capabilitiesWithElicitation = McpSchema.ClientCapabilities.builder()
|
||||
.elicitation()
|
||||
.build();
|
||||
|
||||
McpAsyncServerExchange asyncExchangeWithElicitation = new McpAsyncServerExchange(mockSession,
|
||||
capabilitiesWithElicitation, clientInfo);
|
||||
McpSyncServerExchange exchangeWithElicitation = new McpSyncServerExchange(asyncExchangeWithElicitation);
|
||||
|
||||
McpSchema.ElicitRequest elicitRequest = McpSchema.ElicitRequest.builder()
|
||||
.message("Please provide sensitive information")
|
||||
.build();
|
||||
|
||||
McpSchema.ElicitResult expectedResult = McpSchema.ElicitResult.builder()
|
||||
.message(McpSchema.ElicitResult.Action.DECLINE)
|
||||
.build();
|
||||
|
||||
when(mockSession.sendRequest(eq(McpSchema.METHOD_ELICITATION_CREATE), eq(elicitRequest),
|
||||
any(TypeReference.class)))
|
||||
.thenReturn(Mono.just(expectedResult));
|
||||
|
||||
McpSchema.ElicitResult result = exchangeWithElicitation.createElicitation(elicitRequest);
|
||||
|
||||
assertThat(result).isEqualTo(expectedResult);
|
||||
assertThat(result.action()).isEqualTo(McpSchema.ElicitResult.Action.DECLINE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreateElicitationWithCancelAction() {
|
||||
|
||||
McpSchema.ClientCapabilities capabilitiesWithElicitation = McpSchema.ClientCapabilities.builder()
|
||||
.elicitation()
|
||||
.build();
|
||||
|
||||
McpAsyncServerExchange asyncExchangeWithElicitation = new McpAsyncServerExchange(mockSession,
|
||||
capabilitiesWithElicitation, clientInfo);
|
||||
McpSyncServerExchange exchangeWithElicitation = new McpSyncServerExchange(asyncExchangeWithElicitation);
|
||||
|
||||
McpSchema.ElicitRequest elicitRequest = McpSchema.ElicitRequest.builder()
|
||||
.message("Please provide your information")
|
||||
.build();
|
||||
|
||||
McpSchema.ElicitResult expectedResult = McpSchema.ElicitResult.builder()
|
||||
.message(McpSchema.ElicitResult.Action.CANCEL)
|
||||
.build();
|
||||
|
||||
when(mockSession.sendRequest(eq(McpSchema.METHOD_ELICITATION_CREATE), eq(elicitRequest),
|
||||
any(TypeReference.class)))
|
||||
.thenReturn(Mono.just(expectedResult));
|
||||
|
||||
McpSchema.ElicitResult result = exchangeWithElicitation.createElicitation(elicitRequest);
|
||||
|
||||
assertThat(result).isEqualTo(expectedResult);
|
||||
assertThat(result.action()).isEqualTo(McpSchema.ElicitResult.Action.CANCEL);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreateElicitationWithSessionError() {
|
||||
|
||||
McpSchema.ClientCapabilities capabilitiesWithElicitation = McpSchema.ClientCapabilities.builder()
|
||||
.elicitation()
|
||||
.build();
|
||||
|
||||
McpAsyncServerExchange asyncExchangeWithElicitation = new McpAsyncServerExchange(mockSession,
|
||||
capabilitiesWithElicitation, clientInfo);
|
||||
McpSyncServerExchange exchangeWithElicitation = new McpSyncServerExchange(asyncExchangeWithElicitation);
|
||||
|
||||
McpSchema.ElicitRequest elicitRequest = McpSchema.ElicitRequest.builder()
|
||||
.message("Please provide your name")
|
||||
.build();
|
||||
|
||||
when(mockSession.sendRequest(eq(McpSchema.METHOD_ELICITATION_CREATE), eq(elicitRequest),
|
||||
any(TypeReference.class)))
|
||||
.thenReturn(Mono.error(new RuntimeException("Session communication error")));
|
||||
|
||||
assertThatThrownBy(() -> exchangeWithElicitation.createElicitation(elicitRequest))
|
||||
.isInstanceOf(RuntimeException.class)
|
||||
.hasMessage("Session communication error");
|
||||
}
|
||||
|
||||
// ---------------------------------------
|
||||
// Create Message Tests
|
||||
// ---------------------------------------
|
||||
|
||||
@Test
|
||||
void testCreateMessageWithNullCapabilities() {
|
||||
|
||||
McpAsyncServerExchange asyncExchangeWithNullCapabilities = new McpAsyncServerExchange(mockSession, null,
|
||||
clientInfo);
|
||||
McpSyncServerExchange exchangeWithNullCapabilities = new McpSyncServerExchange(
|
||||
asyncExchangeWithNullCapabilities);
|
||||
|
||||
McpSchema.CreateMessageRequest createMessageRequest = McpSchema.CreateMessageRequest.builder()
|
||||
.messages(Arrays
|
||||
.asList(new McpSchema.SamplingMessage(McpSchema.Role.USER, new McpSchema.TextContent("Hello, world!"))))
|
||||
.build();
|
||||
|
||||
assertThatThrownBy(() -> exchangeWithNullCapabilities.createMessage(createMessageRequest))
|
||||
.isInstanceOf(McpError.class)
|
||||
.hasMessage("Client must be initialized. Call the initialize method first!");
|
||||
|
||||
// Verify that sendRequest was never called due to null capabilities
|
||||
verify(mockSession, never()).sendRequest(eq(McpSchema.METHOD_SAMPLING_CREATE_MESSAGE), any(),
|
||||
any(TypeReference.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreateMessageWithoutSamplingCapabilities() {
|
||||
|
||||
McpSchema.ClientCapabilities capabilitiesWithoutSampling = McpSchema.ClientCapabilities.builder()
|
||||
.roots(true)
|
||||
.build();
|
||||
|
||||
McpAsyncServerExchange asyncExchangeWithoutSampling = new McpAsyncServerExchange(mockSession,
|
||||
capabilitiesWithoutSampling, clientInfo);
|
||||
McpSyncServerExchange exchangeWithoutSampling = new McpSyncServerExchange(asyncExchangeWithoutSampling);
|
||||
|
||||
McpSchema.CreateMessageRequest createMessageRequest = McpSchema.CreateMessageRequest.builder()
|
||||
.messages(Arrays
|
||||
.asList(new McpSchema.SamplingMessage(McpSchema.Role.USER, new McpSchema.TextContent("Hello, world!"))))
|
||||
.build();
|
||||
|
||||
assertThatThrownBy(() -> exchangeWithoutSampling.createMessage(createMessageRequest))
|
||||
.isInstanceOf(McpError.class)
|
||||
.hasMessage("Client must be configured with sampling capabilities");
|
||||
|
||||
// Verify that sendRequest was never called due to missing sampling capabilities
|
||||
verify(mockSession, never()).sendRequest(eq(McpSchema.METHOD_SAMPLING_CREATE_MESSAGE), any(),
|
||||
any(TypeReference.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreateMessageWithBasicRequest() {
|
||||
|
||||
McpSchema.ClientCapabilities capabilitiesWithSampling = McpSchema.ClientCapabilities.builder()
|
||||
.sampling()
|
||||
.build();
|
||||
|
||||
McpAsyncServerExchange asyncExchangeWithSampling = new McpAsyncServerExchange(mockSession,
|
||||
capabilitiesWithSampling, clientInfo);
|
||||
McpSyncServerExchange exchangeWithSampling = new McpSyncServerExchange(asyncExchangeWithSampling);
|
||||
|
||||
McpSchema.CreateMessageRequest createMessageRequest = McpSchema.CreateMessageRequest.builder()
|
||||
.messages(Arrays
|
||||
.asList(new McpSchema.SamplingMessage(McpSchema.Role.USER, new McpSchema.TextContent("Hello, world!"))))
|
||||
.build();
|
||||
|
||||
McpSchema.CreateMessageResult expectedResult = McpSchema.CreateMessageResult.builder()
|
||||
.role(McpSchema.Role.ASSISTANT)
|
||||
.content(new McpSchema.TextContent("Hello! How can I help you today?"))
|
||||
.model("gpt-4")
|
||||
.stopReason(McpSchema.CreateMessageResult.StopReason.END_TURN)
|
||||
.build();
|
||||
|
||||
when(mockSession.sendRequest(eq(McpSchema.METHOD_SAMPLING_CREATE_MESSAGE), eq(createMessageRequest),
|
||||
any(TypeReference.class)))
|
||||
.thenReturn(Mono.just(expectedResult));
|
||||
|
||||
McpSchema.CreateMessageResult result = exchangeWithSampling.createMessage(createMessageRequest);
|
||||
|
||||
assertThat(result).isEqualTo(expectedResult);
|
||||
assertThat(result.role()).isEqualTo(McpSchema.Role.ASSISTANT);
|
||||
assertThat(result.content()).isInstanceOf(McpSchema.TextContent.class);
|
||||
assertThat(((McpSchema.TextContent) result.content()).text()).isEqualTo("Hello! How can I help you today?");
|
||||
assertThat(result.model()).isEqualTo("gpt-4");
|
||||
assertThat(result.stopReason()).isEqualTo(McpSchema.CreateMessageResult.StopReason.END_TURN);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreateMessageWithImageContent() {
|
||||
|
||||
McpSchema.ClientCapabilities capabilitiesWithSampling = McpSchema.ClientCapabilities.builder()
|
||||
.sampling()
|
||||
.build();
|
||||
|
||||
McpAsyncServerExchange asyncExchangeWithSampling = new McpAsyncServerExchange(mockSession,
|
||||
capabilitiesWithSampling, clientInfo);
|
||||
McpSyncServerExchange exchangeWithSampling = new McpSyncServerExchange(asyncExchangeWithSampling);
|
||||
|
||||
// Create request with image content
|
||||
McpSchema.CreateMessageRequest createMessageRequest = McpSchema.CreateMessageRequest.builder()
|
||||
.messages(Arrays.asList(new McpSchema.SamplingMessage(McpSchema.Role.USER,
|
||||
new McpSchema.ImageContent(null, "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD...",
|
||||
"image/jpeg"))))
|
||||
.build();
|
||||
|
||||
McpSchema.CreateMessageResult expectedResult = McpSchema.CreateMessageResult.builder()
|
||||
.role(McpSchema.Role.ASSISTANT)
|
||||
.content(new McpSchema.TextContent("I can see an image. It appears to be a photograph."))
|
||||
.model("gpt-4-vision")
|
||||
.stopReason(McpSchema.CreateMessageResult.StopReason.END_TURN)
|
||||
.build();
|
||||
|
||||
when(mockSession.sendRequest(eq(McpSchema.METHOD_SAMPLING_CREATE_MESSAGE), eq(createMessageRequest),
|
||||
any(TypeReference.class)))
|
||||
.thenReturn(Mono.just(expectedResult));
|
||||
|
||||
McpSchema.CreateMessageResult result = exchangeWithSampling.createMessage(createMessageRequest);
|
||||
|
||||
assertThat(result).isEqualTo(expectedResult);
|
||||
assertThat(result.role()).isEqualTo(McpSchema.Role.ASSISTANT);
|
||||
assertThat(result.model()).isEqualTo("gpt-4-vision");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreateMessageWithSessionError() {
|
||||
|
||||
McpSchema.ClientCapabilities capabilitiesWithSampling = McpSchema.ClientCapabilities.builder()
|
||||
.sampling()
|
||||
.build();
|
||||
|
||||
McpAsyncServerExchange asyncExchangeWithSampling = new McpAsyncServerExchange(mockSession,
|
||||
capabilitiesWithSampling, clientInfo);
|
||||
McpSyncServerExchange exchangeWithSampling = new McpSyncServerExchange(asyncExchangeWithSampling);
|
||||
|
||||
McpSchema.CreateMessageRequest createMessageRequest = McpSchema.CreateMessageRequest.builder()
|
||||
.messages(Arrays
|
||||
.asList(new McpSchema.SamplingMessage(McpSchema.Role.USER, new McpSchema.TextContent("Hello"))))
|
||||
.build();
|
||||
|
||||
when(mockSession.sendRequest(eq(McpSchema.METHOD_SAMPLING_CREATE_MESSAGE), eq(createMessageRequest),
|
||||
any(TypeReference.class)))
|
||||
.thenReturn(Mono.error(new RuntimeException("Session communication error")));
|
||||
|
||||
assertThatThrownBy(() -> exchangeWithSampling.createMessage(createMessageRequest))
|
||||
.isInstanceOf(RuntimeException.class)
|
||||
.hasMessage("Session communication error");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreateMessageWithIncludeContext() {
|
||||
|
||||
McpSchema.ClientCapabilities capabilitiesWithSampling = McpSchema.ClientCapabilities.builder()
|
||||
.sampling()
|
||||
.build();
|
||||
|
||||
McpAsyncServerExchange asyncExchangeWithSampling = new McpAsyncServerExchange(mockSession,
|
||||
capabilitiesWithSampling, clientInfo);
|
||||
McpSyncServerExchange exchangeWithSampling = new McpSyncServerExchange(asyncExchangeWithSampling);
|
||||
|
||||
McpSchema.CreateMessageRequest createMessageRequest = McpSchema.CreateMessageRequest.builder()
|
||||
.messages(Arrays.asList(new McpSchema.SamplingMessage(McpSchema.Role.USER,
|
||||
new McpSchema.TextContent("What files are available?"))))
|
||||
.includeContext(McpSchema.CreateMessageRequest.ContextInclusionStrategy.ALL_SERVERS)
|
||||
.build();
|
||||
|
||||
McpSchema.CreateMessageResult expectedResult = McpSchema.CreateMessageResult.builder()
|
||||
.role(McpSchema.Role.ASSISTANT)
|
||||
.content(new McpSchema.TextContent("Based on the available context, I can see several files..."))
|
||||
.model("gpt-4")
|
||||
.stopReason(McpSchema.CreateMessageResult.StopReason.END_TURN)
|
||||
.build();
|
||||
|
||||
when(mockSession.sendRequest(eq(McpSchema.METHOD_SAMPLING_CREATE_MESSAGE), eq(createMessageRequest),
|
||||
any(TypeReference.class)))
|
||||
.thenReturn(Mono.just(expectedResult));
|
||||
|
||||
McpSchema.CreateMessageResult result = exchangeWithSampling.createMessage(createMessageRequest);
|
||||
|
||||
assertThat(result).isEqualTo(expectedResult);
|
||||
assertThat(((McpSchema.TextContent) result.content()).text()).contains("context");
|
||||
}
|
||||
|
||||
// ---------------------------------------
|
||||
// Ping Tests
|
||||
// ---------------------------------------
|
||||
|
||||
@Test
|
||||
void testPingWithSuccessfulResponse() {
|
||||
|
||||
java.util.Map<String, Object> expectedResponse = java.util.Map.of();
|
||||
|
||||
when(mockSession.sendRequest(eq(McpSchema.METHOD_PING), eq(null), any(TypeReference.class)))
|
||||
.thenReturn(Mono.just(expectedResponse));
|
||||
|
||||
exchange.ping();
|
||||
|
||||
// Verify that sendRequest was called with correct parameters
|
||||
verify(mockSession, times(1)).sendRequest(eq(McpSchema.METHOD_PING), eq(null), any(TypeReference.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPingWithMcpError() {
|
||||
// Given - Mock an MCP-specific error during ping
|
||||
McpError mcpError = new McpError("Server unavailable");
|
||||
when(mockSession.sendRequest(eq(McpSchema.METHOD_PING), eq(null), any(TypeReference.class)))
|
||||
.thenReturn(Mono.error(mcpError));
|
||||
|
||||
// When & Then
|
||||
assertThatThrownBy(() -> exchange.ping()).isInstanceOf(McpError.class).hasMessage("Server unavailable");
|
||||
|
||||
verify(mockSession, times(1)).sendRequest(eq(McpSchema.METHOD_PING), eq(null), any(TypeReference.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPingMultipleCalls() {
|
||||
|
||||
when(mockSession.sendRequest(eq(McpSchema.METHOD_PING), eq(null), any(TypeReference.class)))
|
||||
.thenReturn(Mono.just(Map.of()))
|
||||
.thenReturn(Mono.just(Map.of()));
|
||||
|
||||
// First call
|
||||
exchange.ping();
|
||||
|
||||
// Second call
|
||||
exchange.ping();
|
||||
|
||||
// Verify that sendRequest was called twice
|
||||
verify(mockSession, times(2)).sendRequest(eq(McpSchema.METHOD_PING), eq(null), any(TypeReference.class));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -106,7 +106,7 @@ class HttpServletSseServerTransportProviderIntegrationTests {
|
|||
// Sampling Tests
|
||||
// ---------------------------------------
|
||||
@Test
|
||||
@Disabled
|
||||
// @Disabled
|
||||
void testCreateMessageWithoutSamplingCapabilities() {
|
||||
|
||||
McpServerFeatures.AsyncToolSpecification tool = new McpServerFeatures.AsyncToolSpecification(
|
||||
|
|
@ -345,7 +345,7 @@ class HttpServletSseServerTransportProviderIntegrationTests {
|
|||
// Elicitation Tests
|
||||
// ---------------------------------------
|
||||
@Test
|
||||
@Disabled
|
||||
// @Disabled
|
||||
void testCreateElicitationWithoutElicitationCapabilities() {
|
||||
|
||||
McpServerFeatures.AsyncToolSpecification tool = new McpServerFeatures.AsyncToolSpecification(
|
||||
|
|
@ -958,4 +958,58 @@ class HttpServletSseServerTransportProviderIntegrationTests {
|
|||
mcpServer.close();
|
||||
}
|
||||
|
||||
// ---------------------------------------
|
||||
// Ping Tests
|
||||
// ---------------------------------------
|
||||
@Test
|
||||
void testPingSuccess() {
|
||||
// Create server with a tool that uses ping functionality
|
||||
AtomicReference<String> executionOrder = new AtomicReference<>("");
|
||||
|
||||
McpServerFeatures.AsyncToolSpecification tool = new McpServerFeatures.AsyncToolSpecification(
|
||||
new McpSchema.Tool("ping-async-test", "Test ping async behavior", emptyJsonSchema),
|
||||
(exchange, request) -> {
|
||||
|
||||
executionOrder.set(executionOrder.get() + "1");
|
||||
|
||||
// Test async ping behavior
|
||||
return exchange.ping().doOnNext(result -> {
|
||||
|
||||
assertThat(result).isNotNull();
|
||||
// Ping should return an empty object or map
|
||||
assertThat(result).isInstanceOf(Map.class);
|
||||
|
||||
executionOrder.set(executionOrder.get() + "2");
|
||||
assertThat(result).isNotNull();
|
||||
}).then(Mono.fromCallable(() -> {
|
||||
executionOrder.set(executionOrder.get() + "3");
|
||||
return new CallToolResult("Async ping test completed", false);
|
||||
}));
|
||||
});
|
||||
|
||||
var mcpServer = McpServer.async(mcpServerTransportProvider)
|
||||
.serverInfo("test-server", "1.0.0")
|
||||
.capabilities(ServerCapabilities.builder().tools(true).build())
|
||||
.tools(tool)
|
||||
.build();
|
||||
|
||||
try (var mcpClient = clientBuilder.build()) {
|
||||
|
||||
// Initialize client
|
||||
InitializeResult initResult = mcpClient.initialize();
|
||||
assertThat(initResult).isNotNull();
|
||||
|
||||
// Call the tool that tests ping async behavior
|
||||
CallToolResult result = mcpClient.callTool(new McpSchema.CallToolRequest("ping-async-test", Map.of()));
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result.content().get(0)).isInstanceOf(McpSchema.TextContent.class);
|
||||
assertThat(((McpSchema.TextContent) result.content().get(0)).text()).isEqualTo("Async ping test completed");
|
||||
|
||||
// Verify execution order
|
||||
assertThat(executionOrder.get()).isEqualTo("123");
|
||||
}
|
||||
|
||||
mcpServer.close();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue