11package main
22
33import (
4- "bytes "
4+ "bufio "
55 "crypto/rand"
66 "encoding/json"
77 "fmt"
@@ -376,8 +376,8 @@ func buildJSONRPCRequest(method, toolName string, arguments map[string]any) (str
376376 return string (jsonData ), nil
377377}
378378
379- // executeServerCommand runs the specified command, sends the JSON request to stdin,
380- // and returns the response from stdout
379+ // executeServerCommand runs the specified command, performs the MCP initialization
380+ // handshake, sends the JSON request to stdin, and returns the response from stdout.
381381func executeServerCommand (cmdStr , jsonRequest string ) (string , error ) {
382382 // Split the command string into command and arguments
383383 cmdParts := strings .Fields (cmdStr )
@@ -393,28 +393,119 @@ func executeServerCommand(cmdStr, jsonRequest string) (string, error) {
393393 return "" , fmt .Errorf ("failed to create stdin pipe: %w" , err )
394394 }
395395
396- // Setup stdout and stderr pipes
397- var stdout , stderr bytes.Buffer
398- cmd .Stdout = & stdout
396+ // Setup stdout pipe for line-by-line reading
397+ stdoutPipe , err := cmd .StdoutPipe ()
398+ if err != nil {
399+ return "" , fmt .Errorf ("failed to create stdout pipe: %w" , err )
400+ }
401+
402+ // Stderr still uses a buffer
403+ var stderr strings.Builder
399404 cmd .Stderr = & stderr
400405
401406 // Start the command
402407 if err := cmd .Start (); err != nil {
403408 return "" , fmt .Errorf ("failed to start command: %w" , err )
404409 }
405410
406- // Write the JSON request to stdin
411+ // Ensure the child process is cleaned up on every return path.
412+ // stdin must be closed before Wait so the server sees EOF and exits;
413+ // its non-zero exit status on EOF is expected, so we ignore the error.
414+ defer func () {
415+ _ = stdin .Close ()
416+ _ = cmd .Wait ()
417+ }()
418+
419+ // Use a scanner with a large buffer for reading JSON-RPC responses
420+ scanner := bufio .NewScanner (stdoutPipe )
421+ scanner .Buffer (make ([]byte , 0 , 1024 * 1024 ), 1024 * 1024 ) // 1MB max line size
422+
423+ // Step 1: Send MCP initialize request
424+ initReq , err := buildInitializeRequest ()
425+ if err != nil {
426+ return "" , fmt .Errorf ("failed to build initialize request: %w" , err )
427+ }
428+ if _ , err := io .WriteString (stdin , initReq + "\n " ); err != nil {
429+ return "" , fmt .Errorf ("failed to write initialize request: %w" , err )
430+ }
431+
432+ // Step 2: Read initialize response (skip any server notifications)
433+ if _ , err := readJSONRPCResponse (scanner ); err != nil {
434+ return "" , fmt .Errorf ("failed to read initialize response: %w, stderr: %s" , err , stderr .String ())
435+ }
436+
437+ // Step 3: Send initialized notification
438+ if _ , err := io .WriteString (stdin , buildInitializedNotification ()+ "\n " ); err != nil {
439+ return "" , fmt .Errorf ("failed to write initialized notification: %w" , err )
440+ }
441+
442+ // Step 4: Send the actual request
407443 if _ , err := io .WriteString (stdin , jsonRequest + "\n " ); err != nil {
408- return "" , fmt .Errorf ("failed to write to stdin : %w" , err )
444+ return "" , fmt .Errorf ("failed to write request : %w" , err )
409445 }
410- _ = stdin .Close ()
411446
412- // Wait for the command to complete
413- if err := cmd .Wait (); err != nil {
414- return "" , fmt .Errorf ("command failed: %w, stderr: %s" , err , stderr .String ())
447+ // Step 5: Read the actual response (skip any server notifications)
448+ response , err := readJSONRPCResponse (scanner )
449+ if err != nil {
450+ return "" , fmt .Errorf ("failed to read response: %w, stderr: %s" , err , stderr .String ())
415451 }
416452
417- return stdout .String (), nil
453+ return response , nil
454+ }
455+
456+ // buildInitializeRequest creates the MCP initialize handshake request.
457+ func buildInitializeRequest () (string , error ) {
458+ id , err := rand .Int (rand .Reader , big .NewInt (10000 ))
459+ if err != nil {
460+ return "" , fmt .Errorf ("failed to generate random ID: %w" , err )
461+ }
462+ msg := map [string ]any {
463+ "jsonrpc" : "2.0" ,
464+ "id" : int (id .Int64 ()),
465+ "method" : "initialize" ,
466+ "params" : map [string ]any {
467+ "protocolVersion" : "2024-11-05" ,
468+ "capabilities" : map [string ]any {},
469+ "clientInfo" : map [string ]any {
470+ "name" : "mcpcurl" ,
471+ "version" : "0.1.0" ,
472+ },
473+ },
474+ }
475+ data , err := json .Marshal (msg )
476+ if err != nil {
477+ return "" , fmt .Errorf ("failed to marshal initialize request: %w" , err )
478+ }
479+ return string (data ), nil
480+ }
481+
482+ // buildInitializedNotification creates the MCP initialized notification.
483+ func buildInitializedNotification () string {
484+ return `{"jsonrpc":"2.0","method":"notifications/initialized"}`
485+ }
486+
487+ // readJSONRPCResponse reads lines from the scanner, skipping server-initiated
488+ // notifications (messages without an "id" field), and returns the first response.
489+ func readJSONRPCResponse (scanner * bufio.Scanner ) (string , error ) {
490+ for scanner .Scan () {
491+ line := scanner .Text ()
492+ // JSON-RPC responses have an "id" field; notifications do not.
493+ var msg map [string ]json.RawMessage
494+ if err := json .Unmarshal ([]byte (line ), & msg ); err != nil {
495+ return "" , fmt .Errorf ("failed to parse JSON-RPC message: %w" , err )
496+ }
497+ if _ , hasID := msg ["id" ]; hasID {
498+ if errField , hasErr := msg ["error" ]; hasErr {
499+ return "" , fmt .Errorf ("server returned error: %s" , string (errField ))
500+ }
501+ return line , nil
502+ }
503+ // No "id" — this is a notification, skip it
504+ }
505+ if err := scanner .Err (); err != nil {
506+ return "" , err
507+ }
508+ return "" , fmt .Errorf ("unexpected end of output" )
418509}
419510
420511func printResponse (response string , prettyPrint bool ) error {
0 commit comments