Skip to content

initialize negotiates 2026-07-28 through the legacy handshake #1033

Description

@cclabadmin

Describe the bug

The Go SDK's initialize handler accepts protocolVersion: "2026-07-28" and echoes it back in the InitializeResult, even though the 2026-07-28 revision removed the initialize/notifications/initialized handshake and moved protocol version, client identity, and client capabilities to per-request _meta.

The draft specification's versioning page defines the two eras:

  • Modern (2026-07-28 and later): protocol versions that convey version, identity, and capabilities as per-request metadata.
  • Legacy (2025-11-25 and earlier): protocol versions that establish a session with an initialize handshake.

For dual-era servers, the spec says: "An initialize request selects legacy semantics, scoped to the stdio process (stdio) or the session (HTTP), as specified by the negotiated legacy protocol version." That seems to imply that initialize should negotiate one of the legacy revisions, not a modern revision such as 2026-07-28.

When initialize negotiates "2026-07-28", the session appears to claim a protocol revision that no longer defines the handshake that created it. The resulting state looks mixed: it declares a modern version, but it was established through the legacy initialization path without per-request _meta.

This appears to be a version-negotiation consistency issue.

Environment tested:

  • main snapshot from 2026-06-28: 65ece5964304528e7568507e2b17351824ef73fc
  • Server used: conformance/everything-server
  • Transport: stdio

To Reproduce

Steps to reproduce the behavior:

  1. Start the Go SDK stdio server.
  2. Send an initialize request with protocolVersion: "2026-07-28":
{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"protocolVersion":"2026-07-28","capabilities":{"roots":{"listChanged":true},"sampling":{},"elicitation":{}},"clientInfo":{"name":"repro","version":"0.1.0"}}}
  1. Observe the response.

Expected behavior

When a dual-era server receives initialize, I would expect it to negotiate a legacy protocol revision. In this case, I would expect the server to either:

  • Negotiate down to "2025-11-25" or the latest legacy revision it supports, since initialize selects legacy semantics; or
  • Reject the request with an error indicating that "2026-07-28" is not available through the legacy initialize handshake, optionally pointing the client to server/discover with per-request _meta.

I would not expect the legacy initialize path to echo "2026-07-28", because that makes the resulting session claim a protocol revision that does not define the handshake that created it.

Observed behavior

The server responds:

{"jsonrpc":"2.0","id":0,"result":{"capabilities":{"completions":{},"logging":{},"prompts":{"listChanged":true},"resources":{"listChanged":true,"subscribe":true},"tools":{"listChanged":true}},"protocolVersion":"2026-07-28","serverInfo":{"name":"mcp-conformance-test-server","version":"1.0.0"}}}

The response echoes "protocolVersion": "2026-07-28" — a revision that no longer defines the initialize handshake.

Additional context

Looking at mcp/shared.go (~line 53), the negotiatedVersion function has a comment that says: "Cap the supported versions at the legacy protocolVersion20251125, as this method is used by the initialize method which is deprecated in version protocolVersion20260728." However, the function body does not appear to implement this cap: it returns clientVersion as-is when the version is found in supportedProtocolVersions, which includes "2026-07-28".

func negotiatedVersion(clientVersion string) string {
    // Cap the supported versions at the legacy protocolVersion20251125 ...
    if !slices.Contains(supportedProtocolVersions, clientVersion) {
        return protocolVersion20251125
    }
    return clientVersion  // ← "2026-07-28" passes through uncapped
}

This looks like the comment describes the intended behavior, but the code does not enforce it. If the cap were applied, initialize would negotiate "2025-11-25" for a "2026-07-28" request, which would appear consistent with the spec's "negotiated legacy protocol version" model.

As a supporting observation: the InitializeResult echoed from this path also omits resultType, a field that the 2026-07-28 schema requires on all result objects. This is consistent with the session being in a mixed state — the negotiated version says 2026-07-28, but the response structure follows legacy semantics. I am tracking the resultType omission separately.

Related spec references:

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Fields

    No fields configured for Bug.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions