@@ -297,6 +297,16 @@ class McpStdioConnection extends EventEmitter {
297297 while ( this . frameBuffer . length > 0 ) {
298298 const header = this . findHeaderEnd ( this . frameBuffer ) ;
299299 if ( ! header ) {
300+ // Compatibility: some legacy MCP servers speak newline-delimited JSON-RPC.
301+ // If buffered stdout looks like JSON lines, parse it immediately instead of
302+ // waiting for a Content-Length timeout.
303+ const preview = this . frameBuffer . toString ( 'utf8' , 0 , Math . min ( this . frameBuffer . length , 256 ) ) ;
304+ const trimmed = preview . trimStart ( ) ;
305+ const looksLikeJsonLine = trimmed . startsWith ( '{' ) || trimmed . startsWith ( '[' ) ;
306+ if ( looksLikeJsonLine && this . frameBuffer . includes ( 0x0a ) ) {
307+ this . parseLineDelimitedData ( this . frameBuffer ) ;
308+ this . frameBuffer = Buffer . alloc ( 0 ) ;
309+ }
300310 return ;
301311 }
302312
@@ -817,7 +827,7 @@ export class McpClientManager {
817827 */
818828 private async connectStdio ( config : McpServerConfig ) : Promise < void > {
819829 try {
820- const connected = await this . connectStdioWithFraming ( config , 'content-length' ) ;
830+ const connected = await this . connectStdioWithFallbackFraming ( config ) ;
821831 this . registerConnectedStdioServer ( config , connected . connection , connected . tools ) ;
822832 } catch ( error ) {
823833 if ( ! this . shouldRetryNpxWithIsolatedCache ( config , error ) ) {
@@ -830,7 +840,7 @@ export class McpClientManager {
830840 } ;
831841
832842 try {
833- const connected = await this . connectStdioWithFraming ( retryConfig , 'content-length' ) ;
843+ const connected = await this . connectStdioWithFallbackFraming ( retryConfig ) ;
834844 // Keep persisted config intact; retry cache env is only a runtime override.
835845 this . registerConnectedStdioServer ( config , connected . connection , connected . tools ) ;
836846 } catch ( retryError ) {
@@ -849,6 +859,42 @@ export class McpClientManager {
849859 return isRetriableNpxInstallError ( message ) ;
850860 }
851861
862+ /**
863+ * Some MCP servers still use newline-delimited JSON-RPC over stdio.
864+ * Start with Content-Length framing (spec), then fallback to newline when
865+ * initialize stalls/closes without a successful handshake.
866+ */
867+ private shouldRetryWithNewlineFraming ( error : unknown ) : boolean {
868+ const message = error instanceof Error ? error . message : String ( error ) ;
869+ return (
870+ message . includes ( 'MCP request "initialize" timed out' )
871+ || message . includes ( 'MCP connection closed before initialization completed' )
872+ || message . includes ( 'MCP connection closed (server exited with code' )
873+ ) ;
874+ }
875+
876+ private async connectStdioWithFallbackFraming (
877+ config : McpServerConfig
878+ ) : Promise < { connection : McpStdioConnection ; tools : McpToolDefinition [ ] } > {
879+ try {
880+ return await this . connectStdioWithFraming ( config , 'content-length' ) ;
881+ } catch ( contentLengthError ) {
882+ if ( ! this . shouldRetryWithNewlineFraming ( contentLengthError ) ) {
883+ throw contentLengthError ;
884+ }
885+
886+ try {
887+ return await this . connectStdioWithFraming ( config , 'newline' ) ;
888+ } catch ( newlineError ) {
889+ const first = contentLengthError instanceof Error
890+ ? contentLengthError . message
891+ : String ( contentLengthError ) ;
892+ const second = newlineError instanceof Error ? newlineError . message : String ( newlineError ) ;
893+ throw new Error ( `${ first } \nRetry with newline framing failed: ${ second } ` ) ;
894+ }
895+ }
896+ }
897+
852898 /**
853899 * Tries to establish a stdio MCP connection with a specific framing mode.
854900 */
0 commit comments