FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

Treat McpClientOptions.ProtocolVersion as the fallback floor and docu… · modelcontextprotocol/csharp-sdk@b898250 · GitHub

Repository navigation

Commit b898250

Browse files
andcommitted
Treat McpClientOptions.ProtocolVersion as the fallback floor and document it
A non-null ProtocolVersion is now both the requested version and the minimum the client will accept, so explicitly pinning a version disables the automatic legacy-server fallback and throws instead of silently downgrading below it. This removes the separate MinProtocolVersion option, which only duplicated that intent; callers that need to try multiple versions can leave ProtocolVersion unset or retry the connection with a different value. Update the July2026 fallback tests accordingly: the pin-and-fall-back cases now use the default (null) options, and the former Client_WithMinProtocolVersion test becomes a pinned-2026-refuses-fallback assertion. In the stateless docs, drop the removed MinProtocolVersion and McpSession.DraftProtocolVersion references from the opt-out-of-fallback guidance, and correct the "why is stateless the default" note: the 2025-11-25 (and older) revisions never required a server to use the Mcp-Session-Id header. The original SSE transport could only operate statefully, and keeping Streamable HTTP stateful by default preserved server-to-client requests on 2025-11-25. A client was required to echo a server-assigned session id, but assigning one was always the server's choice. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent c6d0f89 commit b898250

5 files changed

Lines changed: 44 additions & 89 deletions

File tree

‎docs/concepts/stateless/stateless.md‎

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ When sessions are enabled (`Stateless = false`), the server creates and tracks a
2424

