Align the 2026-07-28 wire with spec PR #3002 and SEP-2575 HTTP statuses (#1752)

Co-authored-by: Stephen Halter <halter73@gmail.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: tarekgh <tarek.mahmoud.sayed@gmail.com>
This commit is contained in:
Paul Carleton 2026-07-28 00:08:03 +01:00 • committed by GitHub
commit 26f60ab7f4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 894 additions and 101 deletions

View file

@ -733,6 +733,12 @@ public abstract partial class McpClient : McpSession
/// For a simpler API that handles both subscription and notification registration in a single call,
/// use <see cref="SubscribeToResourceAsync(Uri, Func{ResourceUpdatedNotificationParams, CancellationToken, ValueTask}, RequestOptions?, CancellationToken)"/>.
/// </para>
/// <para>
/// The 2026-07-28 protocol revision (SEP-2575) removed <c>resources/subscribe</c> in favor of
/// <see cref="RequestMethods.SubscriptionsListen"/> with <c>resourceSubscriptions</c>. On a session that
/// negotiated that revision or later, this method throws an <see cref="McpException"/> with
/// <see cref="McpErrorCode.MethodNotFound"/>.
/// </para>
/// </remarks>
public Task SubscribeToResourceAsync(
SubscribeRequestParams requestParams,
@ -931,6 +937,12 @@ public abstract partial class McpClient : McpSession
/// <returns>The result of the request.</returns>
/// <exception cref="ArgumentNullException"><paramref name="requestParams"/> is <see langword="null"/>.</exception>
/// <exception cref="McpException">The request failed or the server returned an error response.</exception>
/// <remarks>
/// The 2026-07-28 protocol revision (SEP-2575) removed <c>resources/unsubscribe</c> in favor of
/// <see cref="RequestMethods.SubscriptionsListen"/> with <c>resourceSubscriptions</c>. On a session that
/// negotiated that revision or later, this method throws an <see cref="McpException"/> with
/// <see cref="McpErrorCode.MethodNotFound"/>.
/// </remarks>
public Task UnsubscribeFromResourceAsync(
UnsubscribeRequestParams requestParams,
CancellationToken cancellationToken = default)

View file

@ -29,14 +29,16 @@ public abstract partial class McpClient : McpSession
/// <remarks>
/// <para>
/// This property provides identification details about the connected server, including its name and version.
/// It is populated during the initialization handshake and is available after a successful connection.
/// It is populated during the initialization handshake or from <c>server/discover</c> result metadata.
/// </para>
/// <para>
/// This information can be useful for logging, debugging, compatibility checks, and displaying server
/// information to users.
/// </para>
/// </remarks>
/// <exception cref="InvalidOperationException">The client is not connected.</exception>
/// <exception cref="InvalidOperationException">
/// The client is not connected, or the server omitted the optional server identity metadata.
/// </exception>
public abstract Implementation ServerInfo { get; }
/// <summary>

View file

@ -170,7 +170,8 @@ internal sealed partial class McpClientImpl : McpClient
public override ServerCapabilities ServerCapabilities => _serverCapabilities ?? throw new InvalidOperationException("The client is not connected.");
/// <inheritdoc/>
public override Implementation ServerInfo => _serverInfo ?? throw new InvalidOperationException("The client is not connected.");
public override Implementation ServerInfo => _serverInfo ?? throw new InvalidOperationException(
"The client is not connected, or the connected server did not provide optional server identity metadata.");
/// <inheritdoc/>
public override string? ServerInstructions => _serverInstructions;
@ -432,15 +433,19 @@ internal sealed partial class McpClientImpl : McpClient
}
else
{
var discoveredServerInfo = GetServerInfoFromDiscover(discoverResult!);
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));
serverInfo: discoveredServerInfo is null
? "(none)"
: JsonSerializer.Serialize(discoveredServerInfo, McpJsonUtilities.JsonContext.Default.Implementation));
}
_serverCapabilities = discoverResult!.Capabilities;
_serverInfo = discoverResult.ServerInfo;
_serverInfo = discoveredServerInfo;
_serverInstructions = discoverResult.Instructions;
}
@ -484,6 +489,30 @@ internal sealed partial class McpClientImpl : McpClient
LogClientConnected(_endpointName);
}
/// <summary>
/// Resolves the server identity from a <c>server/discover</c> result. The 2026-07-28 revision carries
/// <c>serverInfo</c> in the result's <c>_meta/io.modelcontextprotocol/serverInfo</c> field rather than
/// the result body. Identity is optional, so a server that omits it yields <see langword="null"/>.
/// </summary>
private static Implementation? GetServerInfoFromDiscover(DiscoverResult discoverResult)
{
if (discoverResult.Meta is { } meta &&
meta.TryGetPropertyValue(MetaKeys.ServerInfo, out JsonNode? serverInfoNode))
{
if (serverInfoNode is null)
{
throw new JsonException(
$"Discover result metadata '{MetaKeys.ServerInfo}' must contain a server implementation.");
}
return JsonSerializer.Deserialize(serverInfoNode, McpJsonUtilities.JsonContext.Default.Implementation)
?? throw new JsonException(
$"Discover result metadata '{MetaKeys.ServerInfo}' must contain a server implementation.");
}
return null;
}
/// <summary>
/// Performs the initialize handshake (initialize request + initialized notification),
/// records the negotiated protocol version, and stores the server capabilities/info/instructions.

View file

@ -135,7 +135,7 @@ public sealed class McpClientTool : AIFunction
// would lose that information. So, we only do the translation if there is no additional information to preserve.
if (result.IsError is not true &&
result.StructuredContent is null &&
result.Meta is not { Count: > 0 })
!HasApplicationResultMetadata(result.Meta))
{
switch (result.Content.Count)
{
@ -150,6 +150,26 @@ public sealed class McpClientTool : AIFunction
return JsonSerializer.SerializeToElement(result, McpJsonUtilities.JsonContext.Default.CallToolResult);
}
private static bool HasApplicationResultMetadata(JsonObject? meta)
{
if (meta is null)
{
return false;
}
foreach (var property in meta)
{
// Server identity is protocol metadata already exposed through McpClient.ServerInfo.
// It should not force an otherwise simple tool result into its JSON envelope.
if (!string.Equals(property.Key, MetaKeys.ServerInfo, StringComparison.Ordinal))
{
return true;
}
}
return false;
}
/// <summary>
/// Invokes the tool on the server.
/// </summary>