| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent c6d0f89 commit b898250
5 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -24,7 +24,7 @@ When sessions are enabled (`Stateless = false`), the server creates and tracks a | |||
| 24 | 24 | ||
| 25 | 25 | <!-- mlc-disable-next-line --> | |
| 26 | 26 | > [!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). | ||
| 28 | 28 | ||
| 29 | 29 | ## Forward and backward compatibility | |
| 30 | 30 | ||
@@ -53,13 +53,12 @@ The `2026-07-28` protocol revision goes further than `Stateless = true`: it remo | |||
| 53 | 53 | ||
| 54 | 54 | The era is cached per <xref:ModelContextProtocol.Client.IClientTransport> instance, so the probe cost is paid only on the first connect. | |
| 55 | 55 | ||
| 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. | ||
| 57 | 57 | ||
| 58 | 58 | ```csharp | |
| 59 | 59 | var clientOptions = new McpClientOptions | |
| 60 | 60 | { | |
| 61 | - ProtocolVersion = McpSession.DraftProtocolVersion, | ||
| 62 | - MinProtocolVersion = McpSession.DraftProtocolVersion, | ||
| 61 | + ProtocolVersion = "2026-07-28", | ||
| 63 | 62 | }; | |
| 64 | 63 | ``` | |
| 65 | 64 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -392,13 +392,15 @@ public async Task ConnectAsync(CancellationToken cancellationToken = default) | |||
| 392 | 392 | .FirstOrDefault() | |
| 393 | 393 | ?? McpHttpHeaders.November2025ProtocolVersion; | |
| 394 | 394 | ||
| 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) | ||
| 399 | 400 | { | |
| 400 | 401 | 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. " + | ||
| 402 | 404 | (serverSupportedVersions is null | |
| 403 | 405 | ? "The server appears to be a legacy server that requires the deprecated initialize handshake." | |
| 404 | 406 | : $"Server-supported versions: {string.Join(", ", serverSupportedVersions)}.")); | |
@@ -495,15 +497,6 @@ private async Task PerformLegacyInitializeAsync(string requestProtocol, Cancella | |||
| 495 | 497 | throw new McpException($"Server protocol version mismatch. Expected {requestProtocol}, got {initializeResponse.ProtocolVersion}"); | |
| 496 | 498 | } | |
| 497 | 499 | ||
| 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 | - | ||
| 507 | 500 | _negotiatedProtocolVersion = initializeResponse.ProtocolVersion; | |
| 508 | 501 | _sessionHandler.NegotiatedProtocolVersion = _negotiatedProtocolVersion; | |
| 509 | 502 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -52,51 +52,22 @@ public sealed class McpClientOptions | |||
| 52 | 52 | /// </summary> | |
| 53 | 53 | /// <remarks> | |
| 54 | 54 | /// <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> | ||
| 61 | 55 | /// 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, | ||
| 64 | 58 | /// downgrading to any version the server advertises, when the server does not support that revision. | |
| 65 | 59 | /// </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> | ||
| 78 | 60 | /// <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. | ||
| 84 | 68 | /// </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> | ||
| 98 | 69 | /// </remarks> | |
| 99 | - public string? MinProtocolVersion { get; set; } | ||
| 70 | + public string? ProtocolVersion { get; set; } | ||
| 100 | 71 | ||
| 101 | 72 | /// <summary> | |
| 102 | 73 | /// Gets or sets a timeout for the client-server initialization handshake sequence. | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -157,10 +157,9 @@ await StartServerAsync(async context => | |||
| 157 | 157 | Endpoint = new("http://localhost:5000/mcp"), | |
| 158 | 158 | }, HttpClient, LoggerFactory); | |
| 159 | 159 | ||
| 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); | ||
| 164 | 163 | ||
| 165 | 164 | Assert.Equal("2025-06-18", client.NegotiatedProtocolVersion); | |
| 166 | 165 | ||
@@ -245,10 +244,9 @@ await StartServerAsync(async context => | |||
| 245 | 244 | Endpoint = new("http://localhost:5000/mcp"), | |
| 246 | 245 | }, HttpClient, LoggerFactory); | |
| 247 | 246 | ||
| 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); | ||
| 252 | 250 | ||
| 253 | 251 | Assert.Equal("2025-11-25", client.NegotiatedProtocolVersion); | |
| 254 | 252 | } | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -9,20 +9,16 @@ namespace ModelContextProtocol.Tests.Client; | |||
| 9 | 9 | ||
| 10 | 10 | /// <summary> | |
| 11 | 11 | /// 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. | ||
| 19 | 17 | /// </summary> | |
| 20 | 18 | /// <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. | ||
| 26 | 22 | /// </remarks> | |
| 27 | 23 | public class July2026ProtocolFallbackTests(ITestOutputHelper testOutputHelper) : LoggedTest(testOutputHelper) | |
| 28 | 24 | { | |
@@ -32,10 +28,9 @@ public async Task Client_OnMethodNotFound_FallsBackTo_Initialize_AcceptsDowngrad | |||
| 32 | 28 | var ct = TestContext.Current.CancellationToken; | |
| 33 | 29 | await using var transport = new LegacyServerTestTransport(serverNegotiatedVersion: "2025-06-18"); | |
| 34 | 30 | ||
| 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); | ||
| 39 | 34 | ||
| 40 | 35 | Assert.True(transport.ServerDiscoverProbed); | |
| 41 | 36 | Assert.True(transport.LegacyInitializeReceived); | |
@@ -50,17 +45,16 @@ public async Task Client_OnInvalidParams_FallsBackTo_Initialize() | |||
| 50 | 45 | serverNegotiatedVersion: "2025-11-25", | |
| 51 | 46 | probeErrorCode: (int)McpErrorCode.InvalidParams); | |
| 52 | 47 | ||
| 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); | ||
| 57 | 51 | ||
| 58 | 52 | Assert.True(transport.LegacyInitializeReceived); | |
| 59 | 53 | Assert.Equal("2025-11-25", client.NegotiatedProtocolVersion); | |
| 60 | 54 | } | |
| 61 | 55 | ||
| 62 | 56 | [Fact] | |
| 63 | - public async Task Client_WithMinProtocolVersion_RefusesFallback_BelowMinimum() | ||
| 57 | + public async Task Client_WithPinnedJuly2026Version_RefusesFallback_ToLegacyServer() | ||
| 64 | 58 | { | |
| 65 | 59 | var ct = TestContext.Current.CancellationToken; | |
| 66 | 60 | await using var transport = new LegacyServerTestTransport(serverNegotiatedVersion: "2025-06-18"); | |
@@ -69,13 +63,13 @@ public async Task Client_WithMinProtocolVersion_RefusesFallback_BelowMinimum() | |||
| 69 | 63 | { | |
| 70 | 64 | await using var client = await McpClient.CreateAsync(transport, new McpClientOptions | |
| 71 | 65 | { | |
| 66 | + // Pinning the version makes it the minimum too, so the client refuses to fall back. | ||
| 72 | 67 | ProtocolVersion = McpHttpHeaders.July2026ProtocolVersion, | |
| 73 | - MinProtocolVersion = McpHttpHeaders.July2026ProtocolVersion, | ||
| 74 | 68 | }, loggerFactory: LoggerFactory, cancellationToken: ct); | |
| 75 | 69 | }); | |
| 76 | 70 | ||
| 77 | 71 | Assert.IsType<McpException>(exception); | |
| 78 | - Assert.Contains("minimum", exception.Message, StringComparison.OrdinalIgnoreCase); | ||
| 72 | + Assert.Contains("2026-07-28", exception.Message, StringComparison.OrdinalIgnoreCase); | ||
| 79 | 73 | } | |
| 80 | 74 | ||
| 81 | 75 | [Fact] | |
@@ -133,9 +127,9 @@ public async Task Client_OnSilentProbe_FallsBackTo_Initialize_AfterConfiguredPro | |||
| 133 | 127 | silentDiscoverProbe: true); | |
| 134 | 128 | ||
| 135 | 129 | var stopwatch = Stopwatch.StartNew(); | |
| 130 | + // Default options (ProtocolVersion = null) prefer 2026-07-28 but allow automatic fallback. | ||
| 136 | 131 | await using var client = await McpClient.CreateAsync(transport, new McpClientOptions | |
| 137 | 132 | { | |
| 138 | - ProtocolVersion = McpHttpHeaders.July2026ProtocolVersion, | ||
| 139 | 133 | DiscoverProbeTimeout = TimeSpan.FromMilliseconds(250), | |
| 140 | 134 | InitializationTimeout = TestConstants.DefaultTimeout, | |
| 141 | 135 | }, loggerFactory: LoggerFactory, cancellationToken: ct); | |
| Back | FazBrowse Home | New Git URL |
0 commit comments