Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions docs/concepts/stateless/stateless.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,17 @@ The `2026-07-28` protocol revision goes further than `Stateless = true`: it remo

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

**Skipping the discovery round trip.** If you already trust a server's discovery information, provide it through <xref:ModelContextProtocol.Client.McpClientOptions.PriorDiscoverResult> when creating the client. The client skips `server/discover`, chooses a compatible modern protocol version from `SupportedVersions` (honoring <xref:ModelContextProtocol.Client.McpClientOptions.ProtocolVersion> when you pin one), and uses the supplied capabilities, server info, instructions, and cache hints as its connected state. You can get a reusable snapshot from a previous modern connection with <xref:ModelContextProtocol.Client.McpClient.GetDiscoverResult*>.

```csharp
DiscoverResult cachedDiscoverResult = await LoadCachedDiscoverResultAsync();

var clientOptions = new McpClientOptions
{
PriorDiscoverResult = cachedDiscoverResult,
};
```

**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.

```csharp
Expand Down
45 changes: 45 additions & 0 deletions src/ModelContextProtocol.Core/Client/McpClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,51 @@ protected McpClient()
/// </remarks>
public abstract string? ServerInstructions { get; }

/// <summary>
/// Gets a discovery result that can be reused as prior knowledge for a later connection to the same server.
/// </summary>
/// <returns>
/// A <see cref="DiscoverResult"/> describing the connected server's supported protocol versions,
/// capabilities, implementation metadata, and instructions.
/// </returns>
/// <remarks>
/// <para>
/// The returned value can be supplied to <see cref="McpClientOptions.PriorDiscoverResult"/> to skip
/// the <c>server/discover</c> round trip when reconnecting to a trusted server. This method is intended
/// for clients connected with the 2026-07-28-or-later connection model.
/// </para>
/// <para>
/// The default implementation returns a minimal discovery result containing only the negotiated
/// protocol version. Concrete clients may override this to preserve the full <c>server/discover</c>
/// response, including every server-supported version and caching hints.
Comment on lines +72 to +74
/// </para>
/// </remarks>
/// <exception cref="InvalidOperationException">
/// The client is not connected or the negotiated protocol version does not use the 2026-07-28-or-later
/// connection model.
/// </exception>
public virtual DiscoverResult GetDiscoverResult()
{
if (NegotiatedProtocolVersion is not { } negotiatedProtocolVersion)
{
throw new InvalidOperationException("The client is not connected.");
}

if (!McpHttpHeaders.IsJuly2026OrLaterProtocolVersion(negotiatedProtocolVersion))
{
throw new InvalidOperationException(
$"A discover result is only available for clients connected with protocol version {McpHttpHeaders.July2026ProtocolVersion} or later.");
}

return new DiscoverResult
{
SupportedVersions = [negotiatedProtocolVersion],
Capabilities = ServerCapabilities,
ServerInfo = ServerInfo,
Instructions = ServerInstructions,
};
}

/// <summary>
/// Gets a <see cref="Task{TResult}"/> that completes when the client session has completed.
/// </summary>
Expand Down
151 changes: 140 additions & 11 deletions src/ModelContextProtocol.Core/Client/McpClientImpl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ internal sealed partial class McpClientImpl : McpClient
private Implementation? _serverInfo;
private string? _serverInstructions;
private string? _negotiatedProtocolVersion;
private DiscoverResult? _discoverResult;

private bool _disposed;

Expand Down Expand Up @@ -297,7 +298,12 @@ public async Task ConnectAsync(CancellationToken cancellationToken = default)
// prefers the 2026-07-28 revision and automatically falls back to the legacy initialize
// handshake when the server doesn't support it. The legacy branch below runs only when
// the caller explicitly pins a version that still supports Streamable HTTP sessions (opting out of the default).
if (_options.ProtocolVersion is null || McpHttpHeaders.IsJuly2026OrLaterProtocolVersion(_options.ProtocolVersion))
if (_options.PriorDiscoverResult is { } priorDiscoverResult)
{
string negotiatedVersion = GetPriorDiscoverProtocolVersion(priorDiscoverResult);
AdoptDiscoverResult(priorDiscoverResult, negotiatedVersion);
}
else if (_options.ProtocolVersion is null || McpHttpHeaders.IsJuly2026OrLaterProtocolVersion(_options.ProtocolVersion))
{
string preferredVersion = _options.ProtocolVersion ?? McpHttpHeaders.July2026ProtocolVersion;

Expand Down Expand Up @@ -411,16 +417,7 @@ public async Task ConnectAsync(CancellationToken cancellationToken = default)
}
else
{
if (_logger.IsEnabled(LogLevel.Information))
{
LogServerCapabilitiesReceived(_endpointName,
capabilities: JsonSerializer.Serialize(discoverResult!.Capabilities, McpJsonUtilities.JsonContext.Default.ServerCapabilities),
serverInfo: JsonSerializer.Serialize(discoverResult.ServerInfo, McpJsonUtilities.JsonContext.Default.Implementation));
}

_serverCapabilities = discoverResult!.Capabilities;
_serverInfo = discoverResult.ServerInfo;
_serverInstructions = discoverResult.Instructions;
AdoptDiscoverResult(discoverResult!, preferredVersion);
}
}
else
Expand Down Expand Up @@ -448,6 +445,138 @@ public async Task ConnectAsync(CancellationToken cancellationToken = default)
LogClientConnected(_endpointName);
}

/// <inheritdoc/>
public override DiscoverResult GetDiscoverResult()
{
if (_discoverResult is not null)
{
return CloneDiscoverResult(_discoverResult);
}

return base.GetDiscoverResult();
}

private string GetPriorDiscoverProtocolVersion(DiscoverResult discoverResult)
{
Throw.IfNull(discoverResult);
Throw.IfNull(discoverResult.SupportedVersions);
Throw.IfNull(discoverResult.Capabilities);
Throw.IfNull(discoverResult.ServerInfo);

if (_options.ProtocolVersion is { } requestedProtocolVersion)
{
if (!McpHttpHeaders.IsJuly2026OrLaterProtocolVersion(requestedProtocolVersion))
{
throw new InvalidOperationException(
$"{nameof(McpClientOptions.PriorDiscoverResult)} can only be used with protocol version {McpHttpHeaders.July2026ProtocolVersion} or later.");
}

if (!discoverResult.SupportedVersions.Contains(requestedProtocolVersion))
{
throw new McpException(
$"The prior discovery result does not include the requested protocol version '{requestedProtocolVersion}'. " +
$"Server-supported versions: {string.Join(", ", discoverResult.SupportedVersions)}.");
}

if (!McpSessionHandler.SupportedProtocolVersions.Contains(requestedProtocolVersion))
{
throw new McpException($"The requested protocol version '{requestedProtocolVersion}' is not supported by this SDK.");
}

return requestedProtocolVersion;
}

string? negotiatedVersion = discoverResult.SupportedVersions
.Where(version =>
McpHttpHeaders.IsJuly2026OrLaterProtocolVersion(version) &&
McpSessionHandler.SupportedProtocolVersions.Contains(version))
.OrderByDescending(version => version, StringComparer.Ordinal)
.FirstOrDefault();

if (negotiatedVersion is null)
{
throw new McpException(
$"The prior discovery result does not contain a protocol version supported by this SDK for the " +
$"{McpHttpHeaders.July2026ProtocolVersion}-or-later connection model. " +
$"Server-supported versions: {string.Join(", ", discoverResult.SupportedVersions)}.");
}

return negotiatedVersion;
}

private void AdoptDiscoverResult(DiscoverResult discoverResult, string negotiatedVersion)
{
if (_logger.IsEnabled(LogLevel.Information))
{
LogServerCapabilitiesReceived(_endpointName,
capabilities: JsonSerializer.Serialize(discoverResult.Capabilities, McpJsonUtilities.JsonContext.Default.ServerCapabilities),
serverInfo: JsonSerializer.Serialize(discoverResult.ServerInfo, McpJsonUtilities.JsonContext.Default.Implementation));
}

_discoverResult = CloneDiscoverResult(discoverResult);
_serverCapabilities = _discoverResult.Capabilities;
_serverInfo = _discoverResult.ServerInfo;
_serverInstructions = _discoverResult.Instructions;
_negotiatedProtocolVersion = negotiatedVersion;
_sessionHandler.NegotiatedProtocolVersion = negotiatedVersion;
}

private static DiscoverResult CloneDiscoverResult(DiscoverResult discoverResult)
{
return new DiscoverResult
{
SupportedVersions = [.. discoverResult.SupportedVersions],
Capabilities = CloneServerCapabilities(discoverResult.Capabilities),
ServerInfo = CloneImplementation(discoverResult.ServerInfo),
Instructions = discoverResult.Instructions,
TimeToLive = discoverResult.TimeToLive,
CacheScope = discoverResult.CacheScope,
Meta = discoverResult.Meta is null ? null : (JsonObject)discoverResult.Meta.DeepClone(),
ResultType = discoverResult.ResultType,
};
}

#pragma warning disable MCP9005 // LoggingCapability is deprecated but still part of the protocol DTO.
private static ServerCapabilities CloneServerCapabilities(ServerCapabilities capabilities) =>
new()
{
Experimental = CloneObjectDictionary(capabilities.Experimental),
Logging = capabilities.Logging is null ? null : new LoggingCapability(),
Prompts = capabilities.Prompts is null ? null : new PromptsCapability { ListChanged = capabilities.Prompts.ListChanged },
Resources = capabilities.Resources is null ? null : new ResourcesCapability
{
Subscribe = capabilities.Resources.Subscribe,
ListChanged = capabilities.Resources.ListChanged,
},
Tools = capabilities.Tools is null ? null : new ToolsCapability { ListChanged = capabilities.Tools.ListChanged },
Completions = capabilities.Completions is null ? null : new CompletionsCapability(),
Extensions = CloneObjectDictionary(capabilities.Extensions),
};
#pragma warning restore MCP9005

private static Implementation CloneImplementation(Implementation implementation) =>
new()
{
Name = implementation.Name,
Title = implementation.Title,
Version = implementation.Version,
Description = implementation.Description,
Icons = implementation.Icons is null ? null : [.. implementation.Icons.Select(CloneIcon)],
WebsiteUrl = implementation.WebsiteUrl,
};

private static Icon CloneIcon(Icon icon) =>
new()
{
Source = icon.Source,
MimeType = icon.MimeType,
Sizes = icon.Sizes is null ? null : [.. icon.Sizes],
Theme = icon.Theme,
};

private static IDictionary<string, object>? CloneObjectDictionary(IDictionary<string, object>? values) =>
values is null ? null : new Dictionary<string, object>(values, StringComparer.Ordinal);

/// <summary>
/// Performs the legacy initialize handshake (initialize request + initialized notification),
/// records the negotiated protocol version, and stores the server capabilities/info/instructions.
Expand Down
25 changes: 25 additions & 0 deletions src/ModelContextProtocol.Core/Client/McpClientOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,31 @@ public sealed class McpClientOptions
/// </remarks>
public string? ProtocolVersion { get; set; }

/// <summary>
/// Gets or sets a trusted discovery result to use when connecting to a server.
/// </summary>
/// <remarks>
/// <para>
/// When set, the client skips the <c>server/discover</c> probe and initializes its modern
/// 2026-07-28-or-later connection state from this result. The result should come from a trusted
/// source, such as a previous call to <see cref="McpClient.GetDiscoverResult"/> for the same server
/// or an equivalent out-of-band configuration.
/// </para>
/// <para>
/// This option is only valid for protocol versions that use the discovery-based connection model.
/// If <see cref="ProtocolVersion"/> is set, it must be <c>2026-07-28</c> or later and must appear in
/// <see cref="DiscoverResult.SupportedVersions"/>. When <see cref="ProtocolVersion"/> is
/// <see langword="null"/>, the client chooses the newest protocol version in
/// <see cref="DiscoverResult.SupportedVersions"/> that this SDK supports and that uses the modern
/// connection model.
/// </para>
/// <para>
/// Leave this unset to preserve the default safe behavior: probe with <c>server/discover</c> and
/// fall back to the legacy <c>initialize</c> handshake when appropriate.
/// </para>
/// </remarks>
public DiscoverResult? PriorDiscoverResult { get; set; }

/// <summary>
/// Gets or sets a timeout for the client-server initialization handshake sequence.
/// </summary>
Expand Down
Loading
Loading