fix: memory leak in session removal - close MCP server properly (#471) (#472)

- Add close() method to N8NDocumentationMCPServer that:
  - Calls server.close() (MCP SDK cleanup)
  - Clears internal cache
  - Nullifies service references to help GC
- Update removeSession() to call server.close() before releasing references

Root cause: removeSession() deleted server from map but didn't call cleanup
Evidence: Production server memory grew ~1GB in 43 minutes (10% to 35%)

Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Romuald Członkowski <romualdczlonkowski@MacBook-Pro-Romuald.local>
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Romuald Członkowski
2025-12-05 18:30:51 +01:00
committed by GitHub
parent 60479e0eb4
commit cdaa29e7a2
12 changed files with 104 additions and 10 deletions

View File

@@ -180,17 +180,29 @@ export class SingleSessionHTTPServer {
*/
private async removeSession(sessionId: string, reason: string): Promise<void> {
try {
// Store reference to transport before deletion
// Store references before deletion
const transport = this.transports[sessionId];
const server = this.servers[sessionId];
// Delete transport FIRST to prevent onclose handler from triggering recursion
// Delete references FIRST to prevent onclose handler from triggering recursion
// This breaks the circular reference: removeSession -> close -> onclose -> removeSession
delete this.transports[sessionId];
delete this.servers[sessionId];
delete this.sessionMetadata[sessionId];
delete this.sessionContexts[sessionId];
// Close transport AFTER deletion
// Close server first (may have references to transport)
// This fixes memory leak where server resources weren't freed (issue #471)
// Handle server close errors separately so transport close still runs
if (server && typeof server.close === 'function') {
try {
await server.close();
} catch (serverError) {
logger.warn('Error closing server', { sessionId, error: serverError });
}
}
// Close transport last
// When onclose handler fires, it won't find the transport anymore
if (transport) {
await transport.close();