mirror of
https://github.com/czlonkowski/n8n-mcp.git
synced 2026-03-25 03:43:08 +00:00
feat: implement session persistence for v2.19.0 (Phase 1 + Phase 2)
Phase 1 - Lazy Session Restoration (REQ-1, REQ-2, REQ-8): - Add onSessionNotFound hook for restoring sessions from external storage - Implement idempotent session creation to prevent race conditions - Add session ID validation for security (prevent injection attacks) - Comprehensive error handling (400/408/500 status codes) - 13 integration tests covering all scenarios Phase 2 - Session Management API (REQ-5): - getActiveSessions(): Get all active session IDs - getSessionState(sessionId): Get session state for persistence - getAllSessionStates(): Bulk session state retrieval - restoreSession(sessionId, context): Manual session restoration - deleteSession(sessionId): Manual session termination - 21 unit tests covering all API methods Benefits: - Sessions survive container restarts - Horizontal scaling support (no session stickiness needed) - Zero-downtime deployments - 100% backwards compatible Implementation Details: - Backend methods in http-server-single-session.ts - Public API methods in mcp-engine.ts - SessionState type exported from index.ts - Synchronous session creation and deletion for reliable testing - Version updated from 2.18.10 to 2.19.0 Tests: 34 passing (13 integration + 21 unit) Coverage: Full API coverage with edge cases Security: Session ID validation prevents SQL/NoSQL injection and path traversal 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,7 @@ import { Request, Response } from 'express';
|
||||
import { SingleSessionHTTPServer } from './http-server-single-session';
|
||||
import { logger } from './utils/logger';
|
||||
import { InstanceContext } from './types/instance-context';
|
||||
import { SessionRestoreHook, SessionState } from './types/session-restoration';
|
||||
|
||||
export interface EngineHealth {
|
||||
status: 'healthy' | 'unhealthy';
|
||||
@@ -25,6 +26,22 @@ export interface EngineHealth {
|
||||
export interface EngineOptions {
|
||||
sessionTimeout?: number;
|
||||
logLevel?: 'error' | 'warn' | 'info' | 'debug';
|
||||
|
||||
/**
|
||||
* Session restoration hook for multi-tenant persistence
|
||||
* Called when a client tries to use an unknown session ID
|
||||
* Return instance context to restore the session, or null to reject
|
||||
*
|
||||
* @since 2.19.0
|
||||
*/
|
||||
onSessionNotFound?: SessionRestoreHook;
|
||||
|
||||
/**
|
||||
* Maximum time to wait for session restoration (milliseconds)
|
||||
* @default 5000 (5 seconds)
|
||||
* @since 2.19.0
|
||||
*/
|
||||
sessionRestorationTimeout?: number;
|
||||
}
|
||||
|
||||
export class N8NMCPEngine {
|
||||
@@ -32,9 +49,9 @@ export class N8NMCPEngine {
|
||||
private startTime: Date;
|
||||
|
||||
constructor(options: EngineOptions = {}) {
|
||||
this.server = new SingleSessionHTTPServer();
|
||||
this.server = new SingleSessionHTTPServer(options);
|
||||
this.startTime = new Date();
|
||||
|
||||
|
||||
if (options.logLevel) {
|
||||
process.env.LOG_LEVEL = options.logLevel;
|
||||
}
|
||||
@@ -97,7 +114,7 @@ export class N8NMCPEngine {
|
||||
total: Math.round(memoryUsage.heapTotal / 1024 / 1024),
|
||||
unit: 'MB'
|
||||
},
|
||||
version: '2.3.2'
|
||||
version: '2.19.0'
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('Health check failed:', error);
|
||||
@@ -106,7 +123,7 @@ export class N8NMCPEngine {
|
||||
uptime: 0,
|
||||
sessionActive: false,
|
||||
memoryUsage: { used: 0, total: 0, unit: 'MB' },
|
||||
version: '2.3.2'
|
||||
version: '2.19.0'
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -118,10 +135,118 @@ export class N8NMCPEngine {
|
||||
getSessionInfo(): { active: boolean; sessionId?: string; age?: number } {
|
||||
return this.server.getSessionInfo();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get all active session IDs (Phase 2 - REQ-5)
|
||||
* Returns array of currently active session IDs
|
||||
*
|
||||
* @returns Array of session IDs
|
||||
* @since 2.19.0
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const engine = new N8NMCPEngine();
|
||||
* const sessionIds = engine.getActiveSessions();
|
||||
* console.log(`Active sessions: ${sessionIds.length}`);
|
||||
* ```
|
||||
*/
|
||||
getActiveSessions(): string[] {
|
||||
return this.server.getActiveSessions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get session state for a specific session (Phase 2 - REQ-5)
|
||||
* Returns session state or null if session doesn't exist
|
||||
*
|
||||
* @param sessionId - The session ID to get state for
|
||||
* @returns SessionState object or null
|
||||
* @since 2.19.0
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const state = engine.getSessionState('session-123');
|
||||
* if (state) {
|
||||
* // Save to database
|
||||
* await db.saveSession(state);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
getSessionState(sessionId: string): SessionState | null {
|
||||
return this.server.getSessionState(sessionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all session states (Phase 2 - REQ-5)
|
||||
* Returns array of all active session states for bulk backup
|
||||
*
|
||||
* @returns Array of SessionState objects
|
||||
* @since 2.19.0
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Periodic backup every 5 minutes
|
||||
* setInterval(async () => {
|
||||
* const states = engine.getAllSessionStates();
|
||||
* for (const state of states) {
|
||||
* await database.upsertSession(state);
|
||||
* }
|
||||
* }, 300000);
|
||||
* ```
|
||||
*/
|
||||
getAllSessionStates(): SessionState[] {
|
||||
return this.server.getAllSessionStates();
|
||||
}
|
||||
|
||||
/**
|
||||
* Manually restore a session (Phase 2 - REQ-5)
|
||||
* Creates a session with the given ID and instance context
|
||||
*
|
||||
* @param sessionId - The session ID to restore
|
||||
* @param instanceContext - Instance configuration
|
||||
* @returns true if session was restored successfully, false otherwise
|
||||
* @since 2.19.0
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Restore session from database
|
||||
* const session = await db.loadSession('session-123');
|
||||
* if (session) {
|
||||
* const restored = engine.restoreSession(
|
||||
* session.sessionId,
|
||||
* session.instanceContext
|
||||
* );
|
||||
* console.log(`Restored: ${restored}`);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
restoreSession(sessionId: string, instanceContext: InstanceContext): boolean {
|
||||
return this.server.manuallyRestoreSession(sessionId, instanceContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Manually delete a session (Phase 2 - REQ-5)
|
||||
* Removes the session and cleans up resources
|
||||
*
|
||||
* @param sessionId - The session ID to delete
|
||||
* @returns true if session was deleted, false if not found
|
||||
* @since 2.19.0
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Delete expired session
|
||||
* const deleted = engine.deleteSession('session-123');
|
||||
* if (deleted) {
|
||||
* await db.deleteSession('session-123');
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
deleteSession(sessionId: string): boolean {
|
||||
return this.server.manuallyDeleteSession(sessionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Graceful shutdown for service lifecycle
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* process.on('SIGTERM', async () => {
|
||||
* await engine.shutdown();
|
||||
|
||||
Reference in New Issue
Block a user