Skip to content

feat: add ping functionality to MCP server exchanges #347

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1023,4 +1023,61 @@ void testCompletionShouldReturnExpectedSuggestions(String clientType) {
mcpServer.close();
}

}
// ---------------------------------------
// Ping Tests
// ---------------------------------------
@ParameterizedTest(name = "{0} : {displayName} ")
@ValueSource(strings = { "httpclient", "webflux" })
void testPingSuccess(String clientType) {
var clientBuilder = clientBuilders.get(clientType);

// 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.closeGracefully().block();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -862,4 +862,58 @@ void testInitialize() {
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();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -132,9 +135,9 @@ public Mono<McpSchema.ListRootsResult> listRoots() {

// @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;
Expand Down Expand Up @@ -174,6 +177,14 @@ public Mono<Void> loggingNotification(LoggingMessageNotification loggingMessageN
});
}

/**
* 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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down Expand Up @@ -108,4 +107,12 @@ public void loggingNotification(LoggingMessageNotification loggingMessageNotific
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();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
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;
Expand Down Expand Up @@ -740,4 +741,61 @@ void testCreateMessageWithIncludeContext() {
}).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));
}

}
Loading
Loading