From 691836bafa98d3a05758cf1d1c6348d33e5148f3 Mon Sep 17 00:00:00 2001 From: Abhijit Date: Wed, 7 Oct 2026 21:41:52 +0530 Subject: [PATCH] mcpclient: report why a stdio server exited when the pipe breaks first A server that exits at once (bad config, missing token) can break the pipe before the client reads its exit; the write error then hid the server's stderr. Wait briefly for the exit and report it, as the read path does. Fixes TestStdioServerThatExits flaking on CI (main, 258cfcf). Co-Authored-By: Claude Opus 5.5 --- internal/mcpclient/client.go | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/internal/mcpclient/client.go b/internal/mcpclient/client.go index 61968eb..90b446f 100644 --- a/internal/mcpclient/client.go +++ b/internal/mcpclient/client.go @@ -282,7 +282,16 @@ func (t *stdioTransport) roundTrip(ctx context.Context, id int64, method string, t.wait[string(rid)] = ch t.pmu.Unlock() if err := t.write(rpcMsg{JSONRPC: "2.0", ID: &rid, Method: method, Params: params}); err != nil { - return nil, err + // a server that died before reading (bad config, missing token) + // breaks the pipe first: report why it exited, not the pipe + select { + case <-t.done: + return nil, t.exited() + case <-time.After(2 * time.Second): + return nil, err + case <-ctx.Done(): + return nil, err + } } select { case m := <-ch: @@ -291,12 +300,7 @@ func (t *stdioTransport) roundTrip(ctx context.Context, id int64, method string, } return m.Result, nil case <-t.done: - t.reap() // stderr is fully copied once the process is reaped - msg := strings.TrimSpace(t.stderr.String()) - if msg == "" && t.err != nil { - msg = t.err.Error() - } - return nil, fmt.Errorf("server exited: %s", lastLines(msg, 3)) + return nil, t.exited() case <-ctx.Done(): t.pmu.Lock() delete(t.wait, string(rid)) @@ -305,6 +309,16 @@ func (t *stdioTransport) roundTrip(ctx context.Context, id int64, method string, } } +// exited explains a server that went away, from its stderr. +func (t *stdioTransport) exited() error { + t.reap() // stderr is fully copied once the process is reaped + msg := strings.TrimSpace(t.stderr.String()) + if msg == "" && t.err != nil { + msg = t.err.Error() + } + return fmt.Errorf("server exited: %s", lastLines(msg, 3)) +} + func (t *stdioTransport) notify(_ context.Context, method string, params any) error { return t.write(rpcMsg{JSONRPC: "2.0", Method: method, Params: params}) }