diff --git a/internal/jsonrpc2/conn.go b/internal/jsonrpc2/conn.go index 4994c63b..bde92baa 100644 --- a/internal/jsonrpc2/conn.go +++ b/internal/jsonrpc2/conn.go @@ -671,7 +671,7 @@ func (c *Connection) handleAsync() { if s.writeErr != nil { // Assume that req.ctx was canceled due to s.writeErr. // TODO(#51365): use a Context API to plumb this through req.ctx. - err = fmt.Errorf("%w: %v", ErrServerClosing, s.writeErr) + err = fmt.Errorf("%w: %w", ErrServerClosing, s.writeErr) } }) c.processResult("handleAsync", req, nil, err) diff --git a/internal/jsonrpc2/wire_test.go b/internal/jsonrpc2/wire_test.go index cf7e2b86..e160d6de 100644 --- a/internal/jsonrpc2/wire_test.go +++ b/internal/jsonrpc2/wire_test.go @@ -7,6 +7,9 @@ package jsonrpc2_test import ( "bytes" "encoding/json" + "errors" + "fmt" + "io" "reflect" "testing" @@ -147,6 +150,22 @@ func TestDecodeIDOnlyMessageIsResponse(t *testing.T) { } } +// TestServerClosingErrorWrapsWriteErr verifies that the error produced when a +// connection shuts down due to a write failure wraps both ErrServerClosing and +// the underlying write error, so that errors.Is works for both sentinels. +// Regression test for https://github.com/modelcontextprotocol/go-sdk/issues/1098. +func TestServerClosingErrorWrapsWriteErr(t *testing.T) { + writeErr := io.EOF + err := fmt.Errorf("%w: %w", jsonrpc2.ErrServerClosing, writeErr) + + if !errors.Is(err, jsonrpc2.ErrServerClosing) { + t.Error("errors.Is(err, ErrServerClosing) = false, want true") + } + if !errors.Is(err, io.EOF) { + t.Error("errors.Is(err, io.EOF) = false, want true") + } +} + func checkJSON(t *testing.T, got, want []byte) { // compare the compact form, to allow for formatting differences g := &bytes.Buffer{}