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

Open
#1,177 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

P3
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:

  1. Release the ClientSessionOptions.ProtocolVersion export (#1113, merged in #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.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.