Skip to content

ClientLifecycleMode::Auto does not fall back for deployed legacy-server responses #1040

Closed

Description

@tsarlandie-oai

Summary

ClientLifecycleMode::Auto only retries legacy initialize when
server/discover fails with JSON-RPC METHOD_NOT_FOUND (-32601). Existing
production MCP servers communicate unsupported modern discovery through other
recognizable responses, so enabling modern negotiation breaks servers that
previously worked.

Observed against deployed servers

Testing protocol 2026-07-28 against public legacy MCP services reproduced the
problem with:

  • OpenAI documentation, which exposes five tools through legacy initialization.
  • Microsoft Learn, which exposes three tools.
  • DeepWiki, which exposes three tools.

All three worked before modern discovery was enabled. With the modern lifecycle
enabled, startup failed with:

Unsupported protocol version: 2026-07-28

Codex’s existing rmcp 1.8 implementation connected to the same servers by retrying initialize with protocol 2025-06-18.

Current behavior

match discover_startup(...).await {
    Ok(()) => {}
    Err(ClientInitializeError::JsonRpcError(error))
        if error.code == ErrorCode::METHOD_NOT_FOUND =>
    {
        legacy_startup(...).await?;
    }
    Err(error) => return Err(error),
}

Production-shaped HTTP prevalidation responses observed during interoperability
testing include:

{
  "jsonrpc": "2.0",
  "id": null,
  "error": {
    "code": -32000,
    "message": "Bad Request: Unsupported protocol version: 2026-07-28 (supported versions: 2025-11-25, 2025-06-18, 2025-03-26, 2024-11-05, 2024-10-07)"
  }
}
{
  "jsonrpc": "2.0",
  "id": null,
  "error": {
    "code": -32000,
    "message": "Bad Request: No valid session ID provided"
  }
}

Both occur as initial HTTP 400 responses before a session exists. Other useful
legacy-only signals include correlated unsupported-version errors, discovery
results advertising exclusively historical protocol versions, and initial HTTP
404/405 responses.

Observed versus expected lifecycle

Actual:

→ server/discover, protocol 2026-07-28
← recognizable legacy-only rejection
✗ startup terminates without attempting initialize

Expected:

→ server/discover, protocol 2026-07-28
← recognizable legacy-only rejection
→ initialize, protocol 2025-06-18
← successful initialization
→ notifications/initialized
→ tools/list

Integration tests now reproduce these production-shaped responses and verify
the successful sequence
["server/discover", "initialize", "notifications/initialized"].

Downgrade safety

Fallback must remain conservative. Unrelated response IDs, arbitrary null-ID
errors, unknown or future protocol versions, HTTP 401/403 responses, existing-
session 404 responses, and ordinary modern-server rejections must not trigger
downgrade.

The observed public-server failures and successful reference-client comparison
demonstrate an existing interoperability regression rather than a hypothetical
extension to automatic lifecycle negotiation.

Activity

  1. added
    P1High: significant functionality gap or spec violation
    on Jul 28, 2026
  2. chrisbennight commented on Aug 3, 2026

    @chrisbennight

    Just adding two more failure cases I saw -


    rmcp 2.2.0 server, default (stateful) streamable-HTTP config - the message parses, but the session layer rejects any sessionless non-initialize request before dispatch:

    HTTP/1.1 422 Unprocessable Entity
    content-length: 45
    
    Unexpected message, expect initialize request

    https://github.com/modelcontextprotocol/rust-sdk/blob/830e088d733c7964c806a2305760dd8deb30dff9/crates/rmcp/src/transport/common/server_side_http.rs


    mcp-go v0.57.0 server, default config - the session manager treats the sessionless probe as an unknown session:

    HTTP/1.1 404 Not Found
    Content-Type: text/plain; charset=utf-8
    
    Invalid session ID

    https://github.com/mark3labs/mcp-go/blob/d7c7b06e4d2506f914eb8f53f35dc42feeb2e0f4/server/streamable_http.go


    Since these are transport level not sure #1088 will fix them, since it reuses the same transport I think

    What I ended up doing was adding retry logic in my client to work around it - dial once under Auto, and on any handshake failure re-dial once with ClientLifecycleMode::Initialize on a fresh transport (the refusal kills the transport worker, so there's nothing left to retry on). I ended up not matching on specific errors since, as noted above, every server phrases the refusal differently - and the status arrives flattened into UnexpectedServerResponse anyway. Awkward, but it works for me in a gateway where I need to paper over servers I don't control.

    I'd also point to https://modelcontextprotocol.io/specification/2026-07-28/basic/lifecycle#backward-compatibility-with-initialization-based-versions as a response to

    Fallback must remain conservative. Unrelated response IDs, arbitrary null-ID
    errors, unknown or future protocol versions, HTTP 401/403 responses, existing-
    session 404 responses, and ordinary modern-server rejections must not trigger
    downgrade.
    

    consider from the spec In both cases, a recognized modern JSON-RPC error (such as [UnsupportedProtocolVersionError](https://modelcontextprotocol.io/specification/2026-07-28/schema#unsupportedprotocolversionerror)) identifies a modern server: the client retries with a supported version rather than falling back. Anything else identifies a legacy server.. - I'd make the argument that downgrade is the conservative option / default for things that aren't a modern JSON-RPC response.

    (sorry if I misunderstood your point!)

  3. added 5 commits that reference this issue on Aug 6, 2026
    1e29393
    2af076e
    a81d9ed
    f3885a5
    e6ee5e0
  4. added a commit that references this issue on Aug 9, 2026
    22be2cc
  5. jamadeo commented on Aug 11, 2026

    @jamadeo
    Contributor

    Reopening as the linked PR is a good step but doesn't handle everything. Most importantly, in the HTTP transport case, we still need to trigger a fallback on empty or malformed 4xx responses, which requires some transport work.

  6. jamadeo commented on Aug 25, 2026

    @jamadeo
    Contributor

    @tsarlandie-oai I think this can now be closed with #1133 and #1211 both in. Please re-open if you see a remaining gap we need to cross here.

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

    P1High: significant functionality gap or spec violationbugSomething is not working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions