Skip to content

Commit 820b75c

Browse files
committed
feat: add ping functionality to MCP server exchanges
- 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>
1 parent d9006ec commit 820b75c

File tree

7 files changed

+1040
-6
lines changed

7 files changed

+1040
-6
lines changed

mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/WebFluxSseIntegrationTests.java

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1023,4 +1023,61 @@ void testCompletionShouldReturnExpectedSuggestions(String clientType) {
10231023
mcpServer.close();
10241024
}
10251025

1026-
}
1026+
// ---------------------------------------
1027+
// Ping Tests
1028+
// ---------------------------------------
1029+
@ParameterizedTest(name = "{0} : {displayName} ")
1030+
@ValueSource(strings = { "httpclient", "webflux" })
1031+
void testPingSuccess(String clientType) {
1032+
var clientBuilder = clientBuilders.get(clientType);
1033+
1034+
// Create server with a tool that uses ping functionality
1035+
AtomicReference<String> executionOrder = new AtomicReference<>("");
1036+
1037+
McpServerFeatures.AsyncToolSpecification tool = new McpServerFeatures.AsyncToolSpecification(
1038+
new McpSchema.Tool("ping-async-test", "Test ping async behavior", emptyJsonSchema),
1039+
(exchange, request) -> {
1040+
1041+
executionOrder.set(executionOrder.get() + "1");
1042+
1043+
// Test async ping behavior
1044+
return exchange.ping().doOnNext(result -> {
1045+
1046+
assertThat(result).isNotNull();
1047+
// Ping should return an empty object or map
1048+
assertThat(result).isInstanceOf(Map.class);
1049+
1050+
executionOrder.set(executionOrder.get() + "2");
1051+
assertThat(result).isNotNull();
1052+
}).then(Mono.fromCallable(() -> {
1053+
executionOrder.set(executionOrder.get() + "3");
1054+
return new CallToolResult("Async ping test completed", false);
1055+
}));
1056+
});
1057+
1058+
var mcpServer = McpServer.async(mcpServerTransportProvider)
1059+
.serverInfo("test-server", "1.0.0")
1060+
.capabilities(ServerCapabilities.builder().tools(true).build())
1061+
.tools(tool)
1062+
.build();
1063+
1064+
try (var mcpClient = clientBuilder.build()) {
1065+
1066+
// Initialize client
1067+
InitializeResult initResult = mcpClient.initialize();
1068+
assertThat(initResult).isNotNull();
1069+
1070+
// Call the tool that tests ping async behavior
1071+
CallToolResult result = mcpClient.callTool(new McpSchema.CallToolRequest("ping-async-test", Map.of()));
1072+
assertThat(result).isNotNull();
1073+
assertThat(result.content().get(0)).isInstanceOf(McpSchema.TextContent.class);
1074+
assertThat(((McpSchema.TextContent) result.content().get(0)).text()).isEqualTo("Async ping test completed");
1075+
1076+
// Verify execution order
1077+
assertThat(executionOrder.get()).isEqualTo("123");
1078+
}
1079+
1080+
mcpServer.closeGracefully().block();
1081+
}
1082+
1083+
}

mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseIntegrationTests.java

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -862,4 +862,58 @@ void testInitialize() {
862862
mcpServer.close();
863863
}
864864

865+
// ---------------------------------------
866+
// Ping Tests
867+
// ---------------------------------------
868+
@Test
869+
void testPingSuccess() {
870+
// Create server with a tool that uses ping functionality
871+
AtomicReference<String> executionOrder = new AtomicReference<>("");
872+
873+
McpServerFeatures.AsyncToolSpecification tool = new McpServerFeatures.AsyncToolSpecification(
874+
new McpSchema.Tool("ping-async-test", "Test ping async behavior", emptyJsonSchema),
875+
(exchange, request) -> {
876+
877+
executionOrder.set(executionOrder.get() + "1");
878+
879+
// Test async ping behavior
880+
return exchange.ping().doOnNext(result -> {
881+
882+
assertThat(result).isNotNull();
883+
// Ping should return an empty object or map
884+
assertThat(result).isInstanceOf(Map.class);
885+
886+
executionOrder.set(executionOrder.get() + "2");
887+
assertThat(result).isNotNull();
888+
}).then(Mono.fromCallable(() -> {
889+
executionOrder.set(executionOrder.get() + "3");
890+
return new CallToolResult("Async ping test completed", false);
891+
}));
892+
});
893+
894+
var mcpServer = McpServer.async(mcpServerTransportProvider)
895+
.serverInfo("test-server", "1.0.0")
896+
.capabilities(ServerCapabilities.builder().tools(true).build())
897+
.tools(tool)
898+
.build();
899+
900+
try (var mcpClient = clientBuilder.build()) {
901+
902+
// Initialize client
903+
InitializeResult initResult = mcpClient.initialize();
904+
assertThat(initResult).isNotNull();
905+
906+
// Call the tool that tests ping async behavior
907+
CallToolResult result = mcpClient.callTool(new McpSchema.CallToolRequest("ping-async-test", Map.of()));
908+
assertThat(result).isNotNull();
909+
assertThat(result.content().get(0)).isInstanceOf(McpSchema.TextContent.class);
910+
assertThat(((McpSchema.TextContent) result.content().get(0)).text()).isEqualTo("Async ping test completed");
911+
912+
// Verify execution order
913+
assertThat(executionOrder.get()).isEqualTo("123");
914+
}
915+
916+
mcpServer.close();
917+
}
918+
865919
}

mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServerExchange.java

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ public class McpAsyncServerExchange {
4242
private static final TypeReference<McpSchema.ElicitResult> ELICITATION_RESULT_TYPE_REF = new TypeReference<>() {
4343
};
4444

45+
public static final TypeReference<Object> OBJECT_TYPE_REF = new TypeReference<>() {
46+
};
47+
4548
/**
4649
* Create a new asynchronous exchange with the client.
4750
* @param session The server session representing a 1-1 interaction.
@@ -132,9 +135,9 @@ public Mono<McpSchema.ListRootsResult> listRoots() {
132135

133136
// @formatter:off
134137
return this.listRoots(McpSchema.FIRST_PAGE)
135-
.expand(result -> (result.nextCursor() != null) ?
138+
.expand(result -> (result.nextCursor() != null) ?
136139
this.listRoots(result.nextCursor()) : Mono.empty())
137-
.reduce(new McpSchema.ListRootsResult(new ArrayList<>(), null),
140+
.reduce(new McpSchema.ListRootsResult(new ArrayList<>(), null),
138141
(allRootsResult, result) -> {
139142
allRootsResult.roots().addAll(result.roots());
140143
return allRootsResult;
@@ -174,6 +177,14 @@ public Mono<Void> loggingNotification(LoggingMessageNotification loggingMessageN
174177
});
175178
}
176179

180+
/**
181+
* Sends a ping request to the client.
182+
* @return A Mono that completes with clients's ping response
183+
*/
184+
public Mono<Object> ping() {
185+
return this.session.sendRequest(McpSchema.METHOD_PING, null, OBJECT_TYPE_REF);
186+
}
187+
177188
/**
178189
* Set the minimum logging level for the client. Messages below this level will be
179190
* filtered out.

mcp/src/main/java/io/modelcontextprotocol/server/McpSyncServerExchange.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
package io.modelcontextprotocol.server;
66

77
import io.modelcontextprotocol.spec.McpSchema;
8-
import io.modelcontextprotocol.spec.McpSchema.LoggingLevel;
98
import io.modelcontextprotocol.spec.McpSchema.LoggingMessageNotification;
109

1110
/**
@@ -108,4 +107,12 @@ public void loggingNotification(LoggingMessageNotification loggingMessageNotific
108107
this.exchange.loggingNotification(loggingMessageNotification).block();
109108
}
110109

110+
/**
111+
* Sends a ping request to the client.
112+
* @return A Mono that completes with clients's ping response
113+
*/
114+
public void ping() {
115+
this.exchange.ping().block();
116+
}
117+
111118
}

mcp/src/test/java/io/modelcontextprotocol/server/McpAsyncServerExchangeTests.java

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import java.util.ArrayList;
88
import java.util.Arrays;
99
import java.util.List;
10+
import java.util.Map;
1011

1112
import com.fasterxml.jackson.core.type.TypeReference;
1213
import io.modelcontextprotocol.spec.McpError;
@@ -740,4 +741,61 @@ void testCreateMessageWithIncludeContext() {
740741
}).verifyComplete();
741742
}
742743

744+
// ---------------------------------------
745+
// Ping Tests
746+
// ---------------------------------------
747+
748+
@Test
749+
void testPingWithSuccessfulResponse() {
750+
751+
java.util.Map<String, Object> expectedResponse = java.util.Map.of();
752+
753+
when(mockSession.sendRequest(eq(McpSchema.METHOD_PING), eq(null), any(TypeReference.class)))
754+
.thenReturn(Mono.just(expectedResponse));
755+
756+
StepVerifier.create(exchange.ping()).assertNext(result -> {
757+
assertThat(result).isEqualTo(expectedResponse);
758+
assertThat(result).isInstanceOf(java.util.Map.class);
759+
}).verifyComplete();
760+
761+
// Verify that sendRequest was called with correct parameters
762+
verify(mockSession, times(1)).sendRequest(eq(McpSchema.METHOD_PING), eq(null), any(TypeReference.class));
763+
}
764+
765+
@Test
766+
void testPingWithMcpError() {
767+
// Given - Mock an MCP-specific error during ping
768+
McpError mcpError = new McpError("Server unavailable");
769+
when(mockSession.sendRequest(eq(McpSchema.METHOD_PING), eq(null), any(TypeReference.class)))
770+
.thenReturn(Mono.error(mcpError));
771+
772+
// When & Then
773+
StepVerifier.create(exchange.ping()).verifyErrorSatisfies(error -> {
774+
assertThat(error).isInstanceOf(McpError.class).hasMessage("Server unavailable");
775+
});
776+
777+
verify(mockSession, times(1)).sendRequest(eq(McpSchema.METHOD_PING), eq(null), any(TypeReference.class));
778+
}
779+
780+
@Test
781+
void testPingMultipleCalls() {
782+
783+
when(mockSession.sendRequest(eq(McpSchema.METHOD_PING), eq(null), any(TypeReference.class)))
784+
.thenReturn(Mono.just(Map.of()))
785+
.thenReturn(Mono.just(Map.of()));
786+
787+
// First call
788+
StepVerifier.create(exchange.ping()).assertNext(result -> {
789+
assertThat(result).isInstanceOf(Map.class);
790+
}).verifyComplete();
791+
792+
// Second call
793+
StepVerifier.create(exchange.ping()).assertNext(result -> {
794+
assertThat(result).isInstanceOf(Map.class);
795+
}).verifyComplete();
796+
797+
// Verify that sendRequest was called twice
798+
verify(mockSession, times(2)).sendRequest(eq(McpSchema.METHOD_PING), eq(null), any(TypeReference.class));
799+
}
800+
743801
}

0 commit comments

Comments
 (0)