diff --git a/src/custom-stdio.ts b/src/custom-stdio.ts index 4c868c90..e1c3bb4c 100644 --- a/src/custom-stdio.ts +++ b/src/custom-stdio.ts @@ -277,6 +277,7 @@ export class FilteredStdioServerTransport extends StdioServerTransport { /** * Public method to send log notifications from anywhere in the application + * Now properly buffers messages before MCP initialization to avoid breaking stdio protocol */ public sendLog(level: "emergency" | "alert" | "critical" | "error" | "warning" | "notice" | "info" | "debug", message: string, data?: any) { // Skip if notifications are disabled (e.g., for Cline) @@ -284,6 +285,17 @@ export class FilteredStdioServerTransport extends StdioServerTransport { return; } + // Buffer messages before initialization to avoid breaking MCP protocol + // MCP requires client to send first message - server cannot write to stdout before that + if (!this.isInitialized) { + this.messageBuffer.push({ + level, + args: [data ? { message, ...data } : message], + timestamp: Date.now() + }); + return; + } + try { const notification: LogNotification = { jsonrpc: "2.0", @@ -315,6 +327,11 @@ export class FilteredStdioServerTransport extends StdioServerTransport { * Send a progress notification (useful for long-running operations) */ public sendProgress(token: string, value: number, total?: number) { + // Don't send progress before initialization - would break MCP protocol + if (!this.isInitialized) { + return; + } + try { const notification = { jsonrpc: "2.0" as const, @@ -346,6 +363,11 @@ export class FilteredStdioServerTransport extends StdioServerTransport { * Send a custom notification with any method name */ public sendCustomNotification(method: string, params: any) { + // Don't send custom notifications before initialization - would break MCP protocol + if (!this.isInitialized) { + return; + } + try { const notification = { jsonrpc: "2.0" as const, diff --git a/src/index.ts b/src/index.ts index ef179f87..450d895c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -39,6 +39,13 @@ async function runServer() { // Set global flag for onboarding control (global as any).disableOnboarding = DISABLE_ONBOARDING; + // Create transport FIRST so all logging gets properly buffered + // This must happen before any code that might use logger.* + const transport = new FilteredStdioServerTransport(); + + // Export transport for use throughout the application + global.mcpTransport = transport; + try { deferLog('info', 'Loading configuration...'); await configManager.loadConfig(); @@ -56,10 +63,6 @@ async function runServer() { // Continue anyway - we'll use an in-memory config } - const transport = new FilteredStdioServerTransport(); - - // Export transport for use throughout the application - global.mcpTransport = transport; // Handle uncaught exceptions process.on('uncaughtException', async (error) => { const errorMessage = error instanceof Error ? error.message : String(error); diff --git a/src/utils/feature-flags.ts b/src/utils/feature-flags.ts index be78145a..2c711623 100644 --- a/src/utils/feature-flags.ts +++ b/src/utils/feature-flags.ts @@ -46,6 +46,10 @@ class FeatureFlagManager { }); }, this.cacheMaxAge); + // Allow process to exit even if interval is pending + // This is critical for proper cleanup when MCP client disconnects + this.refreshInterval.unref(); + logger.info(`Feature flags initialized (refresh every ${this.cacheMaxAge / 1000}s)`); } catch (error) { logger.warning('Failed to initialize feature flags:', error); @@ -107,7 +111,7 @@ class FeatureFlagManager { */ private async fetchFlags(): Promise { try { - logger.debug('Fetching feature flags from:', this.flagUrl); + // Don't log here - runs async and can interfere with MCP clients const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 5000); @@ -132,10 +136,9 @@ class FeatureFlagManager { this.flags = config.flags; this.lastFetch = Date.now(); - // Save to cache + // Save to cache (silently - don't log during async operations + // as it can interfere with MCP clients that close quickly) await this.saveToCache(config); - - logger.info(`Feature flags updated: ${Object.keys(this.flags).length} flags`); } } catch (error: any) { logger.debug('Failed to fetch feature flags:', error.message); @@ -154,7 +157,7 @@ class FeatureFlagManager { } await fs.writeFile(this.cachePath, JSON.stringify(config, null, 2), 'utf8'); - logger.debug('Saved feature flags to cache'); + // Don't log here - this runs async and can cause issues with MCP clients } catch (error) { logger.warning('Failed to save feature flags to cache:', error); }