-
Notifications
You must be signed in to change notification settings - Fork 817
Description
Bug description
The streamable-http server closes the connection immediately after responding to the client's request. As a result, the server fails to send ping messages to the client. Since IDEs like Cursor do not receive the ping, they mark the MCP server as closed (red state) after the idle-timeout period.
Related Issue: This issue was initially reported in Spring AI project: spring-projects/spring-ai#4862
Root Cause: The issue is located in WebFluxStreamableServerTransportProvider.handlePost() method in the mcp-spring/mcp-spring-webflux module. While the initialize request is handled correctly (converting response to SSE stream and keeping connection open), other JSON-RPC requests (like tools/list) cause the connection to close immediately after sending the response, preventing keep-alive ping messages from being sent.
Environment
- MCP Java SDK version: 0.17.0-SNAPSHOT
- Spring AI version: 1.1.0
- Spring Framework version: 6.2.1
- Java version: 17+
- WebFlux: Yes
- Transport: streamable-http
Steps to reproduce
- Use the Spring AI example project: https://github.com/spring-projects/spring-ai-examples/tree/main/model-context-protocol/weather/starter-webflux-server
- Change protocol from STATELESS to STREAMABLE in
application.properties:spring.ai.mcp.server.protocol=STREAMABLE - Add the following settings to
application.properties:logging.level.root=DEBUG spring.ai.mcp.server.keep-alive-interval=30s spring.ai.mcp.server.streamable-http.keep-alive-interval=30s
- Start the webflux-mcp-server and wait for connection with MCP client
- Send "Initialize" and "tools/list" requests
- Monitor the logs
Expected behavior
The webflux-mcp-server should keep the connection open after sending a response to the client's request. It must only close the connection according to the logic defined in the KeepAliveScheduler. This allows keep-alive ping messages to be sent periodically to verify connection health.
Actual behavior
The connection is closed immediately after sending the response. Logs show:
2025-11-13T10:47:56.088+09:00 DEBUG ... r.n.http.server.HttpServerOperations : [f1db6cad-2, L:/[0:0:0:0:0:0:0:1]:8080 - R:/[0:0:0:0:0:0:0:1]:55851] Last HTTP packet was sent, terminating the channel
2025-11-13T10:47:56.089+09:00 DEBUG ... r.netty.channel.ChannelOperations : [HttpServer] Channel inbound receiver cancelled (subscription disposed).
2025-11-13T10:48:20.038+09:00 WARN ... i.m.util.KeepAliveScheduler : Failed to send keep-alive ping to session io.modelcontextprotocol.spec.McpStreamableServerSession@5fd8c0ef: Stream unavailable for session f7223451-6867-4b8c-8b46-e8ded21a7691
Minimal Complete Reproducible example
Use the Spring AI example project as the base:
- Repository: https://github.com/spring-projects/spring-ai-examples/tree/main/model-context-protocol/weather/starter-webflux-server
- Configure as described in "Steps to reproduce"
- The issue manifests when sending any JSON-RPC request after
initialize(e.g.,tools/list)
Technical Details
The problem is in mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/server/transport/WebFluxStreamableServerTransportProvider.java:
- Initialize request (lines 236-277): Correctly handled - converts response to SSE stream and keeps connection open by calling
listeningStream()(lines 267-268) - Other JSONRPCRequest (lines 298-312): Issue location
- Creates SSE stream but doesn't convert it to a listening stream like initialize does
- While
McpStreamableServerSession.responseStream()method (lines 211-229 inMcpStreamableServerSession.java) has logic to convert the response stream to a listening stream to keep connection open - The WebFlux response stream closes immediately after
responseStream()completes, preventing keep-alive ping from being sent
Suggested Fix
Ensure that when handling non-initialize requests in handlePost() method, the response stream remains open after sending the response, allowing KeepAliveScheduler to send ping messages through the stream. This can be achieved by following the same pattern used for initialize requests (lines 250-271), where the response stream is converted to a listening stream.
If you confirm this is a bug, I'm happy to fix it.