Skip to content

Commit 0a0cbee

Browse files
committed
Close servlet streamable HTTP transports on async lifecycle events
1 parent 2bb1481 commit 0a0cbee

2 files changed

Lines changed: 312 additions & 31 deletions

File tree

‎mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStreamableServerTransportProvider.java‎

Lines changed: 35 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
import io.modelcontextprotocol.util.Assert;
2929
import io.modelcontextprotocol.util.KeepAliveScheduler;
3030
import jakarta.servlet.AsyncContext;
31+
import jakarta.servlet.AsyncEvent;
32+
import jakarta.servlet.AsyncListener;
3133
import jakarta.servlet.ServletException;
3234
import jakarta.servlet.annotation.WebServlet;
3335
import jakarta.servlet.http.HttpServlet;
@@ -329,6 +331,7 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response)
329331
// Check if this is a replay request
330332
if (request.getHeader(HttpHeaders.LAST_EVENT_ID) != null) {
331333
String lastId = request.getHeader(HttpHeaders.LAST_EVENT_ID);
334+
registerAsyncLifecycle(asyncContext, sessionId, sessionTransport::close);
332335

333336
try {
334337
session.replay(lastId)
@@ -342,44 +345,21 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response)
342345
}
343346
catch (Exception e) {
344347
logger.error("Failed to replay message: {}", e.getMessage());
345-
asyncContext.complete();
348+
sessionTransport.close();
346349
}
347350
});
348351
}
349352
catch (Exception e) {
350353
logger.error("Failed to replay messages: {}", e.getMessage());
351-
asyncContext.complete();
354+
sessionTransport.close();
352355
}
353356
}
354357
else {
355358
// Establish new listening stream
356359
McpStreamableServerSession.McpStreamableServerSessionStream listeningStream = session
357360
.listeningStream(sessionTransport);
358361

359-
asyncContext.addListener(new jakarta.servlet.AsyncListener() {
360-
@Override
361-
public void onComplete(jakarta.servlet.AsyncEvent event) throws IOException {
362-
logger.debug("SSE connection completed for session: {}", sessionId);
363-
listeningStream.close();
364-
}
365-
366-
@Override
367-
public void onTimeout(jakarta.servlet.AsyncEvent event) throws IOException {
368-
logger.debug("SSE connection timed out for session: {}", sessionId);
369-
listeningStream.close();
370-
}
371-
372-
@Override
373-
public void onError(jakarta.servlet.AsyncEvent event) throws IOException {
374-
logger.debug("SSE connection error for session: {}", sessionId);
375-
listeningStream.close();
376-
}
377-
378-
@Override
379-
public void onStartAsync(jakarta.servlet.AsyncEvent event) throws IOException {
380-
// No action needed
381-
}
382-
});
362+
registerAsyncLifecycle(asyncContext, sessionId, listeningStream::close);
383363
}
384364
}
385365
catch (Exception e) {
@@ -530,6 +510,7 @@ else if (message instanceof McpSchema.JSONRPCRequest jsonrpcRequest) {
530510

531511
HttpServletStreamableMcpSessionTransport sessionTransport = new HttpServletStreamableMcpSessionTransport(
532512
sessionId, asyncContext, response.getWriter());
513+
registerAsyncLifecycle(asyncContext, sessionId, sessionTransport::close);
533514

534515
try {
535516
session.responseStream(jsonrpcRequest, sessionTransport)
@@ -538,7 +519,7 @@ else if (message instanceof McpSchema.JSONRPCRequest jsonrpcRequest) {
538519
}
539520
catch (Exception e) {
540521
logger.error("Failed to handle request stream: {}", e.getMessage());
541-
asyncContext.complete();
522+
sessionTransport.close();
542523
}
543524
}
544525
else {
@@ -571,6 +552,32 @@ else if (message instanceof McpSchema.JSONRPCRequest jsonrpcRequest) {
571552
}
572553
}
573554

555+
private void registerAsyncLifecycle(AsyncContext asyncContext, String sessionId, Runnable onClose) {
556+
asyncContext.addListener(new AsyncListener() {
557+
@Override
558+
public void onComplete(AsyncEvent event) throws IOException {
559+
logger.debug("SSE async context completed for session: {}", sessionId);
560+
onClose.run();
561+
}
562+
563+
@Override
564+
public void onTimeout(AsyncEvent event) throws IOException {
565+
logger.debug("SSE async context timed out for session: {}", sessionId);
566+
onClose.run();
567+
}
568+
569+
@Override
570+
public void onError(AsyncEvent event) throws IOException {
571+
logger.debug("SSE async context errored for session: {}", sessionId);
572+
onClose.run();
573+
}
574+
575+
@Override
576+
public void onStartAsync(AsyncEvent event) throws IOException {
577+
}
578+
});
579+
}
580+
574581
/**
575582
* Handles DELETE requests for session deletion.
576583
* @param request The HTTP servlet request
@@ -761,8 +768,7 @@ public Mono<Void> sendMessage(McpSchema.JSONRPCMessage message, String messageId
761768
}
762769
catch (Exception e) {
763770
logger.error("Failed to send message to session {}: {}", this.sessionId, e.getMessage());
764-
HttpServletStreamableServerTransportProvider.this.sessions.remove(this.sessionId);
765-
this.asyncContext.complete();
771+
this.close();
766772
}
767773
finally {
768774
lock.unlock();
@@ -806,8 +812,6 @@ public void close() {
806812
}
807813

808814
this.closed = true;
809-
810-
// HttpServletStreamableServerTransportProvider.this.sessions.remove(this.sessionId);
811815
this.asyncContext.complete();
812816
logger.debug("Successfully completed async context for session {}", sessionId);
813817
}

0 commit comments

Comments
 (0)