mcp: Client.Connect never falls back to initialize when a stdio server ignores server/discover #1332
Description
Describe the bug
For protocol ≥ 2026-07-28, Client.Connect sends server/discover first and falls back to initialize on an error, or on a non-modern error from the server. Some handshake-era stdio servers silently ignore requests they don't recognize before initialize, and the stdio transport spec permits that. With such a server, server/discover never gets a reply, so Connect blocks until the caller's context ends and then fails. It never reaches the initialize fallback.
Setting ClientSessionOptions{ProtocolVersion: "2025-11-25"} connects immediately, but then the client can't use 2026-07-28 with any server.
To Reproduce
A stdio server that only answers initialize and tools/list and drops everything else:
import sys, json
for line in sys.stdin:
m = json.loads(line)
if m.get("method") == "initialize":
r = {"protocolVersion": "2025-11-25", "capabilities": {"tools": {}}, "serverInfo": {"name": "silent", "version": "1"}}
elif m.get("method") == "tools/list":
r = {"tools": []}
else:
continue
print(json.dumps({"jsonrpc": "2.0", "id": m["id"], "result": r}), flush=True)ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_, err := client.Connect(ctx, &mcp.CommandTransport{Command: exec.Command("python3", "silent.py")}, nil)
// v1.8.0 and main at 53effc0: returns after 5s with context deadline exceededExpected behavior
This needs an API decision, so raising it before any PR. One option: bound the discover probe and treat "no reply in time" as a handshake-era server on transports where silence is possible, e.g. ClientSessionOptions.DiscoverTimeout, or a short default for CommandTransport.
Additional context
Found while moving PromptKit's MCP client onto go-sdk v1.8.0 (AltairaLabs/PromptKit#2141). We work around it by restarting the stdio server and connecting with the handshake-era protocol version when discover times out: client.go. Our interop matrix runs the silent server above as a fixture: silent_stdio.py.
Activity
crossxr commentedon Oct 2, 2026
I'd like to work on this.
I reproduced it on main (53effc0) with an
IOTransporttest double that drops everything exceptinitializeandtools/list.Connectblocks until the caller's deadline, and theinitializefallback never runs because it reuses the same expired context.Bounding only the discover probe (
context.WithTimeoutaround thec.discovercall, outer ctx kept forinitialize) fixes it locally, and the rest ofgo test ./mcpstill passes. The remaining question is where the bound comes from:CommandTransport,IOTransport), where the spec allows a server to stay silent beforeinitialize. HTTP keeps the current behavior, since an HTTP server always responds.ClientSessionOptions.DiscoverTimeout: as you suggested, with zero meaning the per-transport default from (1). This would need a proposal.I'd lean toward (1) as the bug fix, with (2) as a follow-up proposal if people want to tune it. A stdio server that answers discover late is harmless: the client has already sent
initialize, which modern servers still accept, and the stray response is dropped.Happy to send a PR for whichever you prefer.
guglielmo-san commentedon Oct 2, 2026
Hi @crossxr I think we can proceed with option 1
crossxr commentedon Oct 2, 2026
@guglielmo-san thanks! I opened #1334 with option 1. It bounds the discover probe at 3s (or half of the caller's remaining deadline, if that's shorter) on stdio transports only, and adds tests for a silent server, a short deadline, and a slow server.
Since it's my first PR here, the CI workflows are waiting for a maintainer to approve the run.
mcp: bound server/discover probe on stdio transports