Skip to content

[v2] Era-negotiation probe drops spec-conformant server/discover responses (top-level resultType/_meta) and misclassifies modern servers as legacy #2722

Description

@Seinra

What happened?

Connecting a Client (stdio transport, pinned negotiation versionNegotiation: { mode: { pin: "2026-07-28" } }) to a spec-conformant MCP 2026-07-28 server fails:

SdkError [EraNegotiationFailed]: Version negotiation failed: the server did not offer
pinned protocol version 2026-07-28 via server/discover (no fallback in pin mode)

The server's response is spec-conformant per the 2026-07-28 Server Discovery spec example, where resultType and _meta appear as top-level siblings of result. Wire-captured exchange (server side verified correct via a binary stdio interceptor):

{"jsonrpc":"2.0","id":"server-discover-probe-1","resultType":"complete","result":{"supportedVersions":["2026-07-28"],"capabilities":{"tools":{"listChanged":false}},"instructions":"..."},"_meta":{"io.modelcontextprotocol/serverInfo":{"name":"<server-name>","version":"0.1.0"}}}

Root cause chain (dist line numbers from @modelcontextprotocol/client 2.0.0 / @modelcontextprotocol/core 2.0.0):

  1. JSONRPCResultResponseSchema = z.object({ jsonrpc, id, result }).strict() (@modelcontextprotocol/core/dist/auth-CUe6YdwF.mjs:191-197; mirrored as wireResultResponse(...).strict() in client/dist/src-D_zzAWoS.mjs:3178) rejects the unknown top-level members resultType / _meta.
  2. ProbeWindow.open's transport.onmessage guard (client/dist/index.mjs:2518-2524) resolves pending probes only when isJSONRPCResultResponse(...) || isJSONRPCErrorResponse(...); the rejected message is dropped silently.
  3. On timeout, the stdio classifier returns {kind:'legacy'} (index.mjs:2321-2323), so pin mode throws at index.mjs:2727 — a modern, spec-literal server gets misdiagnosed as legacy.
  4. Related: decodeResult (src-D_zzAWoS.mjs:3920+) runs only on the post-handshake request path (~line 6120), never during the probe, so even a non-dropped envelope would reach classifyResult's validateResult without decoding.

A raw newline-delimited JSON probe sending exactly this request shape succeeds against the same binary — only the SDK path fails.

What did you expect?

Per the 2026-07-28 Server Discovery spec, the probe should decode the response envelope before classification (or tolerate extra top-level members), and a silently-dropped/timed-out probe reply should surface as a diagnosable error rather than being classified as "legacy".

Suggested fixes (any of)

  • Run decodeResult in the probe path before classifyResult.
  • Relax .strict() → passthrough when parsing response envelopes in the probe window.
  • Minimum viable: log when a probe reply fails envelope parsing instead of silently timing out into a misleading legacy verdict.

Code to reproduce

import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";

// Any stdio server answering server/discover with the spec-example shape
// (resultType/_meta as top-level siblings of result):
const transport = new StdioClientTransport({ command: "<server-command>" });
const client = new Client(
  { name: "repro", version: "1.0.0" },
  { versionNegotiation: { mode: { pin: "2026-07-28" } }, capabilities: {} },
);
await client.connect(transport); // -> EraNegotiationFailed after ~60s silent timeout

SDK version

@modelcontextprotocol/client 2.0.0, @modelcontextprotocol/core 2.0.0

Area

Client

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