Skip to content

Draft protocol: initialize can negotiate a modern protocol version #1680

Description

@cclabadmin

Describe the bug

The C# 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: d6b1615988d3a73ffb653d08778b14fa695e1318
  • Server used: tests/ModelContextProtocol.TestServer
  • Transport: stdio

To Reproduce

Steps to reproduce the behavior:

  1. Start the C# 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":{"protocolVersion":"2026-07-28","capabilities":{"logging":{},"prompts":{},"resources":{"subscribe":true},"tools":{},"completions":{}},"serverInfo":{"name":"TestServer","version":"1.0.0.0"},"instructions":"This is a test server with only stub functionality"}}

The response echoes "protocolVersion": "2026-07-28" — a revision that removed initialize.

This was observed over stdio. I have not separately confirmed the HTTP behavior; I mention ConfigureInitialize because the handler appears not to be stdio-specific.

Additional context

Looking at McpServerImpl.cs, the ConfigureInitialize handler appears to negotiate the version as follows:

string? protocolVersion = options.ProtocolVersion;
protocolVersion ??= request?.ProtocolVersion is string clientProtocolVersion &&
    McpSessionHandler.SupportedProtocolVersions.Contains(clientProtocolVersion) ?
    clientProtocolVersion :
    McpHttpHeaders.November2025ProtocolVersion;

Because SupportedProtocolVersions includes McpHttpHeaders.July2026ProtocolVersion ("2026-07-28"), this check appears to pass and the client's requested version is echoed. The fallback to November2025ProtocolVersion appears to trigger only for versions not in the supported list.

The code comment on lines ~373-377 describes the legacy initialize handshake as authoritative:

"The legacy initialize handshake is authoritative: it may supersede a protocol version a prior server/discover probe established on the same connection..."

That makes sense for choosing the legacy session state after a client has entered through initialize. The part that seems inconsistent with the draft versioning model is allowing that legacy entry point to negotiate a modern protocol revision.

One possible fix would be to cap initialize negotiation at November2025ProtocolVersion or the latest supported legacy revision, and return either a downgraded version or an error when a modern version is requested through the legacy handshake.

Related:

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions