Skip to content

Commit e82fee0

Browse files
committed
fix: keep connection open for keep-alive ping in responseStream
After sending a response in responseStream(), if no listening stream exists yet (i.e., the client hasn't established a GET SSE connection), promote the current response stream to a listening stream instead of closing it. This allows KeepAliveScheduler to send periodic ping messages through the transport. Clients like Cursor that don't establish a separate GET listening stream would otherwise have the connection closed immediately after each response, causing the MCP server to appear as disconnected after the idle-timeout period. Fixes #681
1 parent 46bacda commit e82fee0

File tree

1 file changed

+26
-2
lines changed

1 file changed

+26
-2
lines changed

mcp-core/src/main/java/io/modelcontextprotocol/spec/McpStreamableServerSession.java

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,8 @@ public Mono<Void> responseStream(McpSchema.JSONRPCRequest jsonrpcRequest, McpStr
171171
return transport
172172
.sendMessage(new McpSchema.JSONRPCResponse(McpSchema.JSONRPC_VERSION, jsonrpcRequest.id(), null,
173173
new McpSchema.JSONRPCResponse.JSONRPCError(McpSchema.ErrorCodes.METHOD_NOT_FOUND,
174-
error.message(), error.data())));
174+
error.message(), error.data())))
175+
.then(promoteToListeningStreamOrClose(stream, transport));
175176
}
176177
return requestHandler
177178
.handle(new McpAsyncServerExchange(this.id, stream, clientCapabilities.get(), clientInfo.get(),
@@ -189,7 +190,30 @@ public Mono<Void> responseStream(McpSchema.JSONRPCRequest jsonrpcRequest, McpStr
189190
return Mono.just(errorResponse);
190191
})
191192
.flatMap(transport::sendMessage)
192-
.then(transport.closeGracefully());
193+
.then(promoteToListeningStreamOrClose(stream, transport));
194+
});
195+
}
196+
197+
/**
198+
* Promotes the given response stream to the session's listening stream if no
199+
* listening stream has been established yet. If a listening stream already exists,
200+
* closes the transport gracefully. This allows clients that only use POST (without a
201+
* separate GET SSE stream) to keep the connection alive for keep-alive pings.
202+
* @param stream the response stream to potentially promote
203+
* @param transport the transport to close if promotion is not needed
204+
* @return Mono that completes after either promoting or closing
205+
*/
206+
private Mono<Void> promoteToListeningStreamOrClose(McpStreamableServerSessionStream stream,
207+
McpStreamableServerTransport transport) {
208+
return Mono.defer(() -> {
209+
McpLoggableSession currentListeningStream = this.listeningStreamRef.get();
210+
if (currentListeningStream == this.missingMcpTransportSession) {
211+
if (this.listeningStreamRef.compareAndSet(this.missingMcpTransportSession, stream)) {
212+
logger.debug("Converted response stream to listening stream for session {}", this.id);
213+
return Mono.empty();
214+
}
215+
}
216+
return transport.closeGracefully();
193217
});
194218
}
195219

0 commit comments

Comments
 (0)