Skip to content

Commit 6c5a467

Browse files
committed
Fix MCP stdio protocol violation during startup
The MCP protocol requires the client to send the first message. Writing to stdout before the client's initialization request breaks the protocol handshake, causing 'BrokenResourceError' in clients like langchain-mcp-adapters. Root cause: Transport was created AFTER config/feature flags initialization, so logger.* calls during startup wrote directly to stdout (bypassing buffering) or called sendLog() which also wrote to stdout without checking isInitialized. Additional issue: Async feature flag fetch operations would log messages AFTER the client had already started closing, causing timing conflicts. Fixes: 1. Move FilteredStdioServerTransport creation before config loading 2. Buffer sendLog() messages when isInitialized is false 3. Guard sendProgress/sendCustomNotification when not initialized 4. Add .unref() to feature flag refresh interval for clean process exit 5. Remove async logging from feature flag fetch/save operations Tested with langchain-mcp-adapters - now passes where it previously failed. Fixes #issue-reported-by-ever1022
1 parent d3f5824 commit 6c5a467

File tree

3 files changed

+37
-9
lines changed

3 files changed

+37
-9
lines changed

src/custom-stdio.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,13 +277,25 @@ export class FilteredStdioServerTransport extends StdioServerTransport {
277277

278278
/**
279279
* Public method to send log notifications from anywhere in the application
280+
* Now properly buffers messages before MCP initialization to avoid breaking stdio protocol
280281
*/
281282
public sendLog(level: "emergency" | "alert" | "critical" | "error" | "warning" | "notice" | "info" | "debug", message: string, data?: any) {
282283
// Skip if notifications are disabled (e.g., for Cline)
283284
if (this.disableNotifications) {
284285
return;
285286
}
286287

288+
// Buffer messages before initialization to avoid breaking MCP protocol
289+
// MCP requires client to send first message - server cannot write to stdout before that
290+
if (!this.isInitialized) {
291+
this.messageBuffer.push({
292+
level,
293+
args: [data ? { message, ...data } : message],
294+
timestamp: Date.now()
295+
});
296+
return;
297+
}
298+
287299
try {
288300
const notification: LogNotification = {
289301
jsonrpc: "2.0",
@@ -315,6 +327,11 @@ export class FilteredStdioServerTransport extends StdioServerTransport {
315327
* Send a progress notification (useful for long-running operations)
316328
*/
317329
public sendProgress(token: string, value: number, total?: number) {
330+
// Don't send progress before initialization - would break MCP protocol
331+
if (!this.isInitialized) {
332+
return;
333+
}
334+
318335
try {
319336
const notification = {
320337
jsonrpc: "2.0" as const,
@@ -346,6 +363,11 @@ export class FilteredStdioServerTransport extends StdioServerTransport {
346363
* Send a custom notification with any method name
347364
*/
348365
public sendCustomNotification(method: string, params: any) {
366+
// Don't send custom notifications before initialization - would break MCP protocol
367+
if (!this.isInitialized) {
368+
return;
369+
}
370+
349371
try {
350372
const notification = {
351373
jsonrpc: "2.0" as const,

src/index.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,13 @@ async function runServer() {
3939
// Set global flag for onboarding control
4040
(global as any).disableOnboarding = DISABLE_ONBOARDING;
4141

42+
// Create transport FIRST so all logging gets properly buffered
43+
// This must happen before any code that might use logger.*
44+
const transport = new FilteredStdioServerTransport();
45+
46+
// Export transport for use throughout the application
47+
global.mcpTransport = transport;
48+
4249
try {
4350
deferLog('info', 'Loading configuration...');
4451
await configManager.loadConfig();
@@ -56,10 +63,6 @@ async function runServer() {
5663
// Continue anyway - we'll use an in-memory config
5764
}
5865

59-
const transport = new FilteredStdioServerTransport();
60-
61-
// Export transport for use throughout the application
62-
global.mcpTransport = transport;
6366
// Handle uncaught exceptions
6467
process.on('uncaughtException', async (error) => {
6568
const errorMessage = error instanceof Error ? error.message : String(error);

src/utils/feature-flags.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ class FeatureFlagManager {
4646
});
4747
}, this.cacheMaxAge);
4848

49+
// Allow process to exit even if interval is pending
50+
// This is critical for proper cleanup when MCP client disconnects
51+
this.refreshInterval.unref();
52+
4953
logger.info(`Feature flags initialized (refresh every ${this.cacheMaxAge / 1000}s)`);
5054
} catch (error) {
5155
logger.warning('Failed to initialize feature flags:', error);
@@ -107,7 +111,7 @@ class FeatureFlagManager {
107111
*/
108112
private async fetchFlags(): Promise<void> {
109113
try {
110-
logger.debug('Fetching feature flags from:', this.flagUrl);
114+
// Don't log here - runs async and can interfere with MCP clients
111115

112116
const controller = new AbortController();
113117
const timeout = setTimeout(() => controller.abort(), 5000);
@@ -132,10 +136,9 @@ class FeatureFlagManager {
132136
this.flags = config.flags;
133137
this.lastFetch = Date.now();
134138

135-
// Save to cache
139+
// Save to cache (silently - don't log during async operations
140+
// as it can interfere with MCP clients that close quickly)
136141
await this.saveToCache(config);
137-
138-
logger.info(`Feature flags updated: ${Object.keys(this.flags).length} flags`);
139142
}
140143
} catch (error: any) {
141144
logger.debug('Failed to fetch feature flags:', error.message);
@@ -154,7 +157,7 @@ class FeatureFlagManager {
154157
}
155158

156159
await fs.writeFile(this.cachePath, JSON.stringify(config, null, 2), 'utf8');
157-
logger.debug('Saved feature flags to cache');
160+
// Don't log here - this runs async and can cause issues with MCP clients
158161
} catch (error) {
159162
logger.warning('Failed to save feature flags to cache:', error);
160163
}

0 commit comments

Comments
 (0)