2525
<!-- mlc-disable-next-line -->
2626
> [!NOTE]
27-
> **Why is stateless now the default?** Earlier versions of the SDK defaulted to stateful for back-compat with the `2025-11-25` (and older) protocol revisions, which require the `Mcp-Session-Id` header. The `2026-07-28` protocol revision removes that header (SEP-2567) and the `initialize` handshake (SEP-2575) entirely, so the SDK now defaults <xref:ModelContextProtocol.AspNetCore.HttpServerTransportOptions.Stateless> to `true` to match the new wire format. You can still opt back into sessions with `Stateless = false` for [unsolicited notifications](#how-streamable-http-delivers-messages), resource subscriptions, per-client isolation, or server-to-client requests against clients that don't support [MRTR](xref:mrtr) — see [Stateful mode (sessions)](#stateful-mode-sessions).
27+
> **Why is stateless now the default?** Earlier versions of the SDK defaulted to stateful, but not because the `2025-11-25` (and older) protocol revisions ever required a server to use the `Mcp-Session-Id` header. They didn't. The original SSE transport could only operate statefully, and keeping Streamable HTTP stateful by default let server-to-client requests (elicitation, sampling, roots) keep working on `2025-11-25` the way they always had. A client was required to echo a server-assigned `Mcp-Session-Id` on later requests, but whether to assign one was always the server's choice. The `2026-07-28` protocol revision removes the header (SEP-2567) and the `initialize` handshake (SEP-2575) from the wire format entirely, and server-to-client requests now run through [MRTR](xref:mrtr), so the SDK now defaults <xref:ModelContextProtocol.AspNetCore.HttpServerTransportOptions.Stateless> to `true` to match the new wire format. You can still opt back into sessions with `Stateless = false` for [unsolicited notifications](#how-streamable-http-delivers-messages), resource subscriptions, per-client isolation, or server-to-client requests against clients that don't support [MRTR](xref:mrtr) — see [Stateful mode (sessions)](#stateful-mode-sessions).
2828
2929
## Forward and backward compatibility
3030

@@ -53,13 +53,12 @@ The `2026-07-28` protocol revision goes further than `Stateless = true`: it remo
5353

5454
The era is cached per <xref:ModelContextProtocol.Client.IClientTransport> instance, so the probe cost is paid only on the first connect.
5555

56-
**Opting out of fallback.** Set <xref:ModelContextProtocol.Client.McpClientOptions.MinProtocolVersion> to <xref:ModelContextProtocol.McpSession.DraftProtocolVersion> when you want the client to refuse to fall back. The connect call throws an <xref:ModelContextProtocol.McpException> instead of silently degrading. This is useful for strict-modern production code and for tests that need to assert `2026-07-28`-only behavior.
56+
**Opting out of fallback.** Pin <xref:ModelContextProtocol.Client.McpClientOptions.ProtocolVersion> to `2026-07-28` when you want the client to refuse to fall back. A non-null `ProtocolVersion` is also treated as the minimum, so the connect call throws an <xref:ModelContextProtocol.McpException> instead of silently degrading to a legacy revision. This is useful for strict-modern production code and for tests that need to assert `2026-07-28`-only behavior. To try several versions yourself, leave `ProtocolVersion` unset (the default) or retry the connection with a different value.
5757

5858
```csharp
5959
var clientOptions = new McpClientOptions
6060
{
61-
ProtocolVersion = McpSession.DraftProtocolVersion,
62-
MinProtocolVersion = McpSession.DraftProtocolVersion,
61+
ProtocolVersion = "2026-07-28",
6362
};
6463
```
6564

‎src/ModelContextProtocol.Core/Client/McpClientImpl.cs‎

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -392,13 +392,15 @@ public async Task ConnectAsync(CancellationToken cancellationToken = default)
392392
.FirstOrDefault()
393393
?? McpHttpHeaders.November2025ProtocolVersion;
394394

395-
// Honor MinProtocolVersion: refuse to fall back below the configured minimum.
396-
// String.Compare is the spec's prescribed ordering for ISO-8601 date-based versions.
397-
if (_options.MinProtocolVersion is { } minVersion &&
398-
StringComparer.Ordinal.Compare(fallbackVersion, minVersion) < 0)
395+
// A non-null ProtocolVersion is also the minimum: refuse to fall back below the
396+
// explicitly requested version. String.Compare is the spec's prescribed ordering
397+
// for ISO-8601 date-based versions.
398+
if (_options.ProtocolVersion is { } pinnedVersion &&
399+
StringComparer.Ordinal.Compare(fallbackVersion, pinnedVersion) < 0)
399400
{
400401
throw new McpException(
401-
$"Server does not support the configured minimum protocol version '{minVersion}'. " +
402+
$"The server does not support the requested protocol version '{pinnedVersion}'. " +
403+
"Leave McpClientOptions.ProtocolVersion unset to allow automatic fallback to an older version. " +
402404
(serverSupportedVersions is null
403405
? "The server appears to be a legacy server that requires the deprecated initialize handshake."
404406
: $"Server-supported versions: {string.Join(", ", serverSupportedVersions)}."));
@@ -495,15 +497,6 @@ private async Task PerformLegacyInitializeAsync(string requestProtocol, Cancella
495497
throw new McpException($"Server protocol version mismatch. Expected {requestProtocol}, got {initializeResponse.ProtocolVersion}");
496498
}
497499

498-
// If the user set a MinProtocolVersion, also enforce it against the negotiated response
499-
// (the server could have downgraded further than the version we asked for).
500-
if (_options.MinProtocolVersion is { } minVersion &&
501-
StringComparer.Ordinal.Compare(initializeResponse.ProtocolVersion, minVersion) < 0)
502-
{
503-
throw new McpException(
504-
$"Server negotiated protocol version '{initializeResponse.ProtocolVersion}' is below the configured minimum '{minVersion}'.");
505-
}
506-
507500
_negotiatedProtocolVersion = initializeResponse.ProtocolVersion;
508501
_sessionHandler.NegotiatedProtocolVersion = _negotiatedProtocolVersion;
509502

‎src/ModelContextProtocol.Core/Client/McpClientOptions.cs‎

Lines changed: 10 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -52,51 +52,22 @@ public sealed class McpClientOptions
5252
/// </summary>
5353
/// <remarks>
5454
/// <para>
55-
/// When non-<see langword="null"/>, this version is requested from the server. Setting it to a
56-
/// version that still supports Streamable HTTP sessions, such as <c>2025-11-25</c>, opts out of the
57-
/// <c>2026-07-28</c> revision and forces the <c>initialize</c> handshake; the handshake then fails if
58-
/// the server's negotiated version does not match.
59-
/// </para>
60-
/// <para>
6155
/// When <see langword="null"/> (the default), the client prefers the latest revision (<c>2026-07-28</c>),
62-
/// which removed the <c>initialize</c> handshake and Streamable HTTP sessions: it probes with
63-
/// <c>server/discover</c> and automatically falls back to a legacy <c>initialize</c> handshake,
56+
/// which removed the <c>initialize</c> handshake and Streamable HTTP sessions. It probes with
57+
/// <c>server/discover</c> and automatically falls back to the legacy <c>initialize</c> handshake,
6458
/// downgrading to any version the server advertises, when the server does not support that revision.
6559
/// </para>
66-
/// </remarks>
67-
public string? ProtocolVersion { get; set; }
68-
69-
/// <summary>
70-
/// Gets or sets the minimum protocol version the client will accept during version negotiation.
71-
/// </summary>
72-
/// <remarks>
73-
/// <para>
74-
/// When negotiating with a server that advertises multiple supported versions, or when falling back
75-
/// to a legacy server, the client will refuse any version older than this minimum and surface an
76-
/// <see cref="McpException"/> instead.
77-
/// </para>
7860
/// <para>
79-
/// This is useful when the client requires features that older protocol revisions lack, such as the
80-
/// <c>2026-07-28</c> revision's removal of the <c>initialize</c> handshake and <c>Mcp-Session-Id</c>.
81-
/// Because the client already prefers <c>2026-07-28</c> by default, setting this to
82-
/// <c>2026-07-28</c> disables the automatic legacy-server fallback that otherwise switches to the
83-
/// <c>initialize</c> handshake.
61+
/// When non-<see langword="null"/>, this value is both the requested version and the minimum the client
62+
/// will accept: the client requests exactly this version and refuses to downgrade below it, throwing an
63+
/// <see cref="McpException"/> instead of falling back. Setting it to <c>2026-07-28</c> therefore disables
64+
/// the automatic legacy-server fallback, and setting it to a version that still supports Streamable HTTP
65+
/// sessions, such as <c>2025-11-25</c>, forces the <c>initialize</c> handshake and fails if the server
66+
/// negotiates a different version. To try more than one version, leave this unset for automatic fallback
67+
/// or retry the connection with a different value.
8468
/// </para>
85-
/// <para>
86-
/// If <see langword="null"/> (the default), the client falls back to any version the server
87-
/// advertises, including legacy versions such as 2025-11-25.
88-
/// </para>
89-
/// <example>
90-
/// <code>
91-
/// // 2026-07-28 is already the default; pin the minimum to refuse the legacy fallback.
92-
/// var clientOptions = new McpClientOptions
93-
/// {
94-
/// MinProtocolVersion = "2026-07-28",
95-
/// };
96-
/// </code>
97-
/// </example>
9869
/// </remarks>
99-
public string? MinProtocolVersion { get; set; }
70+
public string? ProtocolVersion { get; set; }
10071

10172
/// <summary>
10273
/// Gets or sets a timeout for the client-server initialization handshake sequence.

‎tests/ModelContextProtocol.AspNetCore.Tests/July2026ProtocolHttpFallbackTests.cs‎

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -157,10 +157,9 @@ await StartServerAsync(async context =>
157157
Endpoint = new("http://localhost:5000/mcp"),
158158
}, HttpClient, LoggerFactory);
159159

160-
await using var client = await McpClient.CreateAsync(transport, new McpClientOptions
161-
{
162-
ProtocolVersion = McpHttpHeaders.July2026ProtocolVersion,
163-
}, loggerFactory: LoggerFactory, cancellationToken: ct);
160+
// Default options prefer 2026-07-28 but allow automatic fallback to a legacy server.
161+
await using var client = await McpClient.CreateAsync(transport, new McpClientOptions(),
162+
loggerFactory: LoggerFactory, cancellationToken: ct);
164163

165164
Assert.Equal("2025-06-18", client.NegotiatedProtocolVersion);
166165

@@ -245,10 +244,9 @@ await StartServerAsync(async context =>
245244
Endpoint = new("http://localhost:5000/mcp"),
246245
}, HttpClient, LoggerFactory);
247246

248-
await using var client = await McpClient.CreateAsync(transport, new McpClientOptions
249-
{
250-
ProtocolVersion = McpHttpHeaders.July2026ProtocolVersion,
251-
}, loggerFactory: LoggerFactory, cancellationToken: ct);
247+
// Default options prefer 2026-07-28 but allow automatic fallback to a legacy server.
248+
await using var client = await McpClient.CreateAsync(transport, new McpClientOptions(),
249+
loggerFactory: LoggerFactory, cancellationToken: ct);
252250

253251
Assert.Equal("2025-11-25", client.NegotiatedProtocolVersion);
254252
}

