modelcontextprotocol / modelcontextprotocol/go-sdk
stdio: a server that exits on the server/discover probe leaves Client.Connect no way to reach the documented initialize fallback
Nobody has claimed this yet.
- Dominant language
- Go
- Stars
- 5.1k
- Forks
- 543
- Avg merge
- 1d 17h
- Merged PRs (30d)
- 37
Description
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:
- Release the
ClientSessionOptions.ProtocolVersionexport (#1113, merged in #1127 on 2026-08-03, not in v1.7.0). With it, an embedder facing a known-bad upstream can pin2025-11-25and skip the probe entirely. That alone unblocks this case, and I'd be happy for that to be the whole answer. - 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.
- 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.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with Client.Connect in mcp/client.go and trace how CommandTransport handles a subprocess that exits during server/discover. Reproduce the case with a stdio server that exits unless the first method is initialize, then inspect the existing HTTP fallback and ProtocolVersion option. Done should be a maintainer-approved behavior or documentation path with coverage for the pre-initialize process-exit case.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go
- Domain
- api
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 42/100