Skip to content

mcp: Client.Connect never falls back to initialize when a stdio server ignores server/discover #1332

Open

Description

@chaholl

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 exceeded

Expected 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

  1. crossxr commented on Oct 2, 2026

    @crossxr

    I'd like to work on this.

    I reproduced it on main (53effc0) with an IOTransport test double that drops everything except initialize and tools/list. Connect blocks until the caller's deadline, and the initialize fallback never runs because it reuses the same expired context.

    Bounding only the discover probe (context.WithTimeout around the c.discover call, outer ctx kept for initialize) fixes it locally, and the rest of go test ./mcp still passes. The remaining question is where the bound comes from:

    1. No new API: apply a short internal default only to stdio-style transports (CommandTransport, IOTransport), where the spec allows a server to stay silent before initialize. HTTP keeps the current behavior, since an HTTP server always responds.
    2. 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.

  2. guglielmo-san commented on Oct 2, 2026

    @guglielmo-san
    Contributor

    Hi @crossxr I think we can proceed with option 1

  3. crossxr commented on Oct 2, 2026

    @crossxr

    @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.

  4. added a commit that references this issue on Oct 2, 2026
    c481cad
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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions