mirror of
https://github.com/czlonkowski/n8n-mcp.git
synced 2026-02-06 05:23:08 +00:00
fix: Reconnect transport layer during session restoration (v2.19.3) (#317)
Fixes critical bug where session restoration successfully restored InstanceContext but failed to reconnect the transport layer, causing all requests on restored sessions to hang indefinitely. Root Cause: The handleRequest() method's session restoration flow (lines 1119-1197) called createSession() which creates a NEW transport separate from the current HTTP request. This separate transport is not linked to the current req/res pair, so responses cannot be sent back through the active HTTP connection. Fix Applied: Replace createSession() call with inline transport creation that mirrors the initialize flow. Create StreamableHTTPServerTransport directly for the current HTTP req/res context and ensure transport is connected to server BEFORE handling request. This makes restored sessions work identically to fresh sessions. Impact: - Zero-downtime deployments now work correctly - Users can continue work after container restart without restarting MCP client - Session persistence is now fully functional for production use Technical Details: The StreamableHTTPServerTransport class from MCP SDK links a specific HTTP req/res pair to the MCP server. Creating transport in createSession() binds it to the wrong req/res (or no req/res at all). The initialize flow got this right, but restoration flow did not. Files Changed: - src/http-server-single-session.ts: Fixed session restoration (lines 1163-1244) - package.json, package.runtime.json, src/mcp-engine.ts: Version bump to 2.19.3 - CHANGELOG.md: Documented fix with technical details Testing: All 13 session persistence integration tests pass, verifying restoration works correctly. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
committed by
GitHub
parent
318986f546
commit
112b40119c
@@ -1160,29 +1160,77 @@ export class SingleSessionHTTPServer {
|
||||
return;
|
||||
}
|
||||
|
||||
// REQ-2: Create session (idempotent) and wait for connection
|
||||
logger.info('Session restoration successful, creating session', {
|
||||
// REQ-2: Create transport and server inline for THIS REQUEST (like initialize flow)
|
||||
// CRITICAL FIX: Don't use createSession() as it creates a separate transport
|
||||
// not linked to the current HTTP req/res pair. We MUST create the transport
|
||||
// for the current request context, just like the initialize flow does.
|
||||
logger.info('Session restoration successful, creating transport inline', {
|
||||
sessionId,
|
||||
instanceId: restoredContext.instanceId
|
||||
});
|
||||
|
||||
// CRITICAL: Wait for server.connect() to complete before proceeding
|
||||
// This ensures the transport is fully ready to handle requests
|
||||
await this.createSession(restoredContext, sessionId, true);
|
||||
// Create server and transport for THIS REQUEST
|
||||
const server = new N8NDocumentationMCPServer(restoredContext);
|
||||
|
||||
// Verify session was created
|
||||
if (!this.transports[sessionId]) {
|
||||
logger.error('Session creation failed after restoration', { sessionId });
|
||||
res.status(500).json({
|
||||
jsonrpc: '2.0',
|
||||
error: {
|
||||
code: -32603,
|
||||
message: 'Session creation failed'
|
||||
},
|
||||
id: req.body?.id || null
|
||||
});
|
||||
return;
|
||||
}
|
||||
transport = new StreamableHTTPServerTransport({
|
||||
sessionIdGenerator: () => sessionId,
|
||||
onsessioninitialized: (initializedSessionId: string) => {
|
||||
// Store both transport and server by session ID when session is initialized
|
||||
logger.info('Session initialized after restoration', {
|
||||
sessionId: initializedSessionId
|
||||
});
|
||||
this.transports[initializedSessionId] = transport;
|
||||
this.servers[initializedSessionId] = server;
|
||||
|
||||
// Store session metadata and context
|
||||
this.sessionMetadata[initializedSessionId] = {
|
||||
lastAccess: new Date(),
|
||||
createdAt: new Date()
|
||||
};
|
||||
this.sessionContexts[initializedSessionId] = restoredContext;
|
||||
}
|
||||
});
|
||||
|
||||
// Set up cleanup handlers (same as initialize flow)
|
||||
transport.onclose = () => {
|
||||
const sid = transport.sessionId;
|
||||
if (sid) {
|
||||
// Prevent recursive cleanup during shutdown
|
||||
if (this.isShuttingDown) {
|
||||
logger.debug('Ignoring transport close event during shutdown', { sessionId: sid });
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info('Restored transport closed, cleaning up', { sessionId: sid });
|
||||
this.removeSession(sid, 'transport_closed').catch(err => {
|
||||
logger.error('Error during transport close cleanup', {
|
||||
sessionId: sid,
|
||||
error: err instanceof Error ? err.message : String(err)
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Handle transport errors to prevent connection drops
|
||||
transport.onerror = (error: Error) => {
|
||||
const sid = transport.sessionId;
|
||||
if (sid) {
|
||||
// Prevent recursive cleanup during shutdown
|
||||
if (this.isShuttingDown) {
|
||||
logger.debug('Ignoring transport error event during shutdown', { sessionId: sid });
|
||||
return;
|
||||
}
|
||||
|
||||
logger.error('Restored transport error', { sessionId: sid, error: error.message });
|
||||
this.removeSession(sid, 'transport_error').catch(err => {
|
||||
logger.error('Error during transport error cleanup', { error: err });
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Connect the server to the transport BEFORE handling the request
|
||||
logger.info('Connecting server to restored session transport');
|
||||
await server.connect(transport);
|
||||
|
||||
// Phase 3: Emit onSessionRestored event (REQ-4)
|
||||
// Fire-and-forget: don't await or block request processing
|
||||
@@ -1193,9 +1241,7 @@ export class SingleSessionHTTPServer {
|
||||
});
|
||||
});
|
||||
|
||||
// Use the restored session
|
||||
transport = this.transports[sessionId];
|
||||
logger.info('Using restored session transport', { sessionId });
|
||||
logger.info('Restored session transport ready', { sessionId });
|
||||
|
||||
} catch (error) {
|
||||
// Handle timeout
|
||||
|
||||
Reference in New Issue
Block a user