‎tests/ModelContextProtocol.Tests/Client/July2026ProtocolFallbackTests.cs‎

Lines changed: 18 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,16 @@ namespace ModelContextProtocol.Tests.Client;
99

1010
/// <summary>
1111
/// Regression tests for the fallback from the 2026-07-28 protocol revision to a legacy protocol in
12-
/// <see cref="McpClient"/>. These verify that a client configured with
13-
/// <c>McpClientOptions.ProtocolVersion = McpHttpHeaders.July2026ProtocolVersion</c>
14-
/// correctly probes a server with <c>server/discover</c>, falls
15-
/// back to the legacy <c>initialize</c> handshake when the server is legacy,
16-
/// and accepts whatever supported protocol version the legacy server
17-
/// negotiates, including a version different from the one the client
18-
/// originally requested.
12+
/// <see cref="McpClient"/>. With default options (<c>ProtocolVersion = null</c>) the client prefers
13+
/// 2026-07-28 but probes with <c>server/discover</c>, falls back to the legacy <c>initialize</c>
14+
/// handshake when the server is legacy, and accepts whatever supported protocol version the legacy
15+
/// server negotiates. Pinning <c>ProtocolVersion</c> to <c>2026-07-28</c> instead makes it the
16+
/// minimum too, so the client refuses to fall back.
1917
/// </summary>
2018
/// <remarks>
21-
/// The originally shipped logic in <c>PerformLegacyInitializeAsync</c> compared
22-
/// the server's response against <c>_options.ProtocolVersion</c>, which for a
23-
/// 2026-07-28 client is <c>"2026-07-28"</c>. When the legacy server downgraded to (say)
24-
/// <c>"2025-06-18"</c>, the comparison threw, even though the legacy
25-
/// negotiation succeeded. These tests guard against that regression.
19+
/// The originally shipped logic in <c>PerformLegacyInitializeAsync</c> compared the server's response
20+
/// against the requested version and threw when a legacy server downgraded to (say) <c>"2025-06-18"</c>,
21+
/// even though the legacy negotiation succeeded. These tests guard against that regression.
2622
/// </remarks>
2723
public class July2026ProtocolFallbackTests(ITestOutputHelper testOutputHelper) : LoggedTest(testOutputHelper)
2824
{
@@ -32,10 +28,9 @@ public async Task Client_OnMethodNotFound_FallsBackTo_Initialize_AcceptsDowngrad
3228
var ct = TestContext.Current.CancellationToken;
3329
await using var transport = new LegacyServerTestTransport(serverNegotiatedVersion: "2025-06-18");
3430

35-
await using var client = await McpClient.CreateAsync(transport, new McpClientOptions
36-
{
37-
ProtocolVersion = McpHttpHeaders.July2026ProtocolVersion,
38-
}, loggerFactory: LoggerFactory, cancellationToken: ct);
31+
// Default options (ProtocolVersion = null) prefer 2026-07-28 but allow automatic fallback.
32+
await using var client = await McpClient.CreateAsync(transport, new McpClientOptions(),
33+
loggerFactory: LoggerFactory, cancellationToken: ct);
3934

4035
Assert.True(transport.ServerDiscoverProbed);
4136
Assert.True(transport.LegacyInitializeReceived);
@@ -50,17 +45,16 @@ public async Task Client_OnInvalidParams_FallsBackTo_Initialize()
5045
serverNegotiatedVersion: "2025-11-25",
5146
probeErrorCode: (int)McpErrorCode.InvalidParams);
5247

53-
await using var client = await McpClient.CreateAsync(transport, new McpClientOptions
54-
{
55-
ProtocolVersion = McpHttpHeaders.July2026ProtocolVersion,
56-
}, loggerFactory: LoggerFactory, cancellationToken: ct);
48+
// Default options (ProtocolVersion = null) prefer 2026-07-28 but allow automatic fallback.
49+
await using var client = await McpClient.CreateAsync(transport, new McpClientOptions(),
50+
loggerFactory: LoggerFactory, cancellationToken: ct);
5751

5852
Assert.True(transport.LegacyInitializeReceived);
5953
Assert.Equal("2025-11-25", client.NegotiatedProtocolVersion);
6054
}
6155

6256
[Fact]
63-
public async Task Client_WithMinProtocolVersion_RefusesFallback_BelowMinimum()
57+
public async Task Client_WithPinnedJuly2026Version_RefusesFallback_ToLegacyServer()
6458
{
6559
var ct = TestContext.Current.CancellationToken;
6660
await using var transport = new LegacyServerTestTransport(serverNegotiatedVersion: "2025-06-18");
@@ -69,13 +63,13 @@ public async Task Client_WithMinProtocolVersion_RefusesFallback_BelowMinimum()
6963
{
7064
await using var client = await McpClient.CreateAsync(transport, new McpClientOptions
7165
{
66+
// Pinning the version makes it the minimum too, so the client refuses to fall back.
7267
ProtocolVersion = McpHttpHeaders.July2026ProtocolVersion,
73-
MinProtocolVersion = McpHttpHeaders.July2026ProtocolVersion,
7468
}, loggerFactory: LoggerFactory, cancellationToken: ct);
7569
});
7670

7771
Assert.IsType<McpException>(exception);
78-
Assert.Contains("minimum", exception.Message, StringComparison.OrdinalIgnoreCase);
72+
Assert.Contains("2026-07-28", exception.Message, StringComparison.OrdinalIgnoreCase);
7973
}
8074

8175
[Fact]
@@ -133,9 +127,9 @@ public async Task Client_OnSilentProbe_FallsBackTo_Initialize_AfterConfiguredPro
133127
silentDiscoverProbe: true);
134128

135129
var stopwatch = Stopwatch.StartNew();
130+
// Default options (ProtocolVersion = null) prefer 2026-07-28 but allow automatic fallback.
136131
await using var client = await McpClient.CreateAsync(transport, new McpClientOptions
137132
{
138-
ProtocolVersion = McpHttpHeaders.July2026ProtocolVersion,
139133
DiscoverProbeTimeout = TimeSpan.FromMilliseconds(250),
140134
InitializationTimeout = TestConstants.DefaultTimeout,
141135
}, loggerFactory: LoggerFactory, cancellationToken: ct);

0 commit comments

Comments
 (0)

Back | FazBrowse Home | New Git URL