spring-projects/spring-ai · error · McpTransportSessionClosedException
McpTransportSessionClosedException
Error message
McpTransportSessionClosedException
What it means
WebClientStreamableHttpTransport.reconnect (called from connect and the connection loop) checks the currently active transport session before re-establishing an SSE stream. If the session reference is ClosedMcpTransportSession.INSTANCE — the session was already closed — reconnect cannot proceed and throws McpTransportSessionClosedException. This signals the transport must be fully reconnected rather than resuming an existing session.
Source
Thrown at mcp/transport/mcp-spring-webflux/src/main/java/org/springframework/ai/mcp/client/webflux/transport/WebClientStreamableHttpTransport.java:240
}
return Mono.empty();
});
}
private Mono<Disposable> reconnect(@Nullable McpTransportStream<Disposable> stream) {
return Mono.deferContextual(ctx -> {
var rh = this.handler.get();
if (rh == null) {
logger.warn("Transport has no request handler registered. Remember to call connect!");
}
final Function<Mono<McpSchema.JSONRPCMessage>, Mono<McpSchema.JSONRPCMessage>> requestHandler = rh != null
? rh : msg -> Mono.error(new IllegalStateException("No request handler"));
final McpTransportSession<Disposable> transportSession = this.activeSession.get();
if (ClosedMcpTransportSession.INSTANCE.equals(transportSession)) {
throw new McpTransportSessionClosedException();
}
if (stream != null) {
if (logger.isDebugEnabled()) {
logger.debug("Reconnecting stream " + stream.streamId() + " with lastId " + stream.lastId());
}
}
else {
logger.debug("Reconnecting with no prior stream");
}
// Here we attempt to initialize the client. In case the server supports SSE,
// we will establish a long-running
// session here and listen for messages. If it doesn't, that's ok, the server
// is a simple, stateless one.
final AtomicReference<@Nullable Disposable> disposableRef = new AtomicReference<>();
Disposable connection = this.webClient.get()
.uri(this.endpoint)
.accept(MediaType.TEXT_EVENT_STREAM)View on GitHub (pinned to 98a7beda4f)
Solutions
- Catch McpTransportSessionClosedException and re-initialize the transport/session from scratch (call connect() again, or recreate the McpClient so a new session is negotiated).
- Check server logs for 404/session-expired responses on the streamable endpoint; ensure the server keeps sessions alive or supports resumption.
- Upgrade spring-ai mcp transport versions — reconnect/resume handling of closed sessions has been hardened over time.
Example fix
// before: assume reconnect always resumes
transport.reconnect(...);
// after
try {
transport.reconnect(...);
} catch (McpTransportSessionClosedException e) {
mcpClient.close(); // discard closed session
mcpClient.initialize(); // negotiate a fresh session
} Defensive patterns
Strategy: retry
Validate before calling
// Before reconnecting, confirm the session is still usable
if (ClosedMcpTransportSession.INSTANCE.equals(activeSession.get())) {
// full re-initialization required, not a resume
} Try / catch
try {
transport.reconnect(stream);
} catch (McpTransportSessionClosedException e) {
client.close();
client = McpClient.sync(httpTransport).build();
client.initialize();
} Prevention
- Handle McpTransportSessionClosedException as a signal to re-initialize, not retry blindly.
- Configure keep-alives / server session timeouts to reduce premature session closure.
- Monitor for HTTP 404/session-expired responses on the streamable endpoint.
When it happens
Trigger: The streamable HTTP transport tries to reconnect a GET/SSE stream after the session was closed (server terminated session, McpTransportSessionClosedException on disconnect, or explicit close), and reconnect observes activeSession == ClosedMcpTransportSession.INSTANCE.
Common situations: Server restart or idle-timeout closed the MCP session; session expired per HTTP semantics (Mcp-Session-Id no longer valid); network drop followed by reconnect attempt against a stale, closed session.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- Error parsing JSON-RPC message:
- Got error when closing transport
- Transport has no request handler registered. Remember to cal
- Session + sessionRepresentation + was not found on the MCP s
- Failed to read stdio connection resource
AI-assisted analysis of spring-projects/spring-ai@98a7beda4f (2026-09-11).
Data as JSON: /api/errors/023c3b1d1d29edb2.
Report an issue: GitHub.