fix: clean up sessions on close and handle DELETE /mcp - #73
Conversation
Sessions were stored in the transports map on initialization but never
removed, causing a memory leak in long-running servers as every client
connection (including those that later disconnect or crash) kept its
StreamableHTTPServerTransport pinned in memory forever.
Wire transport.onclose inside onsessioninitialized so the session is
removed from the map when the transport closes. The sessionId is
captured by closure, avoiding any race between ID assignment and
cleanup registration.
Also add app.delete('/mcp', mcpHandler) to implement the DELETE method
defined by the Streamable HTTP transport spec for explicit session
termination. The SDK's handleRequest handles DELETE semantics and fires
onclose, which removes the session via the fix above. A guard returns
400 for DELETE requests missing a session ID.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughThis change adds support for terminating MCP sessions via HTTP DELETE requests in the Express server. A transport.onclose handler removes sessions from the transports map on closure. DELETE requests missing an mcp-session-id header now return HTTP 400. A new DELETE /mcp route is registered. ChangesDELETE Session Termination
Sequence Diagram(s)sequenceDiagram
participant Client
participant ExpressApp
participant mcpHandler
participant TransportsMap
Client->>ExpressApp: DELETE /mcp
ExpressApp->>mcpHandler: route request
mcpHandler->>mcpHandler: check mcp-session-id header
alt missing session id
mcpHandler->>Client: 400 error response
else session id present
mcpHandler->>TransportsMap: lookup transport
TransportsMap->>mcpHandler: transport.onclose triggers
mcpHandler->>TransportsMap: delete session entry
end
Estimated code review effort🎯 1 (Trivial) | ⏱️ ~5 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Pull request overview
Fixes a long-running memory leak in the MCP HTTP transport session registry by ensuring session transports are removed when a session closes, and adds support for explicit session termination via DELETE /mcp as defined by the Streamable HTTP transport spec.
Changes:
- Register a
transport.onclosehandler during session initialization to remove the session from the in-memorytransportsmap. - Add validation for malformed
DELETE /mcprequests missing anmcp-session-idheader. - Wire up
app.delete("/mcp", mcpHandler)so the Express endpoint accepts DELETE requests.
| // DELETE without a session ID is malformed | ||
| if (req.method === "DELETE" && !sessionId) { | ||
| logger.warn("DELETE request without session ID"); | ||
| res.status(400).json({ error: "Session ID required to terminate a session" }); |
| transport.onclose = () => { | ||
| delete transports[sessionId]; | ||
| logger.info("MCP session closed", { sessionId }); | ||
| }; |
There was a problem hiding this comment.
onclose overwrites the SDK's internal close handler
server.connect(transport) (line 62) is called before transport.handleRequest, and the SDK's Protocol.connect wires transport.onclose to clear its own _transport reference and fire any server.onclose callbacks. Because onsessioninitialized fires inside handleRequest — after connect has already set the handler — this assignment silently discards the SDK's hook. As a result, the McpServer instance never learns the transport closed: server._transport stays set, server.onclose never fires, and the server+transport pair can form a reference cycle that keeps both objects alive after the session is removed from transports. Capture the previous handler and chain it to preserve SDK lifecycle semantics.
| transport.onclose = () => { | |
| delete transports[sessionId]; | |
| logger.info("MCP session closed", { sessionId }); | |
| }; | |
| const previousOnClose = transport.onclose; | |
| transport.onclose = () => { | |
| delete transports[sessionId]; | |
| logger.info("MCP session closed", { sessionId }); | |
| previousOnClose?.(); | |
| }; |
| sessionIdGenerator: () => randomUUID(), | ||
| onsessioninitialized: (sessionId) => { | ||
| transports[sessionId] = transport; | ||
| transport.onclose = () => { |
There was a problem hiding this comment.
The 2026-07-28 spec release candidate (SEP-2567 + SEP-2575) removes the Mcp-Session-Id header and the initialize handshake entirely, but for now we should still fix this in memory session lookup.
| } | ||
|
|
||
| // DELETE without a session ID is malformed | ||
| if (req.method === "DELETE" && !sessionId) { |
There was a problem hiding this comment.
Adds the missing /delete endpoint.
|
🎉 This PR is included in version 1.0.2 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
Sessions were stored in the transports map on initialization but never
removed, causing a memory leak in long-running servers as every client
connection (including those that later disconnect or crash) kept its
StreamableHTTPServerTransport pinned in memory forever.
Wire transport.onclose inside onsessioninitialized so the session is
removed from the map when the transport closes. The sessionId is
captured by closure, avoiding any race between ID assignment and
cleanup registration.
Also add app.delete('/mcp', mcpHandler) to implement the DELETE method
defined by the Streamable HTTP transport spec for explicit session
termination. The SDK's handleRequest handles DELETE semantics and fires
onclose, which removes the session via the fix above. A guard returns
400 for DELETE requests missing a session ID.
Generated with Devin
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Summary by CodeRabbit
DELETErequests.400error.