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:
parent
6787c0cc19
commit
26f60ab7f4
4 changed files with 894 additions and 101 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue