1- import { createInterface } from "node:readline" ;
2-
31export interface JsonRpcRequest {
42 jsonrpc : "2.0" ;
53 id ?: string | number ;
@@ -19,6 +17,11 @@ export type RequestHandler = (
1917 params : Record < string , unknown > ,
2018) => Promise < unknown > ;
2119
20+ export interface StdioMessageParser {
21+ push : ( chunk : Buffer | string ) => void ;
22+ isFramed : ( ) => boolean ;
23+ }
24+
2225// JSON-RPC 2.0 notifications are messages without an `id` field. The spec
2326// (and the MCP transport contract) requires the server to NOT send a
2427// response for notifications. Some clients tolerate spurious responses;
@@ -130,26 +133,131 @@ export async function processLine(
130133 }
131134}
132135
136+ function findHeaderEnd ( buffer : Buffer ) : { headerEnd : number ; bodyStart : number } | null {
137+ const crlf = buffer . indexOf ( "\r\n\r\n" ) ;
138+ const lf = buffer . indexOf ( "\n\n" ) ;
139+ if ( crlf === - 1 && lf === - 1 ) return null ;
140+ if ( crlf !== - 1 && ( lf === - 1 || crlf <= lf ) ) {
141+ return { headerEnd : crlf , bodyStart : crlf + 4 } ;
142+ }
143+ return { headerEnd : lf , bodyStart : lf + 2 } ;
144+ }
145+
146+ function parseContentLength ( header : string ) : number | null {
147+ for ( const line of header . split ( / \r ? \n / ) ) {
148+ const match = line . match ( / ^ c o n t e n t - l e n g t h : \s * ( \d + ) \s * $ / i) ;
149+ if ( match ) return Number ( match [ 1 ] ) ;
150+ }
151+ return null ;
152+ }
153+
154+ export function formatResponse (
155+ response : JsonRpcResponse ,
156+ framed : boolean ,
157+ ) : string | Buffer [ ] {
158+ const body = JSON . stringify ( response ) ;
159+ if ( ! framed ) return `${ body } \n` ;
160+ const bytes = Buffer . from ( body , "utf8" ) ;
161+ return [ Buffer . from ( `Content-Length: ${ bytes . length } \r\n\r\n` , "ascii" ) , bytes ] ;
162+ }
163+
164+ export function createMessageParser (
165+ onMessage : ( message : string ) => void ,
166+ writeErr : ( msg : string ) => void = ( msg ) => process . stderr . write ( msg ) ,
167+ ) : StdioMessageParser {
168+ let buffer = Buffer . alloc ( 0 ) ;
169+ let framed = false ;
170+
171+ function processBuffer ( ) : void {
172+ while ( buffer . length > 0 ) {
173+ if ( buffer [ 0 ] === 10 || buffer [ 0 ] === 13 ) {
174+ buffer = buffer . subarray ( 1 ) ;
175+ continue ;
176+ }
177+
178+ const preview = buffer . toString ( "ascii" , 0 , Math . min ( buffer . length , 32 ) ) ;
179+ if ( / ^ c o n t e n t - l e n g t h : / i. test ( preview ) ) {
180+ const header = findHeaderEnd ( buffer ) ;
181+ if ( ! header ) return ;
182+
183+ const headerText = buffer . subarray ( 0 , header . headerEnd ) . toString ( "ascii" ) ;
184+ const contentLength = parseContentLength ( headerText ) ;
185+ if ( contentLength === null ) {
186+ writeErr ( "[mcp-transport] missing Content-Length header\n" ) ;
187+ buffer = buffer . subarray ( header . bodyStart ) ;
188+ continue ;
189+ }
190+
191+ const messageEnd = header . bodyStart + contentLength ;
192+ if ( buffer . length < messageEnd ) return ;
193+
194+ framed = true ;
195+ const message = buffer . subarray ( header . bodyStart , messageEnd ) . toString ( "utf8" ) ;
196+ buffer = buffer . subarray ( messageEnd ) ;
197+ onMessage ( message ) ;
198+ continue ;
199+ }
200+
201+ const newline = buffer . indexOf ( 10 ) ;
202+ if ( newline === - 1 ) return ;
203+ const line = buffer
204+ . subarray ( 0 , newline )
205+ . toString ( "utf8" )
206+ . replace ( / \r $ / , "" ) ;
207+ buffer = buffer . subarray ( newline + 1 ) ;
208+ onMessage ( line ) ;
209+ }
210+ }
211+
212+ return {
213+ push ( chunk ) {
214+ const bytes = Buffer . isBuffer ( chunk ) ? chunk : Buffer . from ( chunk , "utf8" ) ;
215+ buffer = Buffer . concat ( [ buffer , bytes ] ) ;
216+ processBuffer ( ) ;
217+ } ,
218+ isFramed ( ) {
219+ return framed ;
220+ } ,
221+ } ;
222+ }
223+
133224export function createStdioTransport ( handler : RequestHandler ) : {
134225 start : ( ) => void ;
135226 stop : ( ) => void ;
136227} {
137- let rl : ReturnType < typeof createInterface > | null = null ;
228+ let parser : StdioMessageParser | null = null ;
229+ let queue = Promise . resolve ( ) ;
138230
139231 const writeResponse = ( response : JsonRpcResponse ) => {
140- process . stdout . write ( JSON . stringify ( response ) + "\n" ) ;
232+ const formatted = formatResponse ( response , parser ?. isFramed ( ) ?? false ) ;
233+ if ( typeof formatted === "string" ) {
234+ process . stdout . write ( formatted ) ;
235+ return ;
236+ }
237+ for ( const chunk of formatted ) {
238+ process . stdout . write ( chunk ) ;
239+ }
141240 } ;
142241
143- const onLine = ( line : string ) => processLine ( line , handler , writeResponse ) ;
242+ const onData = ( chunk : Buffer ) => parser ?. push ( chunk ) ;
144243
145244 return {
146245 start ( ) {
147- rl = createInterface ( { input : process . stdin } ) ;
148- rl . on ( "line" , onLine ) ;
246+ parser = createMessageParser ( ( message ) => {
247+ queue = queue . then ( ( ) => processLine ( message , handler , writeResponse ) ) ;
248+ void queue . catch ( ( err ) => {
249+ process . stderr . write (
250+ `[mcp-transport] request processing failed: ${
251+ err instanceof Error ? err . message : String ( err )
252+ } \n`,
253+ ) ;
254+ } ) ;
255+ } ) ;
256+ process . stdin . on ( "data" , onData ) ;
149257 } ,
150258 stop ( ) {
151- rl ?. close ( ) ;
152- rl = null ;
259+ process . stdin . off ( "data" , onData ) ;
260+ parser = null ;
153261 } ,
154262 } ;
155263}
0 commit comments