Skip to content

Commit 3ff8234

Browse files
committed
Close servlet streamable HTTP transports on async lifecycle events
1 parent 305e4cb commit 3ff8234

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
@@ -27,6 +27,8 @@
2727
import io.modelcontextprotocol.util.Assert;
2828
import io.modelcontextprotocol.util.KeepAliveScheduler;
2929
import jakarta.servlet.AsyncContext;
30+
import jakarta.servlet.AsyncEvent;
31+
import jakarta.servlet.AsyncListener;
3032
import jakarta.servlet.ServletException;
3133
import jakarta.servlet.annotation.WebServlet;
3234
import jakarta.servlet.http.HttpServlet;
@@ -328,6 +330,7 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response)
328330
// Check if this is a replay request
329331
if (request.getHeader(HttpHeaders.LAST_EVENT_ID) != null) {
330332
String lastId = request.getHeader(HttpHeaders.LAST_EVENT_ID);
333+
registerAsyncLifecycle(asyncContext, sessionId, sessionTransport::close);
331334

332335
try {
333336
session.replay(lastId)
@@ -341,44 +344,21 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response)
341344
}
342345
catch (Exception e) {
343346
logger.error("Failed to replay message: {}", e.getMessage());
344-
asyncContext.complete();
347+
sessionTransport.close();
345348
}
346349
});
347350
}
348351
catch (Exception e) {
349352
logger.error("Failed to replay messages: {}", e.getMessage());
350-
asyncContext.complete();
353+
sessionTransport.close();
351354
}
352355
}
353356
else {
354357
// Establish new listening stream
355358
McpStreamableServerSession.McpStreamableServerSessionStream listeningStream = session
356359
.listeningStream(sessionTransport);
357360

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

530510
HttpServletStreamableMcpSessionTransport sessionTransport = new HttpServletStreamableMcpSessionTransport(
531511
sessionId, asyncContext, response.getWriter());
512+
registerAsyncLifecycle(asyncContext, sessionId, sessionTransport::close);
532513

533514
try {
534515
session.responseStream(jsonrpcRequest, sessionTransport)
@@ -537,7 +518,7 @@ else if (message instanceof McpSchema.JSONRPCRequest jsonrpcRequest) {
537518
}
538519
catch (Exception e) {
539520
logger.error("Failed to handle request stream: {}", e.getMessage());
540-
asyncContext.complete();
521+
sessionTransport.close();
541522
}
542523
}
543524
else {
@@ -570,6 +551,32 @@ else if (message instanceof McpSchema.JSONRPCRequest jsonrpcRequest) {
570551
}
571552
}
572553

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

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

0 commit comments

Comments
 (0)