Skip to content

stdio: a server that exits on the server/discover probe leaves Client.Connect no way to reach the documented initialize fallback #1177

Description

@ta1kun

Summary

On a CommandTransport (stdio), the SEP-2575 server/discover probe is unrecoverable against a pre-2026-07-28 server that treats an unexpected first message as fatal: the subprocess exits, and the fallback documented in Client.Connect — "fall back to the legacy initialize handshake on any non-modern error from server/discover" (mcp/client.go) — never gets a chance to run, because there is no process left to send initialize to.

This is not hypothetical. @webclaw/mcp v0.6.19 (rmcp-based) does exactly this, and every dial from a v1.7.0 client fails:

[stderr] Error: ExpectedInitializeRequest(Some(Request(JsonRpcRequest { jsonrpc: JsonRpcVersion2_0,
    id: Number(1), request: CustomRequest(CustomRequest { method: "server/discover", ... }) })))
failed to connect: subprocess exited during startup

The same binary answers a plain initialize correctly (it negotiates 2025-06-18), and — notably — answers server/discover with a proper -32601 after the handshake. Only the pre-initialize path is fatal.

I've filed that as a server-side bug (0xMassi/webclaw#107), and I agree it is primarily their bug: the spec's fallback is built on the server replying. This issue is about the SDK-side blast radius, which I think is worth considering separately.

Why it's worth handling on this side too

For HTTP transports the fallback works as designed — a server that dislikes the probe answers with an HTTP or JSON-RPC error and the client moves on. On stdio the failure mode is categorically different: the peer is a process this SDK spawned, and its death takes the whole connection with it. So a spec-conformant client cannot connect to a non-conformant-but-otherwise-working server, and the user-visible error names server/discover — a method they never asked for — rather than anything actionable.

Repro

Any stdio server that exits on a non-initialize first message. Minimal shape, if you'd like one in-tree: a CommandTransport pointed at a binary that reads one line and calls os.Exit(1) unless the method is initialize; Client.Connect fails with the process exit rather than falling back.

Possible directions

Roughly in order of how much I'd expect maintainers to like them, not of preference:

  1. Release the ClientSessionOptions.ProtocolVersion export (No public way for a client to request a specific protocol version #1113, merged in mcp: export ClientSessionOptions.ProtocolVersion #1127 on 2026-08-03, not in v1.7.0). With it, an embedder facing a known-bad upstream can pin 2025-11-25 and skip the probe entirely. That alone unblocks this case, and I'd be happy for that to be the whole answer.
  2. Treat "connection died during the probe, before any reply" as a discover failure on stdio, and retry the handshake on a fresh process. More invasive — it means the transport, not the session, owns a respawn — so I mention it mostly for completeness.
  3. Document the hazard on CommandTransport / Client.Connect, so embedders know that a probe failure on stdio is not equivalent to one on HTTP.

Happy to send a PR for whichever shape you settle on, once maintainers have weighed in (per CONTRIBUTING, I'm not sending one ahead of that).

Workaround, for anyone searching

Wrap the mcp.Connection returned by the transport and answer server/discover locally with -32601, so the probe never reaches the subprocess and the client takes its own documented fallback path. That's what we ship today, gated behind a flag we intend to delete once (1) lands or the upstream server is fixed.

Environment: go-sdk v1.7.0, Go 1.25, Linux x86_64.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P3Nice to haves, rare edge cases

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions