Skip to content

R7: Dual-era MCP client: detect the server's era and fall back to legacy servers #1670

Open

Description

@jlouvel

Follow-up to #1640. McpClientAdapter can already speak both eras, but the caller picks one before start(): 2025-11-25 by default through the SDK client, or 2026-07-28 with setProtocolVersion(McpProtocol.MODERN_VERSION). It never detects which one the server speaks.

Most MCP servers in the wild are still legacy. A client that consumes third-party servers (for example, Ikanos' consumes: type: mcp) can't know the era in advance.

The 2026-07-28 Versioning and Compatibility page defines how a dual-era client does this. This issue implements it as the client side of the legacy bridge in #1671: the modern client is the core, and legacy servers are reached through a thin edge in org.restlet.ext.mcp.internal.legacy that exposes nothing 2026-07-28 deprecates or removes.

Delivery:

  1. Auto mode. A new default on McpClientAdapter (setProtocolVersion unset, or an explicit auto) that picks the era at start(). Explicit 2025-11-25 and 2026-07-28 keep today's behavior.
  2. Modern first, per the spec's transport bindings:
    • HTTP: the first modern request. A result, or a recognized modern JSON-RPC error such as UnsupportedProtocolVersionError (-32022), means a modern server. A 4xx without a recognized modern error body means a legacy server: fall back to initialize.
    • stdio: probe with server/discover. A DiscoverResult or a modern error means modern. Any other error, or a timeout, means legacy: fall back to initialize.
  3. Version selection. On a modern server, pick a mutually supported version from DiscoverResult.supportedVersions or from UnsupportedProtocolVersionError.data.supported, and retry. On a legacy server, accept the version initialize negotiates when it's one the SDK supports (2025-11-25, and earlier revisions if the SDK accepts them), instead of failing unless it's exactly McpProtocol.VERSION.
  4. Cache the era for the lifetime of the origin (HTTP) or the server process (stdio), as the spec recommends. Re-probe once if a cached assumption later fails.
  5. Legacy fallback limits. In fallback the client declares no roots, sampling or elicitation capabilities, never calls logging/setLevel, and rejects server-to-client requests. It opens no listening GET stream and never sends Mcp-Session-Id unless legacy sessions are turned on (item 6).
  6. Opt-in legacy sessions. Many legacy servers require Mcp-Session-Id. McpClientAdapter exposes it through explicitly legacy properties, off by default:
    • setLegacySessions(boolean): the edge keeps the id a legacy server returns on initialize, sends it on later requests, re-initializes once on a 404 (expired session), and sends a best-effort DELETE on close().
    • getLegacySessionId() / setLegacySessionId(String): read the id in use, or supply one the caller owns (for example a proxy relaying an upstream session). A caller-supplied id is sent but never deleted.
    • Ignored on modern servers. Javadoc states that 2026-07-28 removed sessions, that these properties go away with the legacy edge, and that the client still opens no GET stream, does not resume with Last-Event-ID, and rejects server-to-client requests, so servers depending on those won't work. Not marked @Deprecated, to keep callers who need it free of warnings.
  7. One facade, modern types. Era-neutral entry points for the common calls (listTools, callTool, readResource, getPrompt) return modern types. Legacy results are converted at the edge; a missing resultType means complete.
  8. Tests: dual-era client against a modern server, a stateless legacy server, a session-holding legacy server and a dual-era server (R6: Modern MCP core, with legacy MCP as a deletable bridge edge #1671). Cover fallback on HTTP and stdio, era caching and re-probe, version retry on -32022, legacy sessions (kept, sent, expired, deleted; caller-supplied id never deleted; no header when off), and assert on the wire that the fallback never sends a deprecated or removed method.

Out of scope: listening GET streams and Last-Event-ID resumption on legacy sessions, server-initiated elicitation, sampling and roots on legacy servers, and the deprecated HTTP+SSE transport. All are deprecated or removed in 2026-07-28. The sessionful transport spiked in #1659 provides the session handling behind item 6.

Open questions:

  • Should auto become the default, or stay opt-in while the SDK's modern API is a milestone?

Design memo: https://claude.ai/artifact/73QEwzKyg79YHVHjAyHdLd

Related: #1671, #1640, #1659, #1558. Ikanos consumer: naftiko/ikanos#1014.

Activity

  1. jlouvel commented on Nov 13, 2026

    @jlouvel
    CollaboratorAuthor

    Rescoped as the client side of the legacy bridge (#1671): modern first, legacy fallback exposes nothing 2026-07-28 deprecates or removes. Session-holding legacy servers and server-initiated elicitation on legacy sessions (former items 5 and 6) moved out of scope; see the updated description.

  2. jlouvel commented on Nov 13, 2026

    @jlouvel
    CollaboratorAuthor

    Session-holding legacy servers are back in scope, opt-in only: new item 6 adds explicitly legacy setLegacySessions / getLegacySessionId / setLegacySessionId properties on McpClientAdapter, off by default. GET streams, resumption and server-initiated requests stay out.

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