KeepAliveScheduler only pings sessions with an open stream

Signed-off-by: Daniel Garnier-Moiroux <git@garnier.wf>
This commit is contained in:
Daniel Garnier-Moiroux 2026-09-08 17:56:05 +02:00 • committed by Daniel Garnier-Moiroux
commit ca633db18c
2 changed files with 75 additions and 3 deletions

View file

@ -20,6 +20,7 @@ import io.modelcontextprotocol.server.McpTransportContextExtractor;
import io.modelcontextprotocol.spec.HttpHeaders;
import io.modelcontextprotocol.spec.McpError;
import io.modelcontextprotocol.spec.McpSchema;
import io.modelcontextprotocol.spec.McpSession;
import io.modelcontextprotocol.spec.McpStreamableServerSession;
import io.modelcontextprotocol.spec.McpStreamableServerTransport;
import io.modelcontextprotocol.spec.McpStreamableServerTransportProvider;
@ -165,8 +166,7 @@ public class HttpServletStreamableServerTransportProvider extends HttpServlet
if (keepAliveInterval != null) {
this.keepAliveScheduler = KeepAliveScheduler
.builder(() -> (isClosing) ? Flux.empty() : Flux.fromIterable(sessions.values()))
this.keepAliveScheduler = KeepAliveScheduler.builder(this::sessionsToPing)
.initialDelay(keepAliveInterval)
.interval(keepAliveInterval)
.build();
@ -176,6 +176,22 @@ public class HttpServletStreamableServerTransportProvider extends HttpServlet
}
/**
* Returns the sessions a keep-alive ping can be sent to, that is the sessions having
* a listening stream. A session without one, e.g. a client which only ever issues
* POST requests, has nothing to write a ping to: pinging it would fail on every
* interval without ever telling us anything about the client being alive.
* @return the sessions to ping
*/
private Flux<McpSession> sessionsToPing() {
if (this.isClosing) {
return Flux.empty();
}
return Flux.fromIterable(this.sessions.values())
.filter(McpStreamableServerSession::hasListeningStream)
.cast(McpSession.class);
}
@Override
public void setSessionFactory(McpStreamableServerSession.Factory sessionFactory) {
this.sessionFactory = sessionFactory;

View file

@ -178,6 +178,17 @@ public class McpStreamableServerSession implements McpLoggableSession {
}));
}
/**
* Whether the session currently has a listening stream, that is a stream the server
* can send its own requests and notifications to. Sessions have none until the client
* issues the GET request establishing one, and clients are not required to ever issue
* it.
* @return {@code true} if the session has a listening stream
*/
public boolean hasListeningStream() {
return this.listeningStreamRef.get() instanceof McpStreamableServerSessionStream;
}
/**
* Create a listening stream (the generic HTTP GET request, with or without a
* Last-Event-ID header). A session addresses a single listening stream at a time, so