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

  1. Catch McpTransportSessionClosedException and re-initialize the transport/session from scratch (call connect() again, or recreate the McpClient so a new session is negotiated).
  2. Check server logs for 404/session-expired responses on the streamable endpoint; ensure the server keeps sessions alive or supports resumption.
  3. 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

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


AI-assisted analysis of spring-projects/spring-ai@98a7beda4f (2026-09-11). Data as JSON: /api/errors/023c3b1d1d29edb2. Report an issue: GitHub.