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,124 @@ 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 any error after Start()
412+ cleanup := func () {
413+ _ = stdin .Close ()
414+ _ = cmd .Wait ()
415+ }
416+
417+ // Use a scanner with a large buffer for reading JSON-RPC responses
418+ scanner := bufio .NewScanner (stdoutPipe )
419+ scanner .Buffer (make ([]byte , 0 , 1024 * 1024 ), 1024 * 1024 ) // 1MB max line size
420+
421+ // Step 1: Send MCP initialize request
422+ initReq , err := buildInitializeRequest ()
423+ if err != nil {
424+ cleanup ()
425+ return "" , fmt .Errorf ("failed to build initialize request: %w" , err )
426+ }
427+ if _ , err := io .WriteString (stdin , initReq + "\n " ); err != nil {
428+ cleanup ()
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+ cleanup ()
435+ return "" , fmt .Errorf ("failed to read initialize response: %w, stderr: %s" , err , stderr .String ())
436+ }
437+
438+ // Step 3: Send initialized notification
439+ if _ , err := io .WriteString (stdin , buildInitializedNotification ()+ "\n " ); err != nil {
440+ cleanup ()
441+ return "" , fmt .Errorf ("failed to write initialized notification: %w" , err )
442+ }
443+
444+ // Step 4: Send the actual request
407445 if _ , err := io .WriteString (stdin , jsonRequest + "\n " ); err != nil {
408- return "" , fmt .Errorf ("failed to write to stdin: %w" , err )
446+ cleanup ()
447+ return "" , fmt .Errorf ("failed to write request: %w" , err )
448+ }
449+
450+ // Step 5: Read the actual response (skip any server notifications)
451+ response , err := readJSONRPCResponse (scanner )
452+ if err != nil {
453+ cleanup ()
454+ return "" , fmt .Errorf ("failed to read response: %w, stderr: %s" , err , stderr .String ())
409455 }
410- _ = stdin .Close ()
411456
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 ())
457+ // Close stdin and wait for process to exit. The server will see EOF and
458+ // exit with a non-zero status, which is expected — we already have the response.
459+ cleanup ()
460+
461+ return response , nil
462+ }
463+
464+ // buildInitializeRequest creates the MCP initialize handshake request.
465+ func buildInitializeRequest () (string , error ) {
466+ id , err := rand .Int (rand .Reader , big .NewInt (10000 ))
467+ if err != nil {
468+ return "" , fmt .Errorf ("failed to generate random ID: %w" , err )
469+ }
470+ msg := map [string ]any {
471+ "jsonrpc" : "2.0" ,
472+ "id" : int (id .Int64 ()),
473+ "method" : "initialize" ,
474+ "params" : map [string ]any {
475+ "protocolVersion" : "2024-11-05" ,
476+ "capabilities" : map [string ]any {},
477+ "clientInfo" : map [string ]any {
478+ "name" : "mcpcurl" ,
479+ "version" : "0.1.0" ,
480+ },
481+ },
415482 }
483+ data , err := json .Marshal (msg )
484+ if err != nil {
485+ return "" , fmt .Errorf ("failed to marshal initialize request: %w" , err )
486+ }
487+ return string (data ), nil
488+ }
489+
490+ // buildInitializedNotification creates the MCP initialized notification.
491+ func buildInitializedNotification () string {
492+ return `{"jsonrpc":"2.0","method":"notifications/initialized"}`
493+ }
416494
417- return stdout .String (), nil
495+ // readJSONRPCResponse reads lines from the scanner, skipping server-initiated
496+ // notifications (messages without an "id" field), and returns the first response.
497+ func readJSONRPCResponse (scanner * bufio.Scanner ) (string , error ) {
498+ for scanner .Scan () {
499+ line := scanner .Text ()
500+ // JSON-RPC responses have an "id" field; notifications do not.
501+ var msg map [string ]json.RawMessage
502+ if err := json .Unmarshal ([]byte (line ), & msg ); err != nil {
503+ return "" , fmt .Errorf ("failed to parse JSON-RPC message: %w" , err )
504+ }
505+ if _ , hasID := msg ["id" ]; hasID {
506+ return line , nil
507+ }
508+ // No "id" — this is a notification, skip it
509+ }
510+ if err := scanner .Err (); err != nil {
511+ return "" , err
512+ }
513+ return "" , fmt .Errorf ("unexpected end of output" )
418514}
419515
420516func printResponse (response string , prettyPrint bool ) error {
0 commit comments