fix: implement Single-Session architecture to resolve MCP stream errors

- Add ConsoleManager to prevent console output interference with StreamableHTTPServerTransport
- Implement SingleSessionHTTPServer with persistent session reuse
- Create N8NMCPEngine for clean service integration
- Add automatic session expiry after 30 minutes of inactivity
- Update logger to be HTTP-aware during active requests
- Maintain backward compatibility with existing deployments

This fixes the "stream is not readable" error by implementing the Hybrid
Single-Session architecture as documented in MCP_ERROR_FIX_PLAN.md

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
czlonkowski
2025-06-14 15:02:49 +02:00
parent 88dd66bb7a
commit 2cb264fd56
13 changed files with 1894 additions and 51 deletions

View File

@@ -0,0 +1,83 @@
/**
* Console Manager for MCP HTTP Server
*
* Prevents console output from interfering with StreamableHTTPServerTransport
* by silencing console methods during MCP request handling.
*/
export class ConsoleManager {
private originalConsole = {
log: console.log,
error: console.error,
warn: console.warn,
info: console.info,
debug: console.debug,
trace: console.trace
};
private isSilenced = false;
/**
* Silence all console output
*/
public silence(): void {
if (this.isSilenced || process.env.MCP_MODE !== 'http') {
return;
}
this.isSilenced = true;
process.env.MCP_REQUEST_ACTIVE = 'true';
console.log = () => {};
console.error = () => {};
console.warn = () => {};
console.info = () => {};
console.debug = () => {};
console.trace = () => {};
}
/**
* Restore original console methods
*/
public restore(): void {
if (!this.isSilenced) {
return;
}
this.isSilenced = false;
process.env.MCP_REQUEST_ACTIVE = 'false';
console.log = this.originalConsole.log;
console.error = this.originalConsole.error;
console.warn = this.originalConsole.warn;
console.info = this.originalConsole.info;
console.debug = this.originalConsole.debug;
console.trace = this.originalConsole.trace;
}
/**
* Wrap an operation with console silencing
* Automatically restores console on completion or error
*/
public async wrapOperation<T>(operation: () => T | Promise<T>): Promise<T> {
this.silence();
try {
const result = operation();
if (result instanceof Promise) {
return await result.finally(() => this.restore());
}
this.restore();
return result;
} catch (error) {
this.restore();
throw error;
}
}
/**
* Check if console is currently silenced
*/
public get isActive(): boolean {
return this.isSilenced;
}
}
// Export singleton instance for easy use
export const consoleManager = new ConsoleManager();

View File

@@ -14,6 +14,8 @@ export interface LoggerConfig {
export class Logger {
private config: LoggerConfig;
private static instance: Logger;
private useFileLogging = false;
private fileStream: any = null;
constructor(config?: Partial<LoggerConfig>) {
this.config = {
@@ -52,6 +54,13 @@ export class Logger {
if (level <= this.config.level) {
const formattedMessage = this.formatMessage(levelName, message);
// In HTTP mode during request handling, suppress console output
// The ConsoleManager will handle this, but we add a safety check
if (process.env.MCP_MODE === 'http' && process.env.MCP_REQUEST_ACTIVE === 'true') {
// Silently drop the log during active MCP requests
return;
}
switch (level) {
case LogLevel.ERROR:
console.error(formattedMessage, ...args);