Skip to content

jsonrpc2: server-closing error formats writeErr with %v, so errors.Is(err, io.EOF) is false for consumers #1098

Description

@nudzo

Summary

When a connection shuts down because its write side failed, internal/jsonrpc2/conn.go builds the error like this (line 674 at current HEAD, same in v1.6.1):

err = fmt.Errorf("%w: %v", ErrServerClosing, s.writeErr)

The write error — typically io.EOF when a stdio host closes the pipe — is formatted with %v, not %w, so it is not in the error chain: errors.Is(err, io.EOF) is false for every consumer.

ErrServerClosing itself lives in the internal/jsonrpc2 tree, so downstream code cannot name it either. The combination leaves message-text matching as the only way to classify this error, which is exactly what nobody should do.

Why it matters

A stdio MCP server that wants to exit 0 on a clean host disconnect ("the session ended") and exit 1 on a real failure cannot tell the two apart. Concretely: our CLI's MCP server received a request, the host closed stdin while the response was in flight, and the server exited 1 printing server is closing: EOF — reporting a crash where the session merely ended. (We observed this deterministically: send one initialize request through a pipe and close stdin immediately.)

We've since worked around it by classifying shutdown from our own transport observation rather than from the returned error, so this is a good-citizenship report rather than a blocker.

Suggested fix

One character: %v%w on the s.writeErr. That makes errors.Is(err, io.EOF) (and any other sentinel the underlying writer returns) work as intended.

Possibly worth considering alongside: mcp/transport.go internally checks errors.Is(err, jsonrpc2.ErrServerClosing), which downstreams cannot express — a public predicate (or re-exported sentinel) for "the server is closing" would remove the last reason to look at the message text.

Related but distinct: #1061 describes response loss in the same stdin-EOF-while-in-flight scenario; this issue is only about the wrapping of the returned error.

Versions

  • github.com/modelcontextprotocol/go-sdk v1.6.1 (latest release); still present at HEAD (internal/jsonrpc2/conn.go:674).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Fields

    No fields configured for Enhancement.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions