Decouple Handlers and Collections from protocol types (#765)

* Decouple Handlers and Collections (e.g. ToolsHandler and ToolsCollection) from Capabilities by promoting them to the options types.

Also, group all handlers in container classes, for server, I decided to move McpServerHandlers down to Mcp.Core and move the NotificationHandlers there too.

Also, Fix ListResourceTemplatesHandler issue.

* Add remarks for empty protocol capability types

* Keep JsonIgnore properties and mark them as Obsolete

* Continue supporting Obsolete members

* Update Obsolete messages to indicate the member will be removed

* Add EditorBrowsable Never to the remaining obsolete members
This commit is contained in:
David Cantú 2025-09-24 09:42:03 -05:00 • committed by GitHub
commit 5f992994c3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 1588 additions and 1161 deletions

View file

@ -1,11 +1,13 @@
using ModelContextProtocol.Protocol;
using System.ComponentModel;
namespace ModelContextProtocol.Client;
/// <summary>
/// Represents an instance of a Model Context Protocol (MCP) client that connects to and communicates with an MCP server.
/// </summary>
[Obsolete($"Use {nameof(McpClient)} instead.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774
[Obsolete($"Use {nameof(McpClient)} instead. This member will be removed in a subsequent release.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774
[EditorBrowsable(EditorBrowsableState.Never)]
public interface IMcpClient : IMcpEndpoint
{
/// <summary>

View file

@ -631,11 +631,11 @@ public abstract partial class McpClient : McpSession, IMcpClient
}
/// <summary>
/// Creates a sampling handler for use with <see cref="SamplingCapability.SamplingHandler"/> that will
/// Creates a sampling handler for use with <see cref="McpClientHandlers.SamplingHandler"/> that will
/// satisfy sampling requests using the specified <see cref="IChatClient"/>.
/// </summary>
/// <param name="chatClient">The <see cref="IChatClient"/> with which to satisfy sampling requests.</param>
/// <returns>The created handler delegate that can be assigned to <see cref="SamplingCapability.SamplingHandler"/>.</returns>
/// <returns>The created handler delegate that can be assigned to <see cref="McpClientHandlers.SamplingHandler"/>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="chatClient"/> is <see langword="null"/>.</exception>
public static Func<CreateMessageRequestParams?, IProgress<ProgressNotificationValue>, CancellationToken, ValueTask<CreateMessageResult>> CreateSamplingHandler(
IChatClient chatClient)

View file

@ -1,5 +1,6 @@
using Microsoft.Extensions.AI;
using ModelContextProtocol.Protocol;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Text.Json;
@ -19,11 +20,11 @@ namespace ModelContextProtocol.Client;
public static class McpClientExtensions
{
/// <summary>
/// Creates a sampling handler for use with <see cref="SamplingCapability.SamplingHandler"/> that will
/// Creates a sampling handler for use with <see cref="McpClientHandlers.SamplingHandler"/> that will
/// satisfy sampling requests using the specified <see cref="IChatClient"/>.
/// </summary>
/// <param name="chatClient">The <see cref="IChatClient"/> with which to satisfy sampling requests.</param>
/// <returns>The created handler delegate that can be assigned to <see cref="SamplingCapability.SamplingHandler"/>.</returns>
/// <returns>The created handler delegate that can be assigned to <see cref="McpClientHandlers.SamplingHandler"/>.</returns>
/// <remarks>
/// <para>
/// This method creates a function that converts MCP message requests into chat client calls, enabling
@ -84,7 +85,8 @@ public static class McpClientExtensions
/// </remarks>
/// <exception cref="ArgumentNullException"><paramref name="client"/> is <see langword="null"/>.</exception>
/// <exception cref="McpException">Thrown when the server cannot be reached or returns an error response.</exception>
[Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.PingAsync)} instead.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774
[Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.PingAsync)} instead. This member will be removed in a subsequent release.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774
[EditorBrowsable(EditorBrowsableState.Never)]
public static Task PingAsync(this IMcpClient client, CancellationToken cancellationToken = default)
=> AsClientOrThrow(client).PingAsync(cancellationToken);
@ -127,7 +129,8 @@ public static class McpClientExtensions
/// </code>
/// </example>
/// <exception cref="ArgumentNullException"><paramref name="client"/> is <see langword="null"/>.</exception>
[Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.ListToolsAsync)} instead.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774
[Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.ListToolsAsync)} instead. This member will be removed in a subsequent release.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774
[EditorBrowsable(EditorBrowsableState.Never)]
public static ValueTask<IList<McpClientTool>> ListToolsAsync(
this IMcpClient client,
JsonSerializerOptions? serializerOptions = null,
@ -166,7 +169,8 @@ public static class McpClientExtensions
/// </code>
/// </example>
/// <exception cref="ArgumentNullException"><paramref name="client"/> is <see langword="null"/>.</exception>
[Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.EnumerateToolsAsync)} instead.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774
[Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.EnumerateToolsAsync)} instead. This member will be removed in a subsequent release.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774
[EditorBrowsable(EditorBrowsableState.Never)]
public static IAsyncEnumerable<McpClientTool> EnumerateToolsAsync(
this IMcpClient client,
JsonSerializerOptions? serializerOptions = null,
@ -190,7 +194,8 @@ public static class McpClientExtensions
/// </para>
/// </remarks>
/// <exception cref="ArgumentNullException"><paramref name="client"/> is <see langword="null"/>.</exception>
[Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.ListPromptsAsync)} instead.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774
[Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.ListPromptsAsync)} instead. This member will be removed in a subsequent release.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774
[EditorBrowsable(EditorBrowsableState.Never)]
public static ValueTask<IList<McpClientPrompt>> ListPromptsAsync(
this IMcpClient client, CancellationToken cancellationToken = default)
=> AsClientOrThrow(client).ListPromptsAsync(cancellationToken);
@ -222,7 +227,8 @@ public static class McpClientExtensions
/// </code>
/// </example>
/// <exception cref="ArgumentNullException"><paramref name="client"/> is <see langword="null"/>.</exception>
[Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.EnumeratePromptsAsync)} instead.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774
[Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.EnumeratePromptsAsync)} instead. This member will be removed in a subsequent release.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774
[EditorBrowsable(EditorBrowsableState.Never)]
public static IAsyncEnumerable<McpClientPrompt> EnumeratePromptsAsync(
this IMcpClient client, CancellationToken cancellationToken = default)
=> AsClientOrThrow(client).EnumeratePromptsAsync(cancellationToken);
@ -252,7 +258,8 @@ public static class McpClientExtensions
/// </remarks>
/// <exception cref="McpException">Thrown when the prompt does not exist, when required arguments are missing, or when the server encounters an error processing the prompt.</exception>
/// <exception cref="ArgumentNullException"><paramref name="client"/> is <see langword="null"/>.</exception>
[Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.GetPromptAsync)} instead.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774
[Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.GetPromptAsync)} instead. This member will be removed in a subsequent release.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774
[EditorBrowsable(EditorBrowsableState.Never)]
public static ValueTask<GetPromptResult> GetPromptAsync(
this IMcpClient client,
string name,
@ -278,7 +285,8 @@ public static class McpClientExtensions
/// </para>
/// </remarks>
/// <exception cref="ArgumentNullException"><paramref name="client"/> is <see langword="null"/>.</exception>
[Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.ListResourceTemplatesAsync)} instead.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774
[Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.ListResourceTemplatesAsync)} instead. This member will be removed in a subsequent release.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774
[EditorBrowsable(EditorBrowsableState.Never)]
public static ValueTask<IList<McpClientResourceTemplate>> ListResourceTemplatesAsync(
this IMcpClient client, CancellationToken cancellationToken = default)
=> AsClientOrThrow(client).ListResourceTemplatesAsync(cancellationToken);
@ -310,7 +318,8 @@ public static class McpClientExtensions
/// </code>
/// </example>
/// <exception cref="ArgumentNullException"><paramref name="client"/> is <see langword="null"/>.</exception>
[Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.EnumerateResourceTemplatesAsync)} instead.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774
[Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.EnumerateResourceTemplatesAsync)} instead. This member will be removed in a subsequent release.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774
[EditorBrowsable(EditorBrowsableState.Never)]
public static IAsyncEnumerable<McpClientResourceTemplate> EnumerateResourceTemplatesAsync(
this IMcpClient client, CancellationToken cancellationToken = default)
=> AsClientOrThrow(client).EnumerateResourceTemplatesAsync(cancellationToken);
@ -344,7 +353,8 @@ public static class McpClientExtensions
/// </code>
/// </example>
/// <exception cref="ArgumentNullException"><paramref name="client"/> is <see langword="null"/>.</exception>
[Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.ListResourcesAsync)} instead.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774
[Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.ListResourcesAsync)} instead. This member will be removed in a subsequent release.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774
[EditorBrowsable(EditorBrowsableState.Never)]
public static ValueTask<IList<McpClientResource>> ListResourcesAsync(
this IMcpClient client, CancellationToken cancellationToken = default)
=> AsClientOrThrow(client).ListResourcesAsync(cancellationToken);
@ -376,7 +386,8 @@ public static class McpClientExtensions
/// </code>
/// </example>
/// <exception cref="ArgumentNullException"><paramref name="client"/> is <see langword="null"/>.</exception>
[Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.EnumerateResourcesAsync)} instead.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774
[Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.EnumerateResourcesAsync)} instead. This member will be removed in a subsequent release.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774
[EditorBrowsable(EditorBrowsableState.Never)]
public static IAsyncEnumerable<McpClientResource> EnumerateResourcesAsync(
this IMcpClient client, CancellationToken cancellationToken = default)
=> AsClientOrThrow(client).EnumerateResourcesAsync(cancellationToken);
@ -390,7 +401,8 @@ public static class McpClientExtensions
/// <exception cref="ArgumentNullException"><paramref name="client"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentNullException"><paramref name="uri"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException"><paramref name="uri"/> is empty or composed entirely of whitespace.</exception>
[Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.ReadResourceAsync)} instead.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774
[Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.ReadResourceAsync)} instead. This member will be removed in a subsequent release.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774
[EditorBrowsable(EditorBrowsableState.Never)]
public static ValueTask<ReadResourceResult> ReadResourceAsync(
this IMcpClient client, string uri, CancellationToken cancellationToken = default)
=> AsClientOrThrow(client).ReadResourceAsync(uri, cancellationToken);
@ -403,7 +415,8 @@ public static class McpClientExtensions
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
/// <exception cref="ArgumentNullException"><paramref name="client"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentNullException"><paramref name="uri"/> is <see langword="null"/>.</exception>
[Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.ReadResourceAsync)} instead.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774
[Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.ReadResourceAsync)} instead. This member will be removed in a subsequent release.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774
[EditorBrowsable(EditorBrowsableState.Never)]
public static ValueTask<ReadResourceResult> ReadResourceAsync(
this IMcpClient client, Uri uri, CancellationToken cancellationToken = default)
=> AsClientOrThrow(client).ReadResourceAsync(uri, cancellationToken);
@ -418,7 +431,8 @@ public static class McpClientExtensions
/// <exception cref="ArgumentNullException"><paramref name="client"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentNullException"><paramref name="uriTemplate"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException"><paramref name="uriTemplate"/> is empty or composed entirely of whitespace.</exception>
[Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.ReadResourceAsync)} instead.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774
[Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.ReadResourceAsync)} instead. This member will be removed in a subsequent release.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774
[EditorBrowsable(EditorBrowsableState.Never)]
public static ValueTask<ReadResourceResult> ReadResourceAsync(
this IMcpClient client, string uriTemplate, IReadOnlyDictionary<string, object?> arguments, CancellationToken cancellationToken = default)
=> AsClientOrThrow(client).ReadResourceAsync(uriTemplate, arguments, cancellationToken);
@ -452,7 +466,8 @@ public static class McpClientExtensions
/// <exception cref="ArgumentNullException"><paramref name="argumentName"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException"><paramref name="argumentName"/> is empty or composed entirely of whitespace.</exception>
/// <exception cref="McpException">The server returned an error response.</exception>
[Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.CompleteAsync)} instead.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774
[Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.CompleteAsync)} instead. This member will be removed in a subsequent release.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774
[EditorBrowsable(EditorBrowsableState.Never)]
public static ValueTask<CompleteResult> CompleteAsync(this IMcpClient client, Reference reference, string argumentName, string argumentValue, CancellationToken cancellationToken = default)
=> AsClientOrThrow(client).CompleteAsync(reference, argumentName, argumentValue, cancellationToken);
@ -481,7 +496,8 @@ public static class McpClientExtensions
/// <exception cref="ArgumentNullException"><paramref name="client"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentNullException"><paramref name="uri"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException"><paramref name="uri"/> is empty or composed entirely of whitespace.</exception>
[Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.SubscribeToResourceAsync)} instead.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774
[Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.SubscribeToResourceAsync)} instead. This member will be removed in a subsequent release.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774
[EditorBrowsable(EditorBrowsableState.Never)]
public static Task SubscribeToResourceAsync(this IMcpClient client, string uri, CancellationToken cancellationToken = default)
=> AsClientOrThrow(client).SubscribeToResourceAsync(uri, cancellationToken);
@ -509,7 +525,8 @@ public static class McpClientExtensions
/// </remarks>
/// <exception cref="ArgumentNullException"><paramref name="client"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentNullException"><paramref name="uri"/> is <see langword="null"/>.</exception>
[Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.SubscribeToResourceAsync)} instead.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774
[Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.SubscribeToResourceAsync)} instead. This member will be removed in a subsequent release.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774
[EditorBrowsable(EditorBrowsableState.Never)]
public static Task SubscribeToResourceAsync(this IMcpClient client, Uri uri, CancellationToken cancellationToken = default)
=> AsClientOrThrow(client).SubscribeToResourceAsync(uri, cancellationToken);
@ -537,7 +554,8 @@ public static class McpClientExtensions
/// <exception cref="ArgumentNullException"><paramref name="client"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentNullException"><paramref name="uri"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException"><paramref name="uri"/> is empty or composed entirely of whitespace.</exception>
[Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.UnsubscribeFromResourceAsync)} instead.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774
[Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.UnsubscribeFromResourceAsync)} instead. This member will be removed in a subsequent release.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774
[EditorBrowsable(EditorBrowsableState.Never)]
public static Task UnsubscribeFromResourceAsync(this IMcpClient client, string uri, CancellationToken cancellationToken = default)
=> AsClientOrThrow(client).UnsubscribeFromResourceAsync(uri, cancellationToken);
@ -564,7 +582,8 @@ public static class McpClientExtensions
/// </remarks>
/// <exception cref="ArgumentNullException"><paramref name="client"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentNullException"><paramref name="uri"/> is <see langword="null"/>.</exception>
[Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.UnsubscribeFromResourceAsync)} instead.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774
[Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.UnsubscribeFromResourceAsync)} instead. This member will be removed in a subsequent release.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774
[EditorBrowsable(EditorBrowsableState.Never)]
public static Task UnsubscribeFromResourceAsync(this IMcpClient client, Uri uri, CancellationToken cancellationToken = default)
=> AsClientOrThrow(client).UnsubscribeFromResourceAsync(uri, cancellationToken);
@ -603,7 +622,8 @@ public static class McpClientExtensions
/// });
/// </code>
/// </example>
[Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.CallToolAsync)} instead.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774
[Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.CallToolAsync)} instead. This member will be removed in a subsequent release.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774
[EditorBrowsable(EditorBrowsableState.Never)]
public static ValueTask<CallToolResult> CallToolAsync(
this IMcpClient client,
string toolName,

View file

@ -1,4 +1,5 @@
using Microsoft.Extensions.Logging;
using System.ComponentModel;
namespace ModelContextProtocol.Client;
@ -10,7 +11,8 @@ namespace ModelContextProtocol.Client;
/// that connect to MCP servers. It handles the creation and connection
/// of appropriate implementations through the supplied transport.
/// </remarks>
[Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.CreateAsync)} instead.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774
[Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.CreateAsync)} instead. This member will be removed in a subsequent release.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774
[EditorBrowsable(EditorBrowsableState.Never)]
public static partial class McpClientFactory
{
/// <summary>Creates an <see cref="IMcpClient"/>, connecting it to the specified server.</summary>

View file

@ -0,0 +1,88 @@
using Microsoft.Extensions.AI;
using ModelContextProtocol.Protocol;
namespace ModelContextProtocol.Client;
/// <summary>
/// Provides a container for handlers used in the creation of an MCP client.
/// </summary>
/// <remarks>
/// <para>
/// This class provides a centralized collection of delegates that implement various capabilities of the Model Context Protocol.
/// </para>
/// <para>
/// Each handler in this class corresponds to a specific client endpoint in the Model Context Protocol and
/// is responsible for processing a particular type of message. The handlers are used to customize
/// the behavior of the MCP server by providing implementations for the various protocol operations.
/// </para>
/// <para>
/// When a server sends a message to the client, the appropriate handler is invoked to process it
/// according to the protocol specification. Which handler is selected
/// is done based on an ordinal, case-sensitive string comparison.
/// </para>
/// </remarks>
public class McpClientHandlers
{
/// <summary>Gets or sets notification handlers to register with the client.</summary>
/// <remarks>
/// <para>
/// When constructed, the client will enumerate these handlers once, which may contain multiple handlers per notification method key.
/// The client will not re-enumerate the sequence after initialization.
/// </para>
/// <para>
/// Notification handlers allow the client to respond to server-sent notifications for specific methods.
/// Each key in the collection is a notification method name, and each value is a callback that will be invoked
/// when a notification with that method is received.
/// </para>
/// <para>
/// Handlers provided via <see cref="NotificationHandlers"/> will be registered with the client for the lifetime of the client.
/// For transient handlers, <see cref="IMcpEndpoint.RegisterNotificationHandler"/> may be used to register a handler that can
/// then be unregistered by disposing of the <see cref="IAsyncDisposable"/> returned from the method.
/// </para>
/// </remarks>
public IEnumerable<KeyValuePair<string, Func<JsonRpcNotification, CancellationToken, ValueTask>>>? NotificationHandlers { get; set; }
/// <summary>
/// Gets or sets the handler for <see cref="RequestMethods.RootsList"/> requests.
/// </summary>
/// <remarks>
/// This handler is invoked when a client sends a <see cref="RequestMethods.RootsList"/> request to retrieve available roots.
/// The handler receives request parameters and should return a <see cref="ListRootsResult"/> containing the collection of available roots.
/// </remarks>
public Func<ListRootsRequestParams?, CancellationToken, ValueTask<ListRootsResult>>? RootsHandler { get; set; }
/// <summary>
/// Gets or sets the handler for processing <see cref="RequestMethods.ElicitationCreate"/> requests.
/// </summary>
/// <remarks>
/// <para>
/// This handler function is called when an MCP server requests the client to provide additional
/// information during interactions. The client must set this property for the elicitation capability to work.
/// </para>
/// <para>
/// The handler receives message parameters and a cancellation token.
/// It should return a <see cref="ElicitResult"/> containing the response to the elicitation request.
/// </para>
/// </remarks>
public Func<ElicitRequestParams?, CancellationToken, ValueTask<ElicitResult>>? ElicitationHandler { get; set; }
/// <summary>
/// Gets or sets the handler for processing <see cref="RequestMethods.SamplingCreateMessage"/> requests.
/// </summary>
/// <remarks>
/// <para>
/// This handler function is called when an MCP server requests the client to generate content
/// using an AI model. The client must set this property for the sampling capability to work.
/// </para>
/// <para>
/// The handler receives message parameters, a progress reporter for updates, and a
/// cancellation token. It should return a <see cref="CreateMessageResult"/> containing the
/// generated content.
/// </para>
/// <para>
/// You can create a handler using the <see cref="McpClientExtensions.CreateSamplingHandler"/> extension
/// method with any implementation of <see cref="IChatClient"/>.
/// </para>
/// </remarks>
public Func<CreateMessageRequestParams?, IProgress<ProgressNotificationValue>, CancellationToken, ValueTask<CreateMessageResult>>? SamplingHandler { get; set; }
}

View file

@ -49,28 +49,29 @@ internal sealed partial class McpClientImpl : McpClient
var notificationHandlers = new NotificationHandlers();
var requestHandlers = new RequestHandlers();
if (options.Capabilities is { } capabilities)
{
RegisterHandlers(capabilities, notificationHandlers, requestHandlers);
}
RegisterHandlers(options, notificationHandlers, requestHandlers);
_sessionHandler = new McpSessionHandler(isServer: false, transport, endpointName, requestHandlers, notificationHandlers, _logger);
}
private void RegisterHandlers(ClientCapabilities capabilities, NotificationHandlers notificationHandlers, RequestHandlers requestHandlers)
private void RegisterHandlers(McpClientOptions options, NotificationHandlers notificationHandlers, RequestHandlers requestHandlers)
{
if (capabilities.NotificationHandlers is { } notificationHandlersFromCapabilities)
McpClientHandlers handlers = options.Handlers;
#pragma warning disable CS0618 // Type or member is obsolete
var notificationHandlersFromOptions = handlers.NotificationHandlers ?? options.Capabilities?.NotificationHandlers;
var samplingHandler = handlers.SamplingHandler ?? options.Capabilities?.Sampling?.SamplingHandler;
var rootsHandler = handlers.RootsHandler ?? options.Capabilities?.Roots?.RootsHandler;
var elicitationHandler = handlers.ElicitationHandler ?? options.Capabilities?.Elicitation?.ElicitationHandler;
#pragma warning restore CS0618 // Type or member is obsolete
if (notificationHandlersFromOptions is not null)
{
notificationHandlers.RegisterRange(notificationHandlersFromCapabilities);
notificationHandlers.RegisterRange(notificationHandlersFromOptions);
}
if (capabilities.Sampling is { } samplingCapability)
if (samplingHandler is not null)
{
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(
@ -79,34 +80,33 @@ internal sealed partial class McpClientImpl : McpClient
cancellationToken),
McpJsonUtilities.JsonContext.Default.CreateMessageRequestParams,
McpJsonUtilities.JsonContext.Default.CreateMessageResult);
_options.Capabilities ??= new();
_options.Capabilities.Sampling ??= new();
}
if (capabilities.Roots is { } rootsCapability)
if (rootsHandler is not null)
{
if (rootsCapability.RootsHandler is not { } rootsHandler)
{
throw new InvalidOperationException("Roots capability was set but it did not provide a handler.");
}
requestHandlers.Set(
RequestMethods.RootsList,
(request, _, cancellationToken) => rootsHandler(request, cancellationToken),
McpJsonUtilities.JsonContext.Default.ListRootsRequestParams,
McpJsonUtilities.JsonContext.Default.ListRootsResult);
_options.Capabilities ??= new();
_options.Capabilities.Roots ??= new();
}
if (capabilities.Elicitation is { } elicitationCapability)
if (elicitationHandler is not null)
{
if (elicitationCapability.ElicitationHandler is not { } elicitationHandler)
{
throw new InvalidOperationException("Elicitation capability was set but it did not provide a handler.");
}
requestHandlers.Set(
RequestMethods.ElicitationCreate,
(request, _, cancellationToken) => elicitationHandler(request, cancellationToken),
McpJsonUtilities.JsonContext.Default.ElicitRequestParams,
McpJsonUtilities.JsonContext.Default.ElicitResult);
_options.Capabilities ??= new();
_options.Capabilities.Elicitation ??= new();
}
}

View file

@ -11,6 +11,8 @@ namespace ModelContextProtocol.Client;
/// </remarks>
public sealed class McpClientOptions
{
private McpClientHandlers? _handlers;
/// <summary>
/// Gets or sets information about this client implementation, including its name and version.
/// </summary>
@ -63,4 +65,17 @@ public sealed class McpClientOptions
/// <para>The default value is 60 seconds.</para>
/// </remarks>
public TimeSpan InitializationTimeout { get; set; } = TimeSpan.FromSeconds(60);
/// <summary>
/// Gets or sets the container of handlers used by the client for processing protocol messages.
/// </summary>
public McpClientHandlers Handlers
{
get => _handlers ??= new();
set
{
Throw.IfNull(value);
_handlers = value;
}
}
}