Move notification handler registrations to capabilities (#207)
* Move notification handler registrations to capabilities Currently request handlers are set on the capability objects, but notification handlers are set after construction via an AddNotificationHandler method on the IMcpEndpoint interface. This moves handler specification to be at construction as well. This makes it more consistent with request handlers, simplifies the IMcpEndpoint interface to just be about message sending, and avoids a concurrency bug that could occur if someone tried to add a handler while the endpoint was processing notifications. * Address more feedback and further cleanup
This commit is contained in:
parent
8fcdf95785
commit
d21d933876
4 changed files with 292 additions and 288 deletions
|
|
@ -12,6 +12,12 @@ namespace ModelContextProtocol.Client;
|
|||
/// <inheritdoc/>
|
||||
internal sealed class McpClient : McpEndpoint, IMcpClient
|
||||
{
|
||||
private static Implementation DefaultImplementation { get; } = new()
|
||||
{
|
||||
Name = DefaultAssemblyName.Name ?? nameof(McpClient),
|
||||
Version = DefaultAssemblyName.Version?.ToString() ?? "1.0.0",
|
||||
};
|
||||
|
||||
private readonly IClientTransport _clientTransport;
|
||||
private readonly McpClientOptions _options;
|
||||
|
||||
|
|
@ -29,43 +35,53 @@ internal sealed class McpClient : McpEndpoint, IMcpClient
|
|||
/// <param name="options">Options for the client, defining protocol version and capabilities.</param>
|
||||
/// <param name="serverConfig">The server configuration.</param>
|
||||
/// <param name="loggerFactory">The logger factory.</param>
|
||||
public McpClient(IClientTransport clientTransport, McpClientOptions options, McpServerConfig serverConfig, ILoggerFactory? loggerFactory)
|
||||
public McpClient(IClientTransport clientTransport, McpClientOptions? options, McpServerConfig serverConfig, ILoggerFactory? loggerFactory)
|
||||
: base(loggerFactory)
|
||||
{
|
||||
options ??= new();
|
||||
|
||||
_clientTransport = clientTransport;
|
||||
_options = options;
|
||||
|
||||
EndpointName = $"Client ({serverConfig.Id}: {serverConfig.Name})";
|
||||
|
||||
if (options.Capabilities?.Sampling is { } samplingCapability)
|
||||
if (options.Capabilities is { } capabilities)
|
||||
{
|
||||
if (samplingCapability.SamplingHandler is not { } samplingHandler)
|
||||
if (capabilities.NotificationHandlers is { } notificationHandlers)
|
||||
{
|
||||
throw new InvalidOperationException($"Sampling capability was set but it did not provide a handler.");
|
||||
NotificationHandlers.AddRange(notificationHandlers);
|
||||
}
|
||||
|
||||
SetRequestHandler(
|
||||
RequestMethods.SamplingCreateMessage,
|
||||
(request, cancellationToken) => samplingHandler(
|
||||
request,
|
||||
request?.Meta?.ProgressToken is { } token ? new TokenProgress(this, token) : NullProgress.Instance,
|
||||
cancellationToken),
|
||||
McpJsonUtilities.JsonContext.Default.CreateMessageRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.CreateMessageResult);
|
||||
}
|
||||
|
||||
if (options.Capabilities?.Roots is { } rootsCapability)
|
||||
{
|
||||
if (rootsCapability.RootsHandler is not { } rootsHandler)
|
||||
if (capabilities.Sampling is { } samplingCapability)
|
||||
{
|
||||
throw new InvalidOperationException($"Roots capability was set but it did not provide a handler.");
|
||||
if (samplingCapability.SamplingHandler is not { } samplingHandler)
|
||||
{
|
||||
throw new InvalidOperationException($"Sampling capability was set but it did not provide a handler.");
|
||||
}
|
||||
|
||||
RequestHandlers.Set(
|
||||
RequestMethods.SamplingCreateMessage,
|
||||
(request, cancellationToken) => samplingHandler(
|
||||
request,
|
||||
request?.Meta?.ProgressToken is { } token ? new TokenProgress(this, token) : NullProgress.Instance,
|
||||
cancellationToken),
|
||||
McpJsonUtilities.JsonContext.Default.CreateMessageRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.CreateMessageResult);
|
||||
}
|
||||
|
||||
SetRequestHandler(
|
||||
RequestMethods.RootsList,
|
||||
rootsHandler,
|
||||
McpJsonUtilities.JsonContext.Default.ListRootsRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.ListRootsResult);
|
||||
if (capabilities.Roots is { } rootsCapability)
|
||||
{
|
||||
if (rootsCapability.RootsHandler is not { } rootsHandler)
|
||||
{
|
||||
throw new InvalidOperationException($"Roots capability was set but it did not provide a handler.");
|
||||
}
|
||||
|
||||
RequestHandlers.Set(
|
||||
RequestMethods.RootsList,
|
||||
rootsHandler,
|
||||
McpJsonUtilities.JsonContext.Default.ListRootsRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.ListRootsResult);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -96,20 +112,20 @@ internal sealed class McpClient : McpEndpoint, IMcpClient
|
|||
using var initializationCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
initializationCts.CancelAfter(_options.InitializationTimeout);
|
||||
|
||||
try
|
||||
{
|
||||
// Send initialize request
|
||||
var initializeResponse = await this.SendRequestAsync(
|
||||
RequestMethods.Initialize,
|
||||
new InitializeRequestParams
|
||||
{
|
||||
ProtocolVersion = _options.ProtocolVersion,
|
||||
Capabilities = _options.Capabilities ?? new ClientCapabilities(),
|
||||
ClientInfo = _options.ClientInfo
|
||||
},
|
||||
McpJsonUtilities.JsonContext.Default.InitializeRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.InitializeResult,
|
||||
cancellationToken: initializationCts.Token).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
// Send initialize request
|
||||
var initializeResponse = await this.SendRequestAsync(
|
||||
RequestMethods.Initialize,
|
||||
new InitializeRequestParams
|
||||
{
|
||||
ProtocolVersion = _options.ProtocolVersion,
|
||||
Capabilities = _options.Capabilities ?? new ClientCapabilities(),
|
||||
ClientInfo = _options.ClientInfo ?? DefaultImplementation,
|
||||
},
|
||||
McpJsonUtilities.JsonContext.Default.InitializeRequestParams,
|
||||
McpJsonUtilities.JsonContext.Default.InitializeResult,
|
||||
cancellationToken: initializationCts.Token).ConfigureAwait(false);
|
||||
|
||||
// Store server information
|
||||
_logger.ServerCapabilitiesReceived(EndpointName,
|
||||
|
|
|
|||
|
|
@ -5,30 +5,12 @@ using ModelContextProtocol.Protocol.Transport;
|
|||
using ModelContextProtocol.Utils;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using System.Reflection;
|
||||
|
||||
namespace ModelContextProtocol.Client;
|
||||
|
||||
/// <summary>Provides factory methods for creating MCP clients.</summary>
|
||||
public static class McpClientFactory
|
||||
{
|
||||
/// <summary>Default client options to use when none are supplied.</summary>
|
||||
private static readonly McpClientOptions s_defaultClientOptions = CreateDefaultClientOptions();
|
||||
|
||||
/// <summary>Creates default client options to use when no options are supplied.</summary>
|
||||
private static McpClientOptions CreateDefaultClientOptions()
|
||||
{
|
||||
var asmName = (Assembly.GetEntryAssembly() ?? Assembly.GetExecutingAssembly()).GetName();
|
||||
return new()
|
||||
{
|
||||
ClientInfo = new()
|
||||
{
|
||||
Name = asmName.Name ?? "McpClient",
|
||||
Version = asmName.Version?.ToString() ?? "1.0.0",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>Creates an <see cref="IMcpClient"/>, connecting it to the specified server.</summary>
|
||||
/// <param name="serverConfig">Configuration for the target server to which the client should connect.</param>
|
||||
/// <param name="clientOptions">
|
||||
|
|
@ -52,7 +34,6 @@ public static class McpClientFactory
|
|||
{
|
||||
Throw.IfNull(serverConfig);
|
||||
|
||||
clientOptions ??= s_defaultClientOptions;
|
||||
createTransportFunc ??= CreateTransport;
|
||||
|
||||
string endpointName = $"Client ({serverConfig.Id}: {serverConfig.Name})";
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ public class McpClientOptions
|
|||
/// <summary>
|
||||
/// Information about this client implementation.
|
||||
/// </summary>
|
||||
public required Implementation ClientInfo { get; set; }
|
||||
public Implementation? ClientInfo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Client capabilities to advertise to the server.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
using ModelContextProtocol.Protocol.Types;
|
||||
using ModelContextProtocol.Utils.Json;
|
||||
using ModelContextProtocol.Utils;
|
||||
using Microsoft.Extensions.AI;
|
||||
using System.Text.Json;
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue