Skip to content

Commit 78218a4

Browse files
committed
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.
1 parent d344c65 commit 78218a4

36 files changed

+1090
-1248
lines changed

samples/ChatWithTools/Program.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,10 @@
4141
}),
4242
clientOptions: new()
4343
{
44-
Capabilities = new() { Sampling = new() { SamplingHandler = samplingClient.CreateSamplingHandler() } },
44+
Handlers = new()
45+
{
46+
SamplingHandler = samplingClient.CreateSamplingHandler()
47+
}
4548
},
4649
loggerFactory: loggerFactory);
4750

samples/InMemoryTransport/Program.cs

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,7 @@
1010
new StreamServerTransport(clientToServerPipe.Reader.AsStream(), serverToClientPipe.Writer.AsStream()),
1111
new McpServerOptions()
1212
{
13-
Capabilities = new()
14-
{
15-
Tools = new()
16-
{
17-
ToolCollection = [McpServerTool.Create((string arg) => $"Echo: {arg}", new() { Name = "Echo" })]
18-
}
19-
}
13+
ToolCollection = [McpServerTool.Create((string arg) => $"Echo: {arg}", new() { Name = "Echo" })]
2014
});
2115
_ = server.RunAsync();
2216

src/ModelContextProtocol.Core/Client/McpClient.cs

Lines changed: 38 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -39,57 +39,48 @@ public McpClient(IClientTransport clientTransport, McpClientOptions? options, IL
3939

4040
EndpointName = clientTransport.Name;
4141

42-
if (options.Capabilities is { } capabilities)
42+
if (options.Handlers?.NotificationHandlers is { } notificationHandlers)
4343
{
44-
if (capabilities.NotificationHandlers is { } notificationHandlers)
45-
{
46-
NotificationHandlers.RegisterRange(notificationHandlers);
47-
}
48-
49-
if (capabilities.Sampling is { } samplingCapability)
50-
{
51-
if (samplingCapability.SamplingHandler is not { } samplingHandler)
52-
{
53-
throw new InvalidOperationException("Sampling capability was set but it did not provide a handler.");
54-
}
55-
56-
RequestHandlers.Set(
57-
RequestMethods.SamplingCreateMessage,
58-
(request, _, cancellationToken) => samplingHandler(
59-
request,
60-
request?.ProgressToken is { } token ? new TokenProgress(this, token) : NullProgress.Instance,
61-
cancellationToken),
62-
McpJsonUtilities.JsonContext.Default.CreateMessageRequestParams,
63-
McpJsonUtilities.JsonContext.Default.CreateMessageResult);
64-
}
65-
66-
if (capabilities.Roots is { } rootsCapability)
67-
{
68-
if (rootsCapability.RootsHandler is not { } rootsHandler)
69-
{
70-
throw new InvalidOperationException("Roots capability was set but it did not provide a handler.");
71-
}
44+
NotificationHandlers.RegisterRange(notificationHandlers);
45+
}
7246

73-
RequestHandlers.Set(
74-
RequestMethods.RootsList,
75-
(request, _, cancellationToken) => rootsHandler(request, cancellationToken),
76-
McpJsonUtilities.JsonContext.Default.ListRootsRequestParams,
77-
McpJsonUtilities.JsonContext.Default.ListRootsResult);
78-
}
47+
if (options.Handlers?.SamplingHandler is { } samplingHandler)
48+
{
49+
RequestHandlers.Set(
50+
RequestMethods.SamplingCreateMessage,
51+
(request, _, cancellationToken) => samplingHandler(
52+
request,
53+
request?.ProgressToken is { } token ? new TokenProgress(this, token) : NullProgress.Instance,
54+
cancellationToken),
55+
McpJsonUtilities.JsonContext.Default.CreateMessageRequestParams,
56+
McpJsonUtilities.JsonContext.Default.CreateMessageResult);
57+
58+
_options.Capabilities ??= new();
59+
_options.Capabilities.Sampling ??= new();
60+
}
7961

80-
if (capabilities.Elicitation is { } elicitationCapability)
81-
{
82-
if (elicitationCapability.ElicitationHandler is not { } elicitationHandler)
83-
{
84-
throw new InvalidOperationException("Elicitation capability was set but it did not provide a handler.");
85-
}
62+
if (options.Handlers?.RootsHandler is { } rootsHandler)
63+
{
64+
RequestHandlers.Set(
65+
RequestMethods.RootsList,
66+
(request, _, cancellationToken) => rootsHandler(request, cancellationToken),
67+
McpJsonUtilities.JsonContext.Default.ListRootsRequestParams,
68+
McpJsonUtilities.JsonContext.Default.ListRootsResult);
69+
70+
_options.Capabilities ??= new();
71+
_options.Capabilities.Roots ??= new();
72+
}
8673

87-
RequestHandlers.Set(
88-
RequestMethods.ElicitationCreate,
89-
(request, _, cancellationToken) => elicitationHandler(request, cancellationToken),
90-
McpJsonUtilities.JsonContext.Default.ElicitRequestParams,
91-
McpJsonUtilities.JsonContext.Default.ElicitResult);
92-
}
74+
if (options.Handlers?.ElicitationHandler is { } elicitationHandler)
75+
{
76+
RequestHandlers.Set(
77+
RequestMethods.ElicitationCreate,
78+
(request, _, cancellationToken) => elicitationHandler(request, cancellationToken),
79+
McpJsonUtilities.JsonContext.Default.ElicitRequestParams,
80+
McpJsonUtilities.JsonContext.Default.ElicitResult);
81+
82+
_options.Capabilities ??= new();
83+
_options.Capabilities.Elicitation ??= new();
9384
}
9485
}
9586

src/ModelContextProtocol.Core/Client/McpClientExtensions.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -965,11 +965,11 @@ internal static CreateMessageResult ToCreateMessageResult(this ChatResponse chat
965965
}
966966

967967
/// <summary>
968-
/// Creates a sampling handler for use with <see cref="SamplingCapability.SamplingHandler"/> that will
968+
/// Creates a sampling handler for use with <see cref="McpClientHandlers.SamplingHandler"/> that will
969969
/// satisfy sampling requests using the specified <see cref="IChatClient"/>.
970970
/// </summary>
971971
/// <param name="chatClient">The <see cref="IChatClient"/> with which to satisfy sampling requests.</param>
972-
/// <returns>The created handler delegate that can be assigned to <see cref="SamplingCapability.SamplingHandler"/>.</returns>
972+
/// <returns>The created handler delegate that can be assigned to <see cref="McpClientHandlers.SamplingHandler"/>.</returns>
973973
/// <remarks>
974974
/// <para>
975975
/// This method creates a function that converts MCP message requests into chat client calls, enabling
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
using Microsoft.Extensions.AI;
2+
using ModelContextProtocol.Protocol;
3+
4+
namespace ModelContextProtocol.Client;
5+
6+
/// <summary>
7+
/// Provides a container for handlers used in the creation of an MCP client.
8+
/// </summary>
9+
/// <remarks>
10+
/// <para>
11+
/// This class provides a centralized collection of delegates that implement various capabilities of the Model Context Protocol.
12+
/// </para>
13+
/// <para>
14+
/// Each handler in this class corresponds to a specific client endpoint in the Model Context Protocol and
15+
/// is responsible for processing a particular type of message. The handlers are used to customize
16+
/// the behavior of the MCP server by providing implementations for the various protocol operations.
17+
/// </para>
18+
/// <para>
19+
/// When a server sends a message to the client, the appropriate handler is invoked to process it
20+
/// according to the protocol specification. Which handler is selected
21+
/// is done based on an ordinal, case-sensitive string comparison.
22+
/// </para>
23+
/// </remarks>
24+
public class McpClientHandlers
25+
{
26+
27+
/// <summary>Gets or sets notification handlers to register with the client.</summary>
28+
/// <remarks>
29+
/// <para>
30+
/// When constructed, the client will enumerate these handlers once, which may contain multiple handlers per notification method key.
31+
/// The client will not re-enumerate the sequence after initialization.
32+
/// </para>
33+
/// <para>
34+
/// Notification handlers allow the client to respond to server-sent notifications for specific methods.
35+
/// Each key in the collection is a notification method name, and each value is a callback that will be invoked
36+
/// when a notification with that method is received.
37+
/// </para>
38+
/// <para>
39+
/// Handlers provided via <see cref="NotificationHandlers"/> will be registered with the client for the lifetime of the client.
40+
/// For transient handlers, <see cref="IMcpEndpoint.RegisterNotificationHandler"/> may be used to register a handler that can
41+
/// then be unregistered by disposing of the <see cref="IAsyncDisposable"/> returned from the method.
42+
/// </para>
43+
/// </remarks>
44+
public IEnumerable<KeyValuePair<string, Func<JsonRpcNotification, CancellationToken, ValueTask>>>? NotificationHandlers { get; set; }
45+
46+
/// <summary>
47+
/// Gets or sets the handler for <see cref="RequestMethods.RootsList"/> requests.
48+
/// </summary>
49+
/// <remarks>
50+
/// This handler is invoked when a client sends a <see cref="RequestMethods.RootsList"/> request to retrieve available roots.
51+
/// The handler receives request parameters and should return a <see cref="ListRootsResult"/> containing the collection of available roots.
52+
/// </remarks>
53+
public Func<ListRootsRequestParams?, CancellationToken, ValueTask<ListRootsResult>>? RootsHandler { get; set; }
54+
55+
/// <summary>
56+
/// Gets or sets the handler for processing <see cref="RequestMethods.ElicitationCreate"/> requests.
57+
/// </summary>
58+
/// <remarks>
59+
/// <para>
60+
/// This handler function is called when an MCP server requests the client to provide additional
61+
/// information during interactions. The client must set this property for the elicitation capability to work.
62+
/// </para>
63+
/// <para>
64+
/// The handler receives message parameters and a cancellation token.
65+
/// It should return a <see cref="ElicitResult"/> containing the response to the elicitation request.
66+
/// </para>
67+
/// </remarks>
68+
public Func<ElicitRequestParams?, CancellationToken, ValueTask<ElicitResult>>? ElicitationHandler { get; set; }
69+
70+
/// <summary>
71+
/// Gets or sets the handler for processing <see cref="RequestMethods.SamplingCreateMessage"/> requests.
72+
/// </summary>
73+
/// <remarks>
74+
/// <para>
75+
/// This handler function is called when an MCP server requests the client to generate content
76+
/// using an AI model. The client must set this property for the sampling capability to work.
77+
/// </para>
78+
/// <para>
79+
/// The handler receives message parameters, a progress reporter for updates, and a
80+
/// cancellation token. It should return a <see cref="CreateMessageResult"/> containing the
81+
/// generated content.
82+
/// </para>
83+
/// <para>
84+
/// You can create a handler using the <see cref="McpClientExtensions.CreateSamplingHandler"/> extension
85+
/// method with any implementation of <see cref="IChatClient"/>.
86+
/// </para>
87+
/// </remarks>
88+
public Func<CreateMessageRequestParams?, IProgress<ProgressNotificationValue>, CancellationToken, ValueTask<CreateMessageResult>>? SamplingHandler { get; set; }
89+
}

src/ModelContextProtocol.Core/Client/McpClientOptions.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,4 +63,9 @@ public sealed class McpClientOptions
6363
/// <para>The default value is 60 seconds.</para>
6464
/// </remarks>
6565
public TimeSpan InitializationTimeout { get; set; } = TimeSpan.FromSeconds(60);
66+
67+
/// <summary>
68+
/// Gets or sets the container of handlers used by the client for processing protocol messages.
69+
/// </summary>
70+
public McpClientHandlers? Handlers { get; set; }
6671
}

src/ModelContextProtocol.Core/Protocol/ClientCapabilities.cs

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ public sealed class ClientCapabilities
4545
/// </para>
4646
/// <para>
4747
/// The server can use <see cref="McpServerExtensions.RequestRootsAsync"/> to request the list of
48-
/// available roots from the client, which will trigger the client's <see cref="RootsCapability.RootsHandler"/>.
48+
/// available roots from the client, which will trigger the client's <see cref="ModelContextProtocol.Client.McpClientHandlers.RootsHandler"/>.
4949
/// </para>
5050
/// </remarks>
5151
[JsonPropertyName("roots")]
@@ -64,24 +64,4 @@ public sealed class ClientCapabilities
6464
/// </summary>
6565
[JsonPropertyName("elicitation")]
6666
public ElicitationCapability? Elicitation { get; set; }
67-
68-
/// <summary>Gets or sets notification handlers to register with the client.</summary>
69-
/// <remarks>
70-
/// <para>
71-
/// When constructed, the client will enumerate these handlers once, which may contain multiple handlers per notification method key.
72-
/// The client will not re-enumerate the sequence after initialization.
73-
/// </para>
74-
/// <para>
75-
/// Notification handlers allow the client to respond to server-sent notifications for specific methods.
76-
/// Each key in the collection is a notification method name, and each value is a callback that will be invoked
77-
/// when a notification with that method is received.
78-
/// </para>
79-
/// <para>
80-
/// Handlers provided via <see cref="NotificationHandlers"/> will be registered with the client for the lifetime of the client.
81-
/// For transient handlers, <see cref="IMcpEndpoint.RegisterNotificationHandler"/> may be used to register a handler that can
82-
/// then be unregistered by disposing of the <see cref="IAsyncDisposable"/> returned from the method.
83-
/// </para>
84-
/// </remarks>
85-
[JsonIgnore]
86-
public IEnumerable<KeyValuePair<string, Func<JsonRpcNotification, CancellationToken, ValueTask>>>? NotificationHandlers { get; set; }
8767
}

src/ModelContextProtocol.Core/Protocol/CompletionsCapability.cs

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
using ModelContextProtocol.Server;
2-
using System.Text.Json.Serialization;
3-
41
namespace ModelContextProtocol.Protocol;
52

63
/// <summary>
@@ -23,15 +20,4 @@ namespace ModelContextProtocol.Protocol;
2320
public sealed class CompletionsCapability
2421
{
2522
// Currently empty in the spec, but may be extended in the future.
26-
27-
/// <summary>
28-
/// Gets or sets the handler for completion requests.
29-
/// </summary>
30-
/// <remarks>
31-
/// This handler provides auto-completion suggestions for prompt arguments or resource references in the Model Context Protocol.
32-
/// The handler receives a reference type (e.g., "ref/prompt" or "ref/resource") and the current argument value,
33-
/// and should return appropriate completion suggestions.
34-
/// </remarks>
35-
[JsonIgnore]
36-
public McpRequestHandler<CompleteRequestParams, CompleteResult>? CompleteHandler { get; set; }
3723
}
Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
using System.Text.Json.Serialization;
2-
31
namespace ModelContextProtocol.Protocol;
42

53
/// <summary>
@@ -11,26 +9,10 @@ namespace ModelContextProtocol.Protocol;
119
/// </para>
1210
/// <para>
1311
/// When this capability is enabled, an MCP server can request the client to provide additional information
14-
/// during interactions. The client must set a <see cref="ElicitationHandler"/> to process these requests.
12+
/// during interactions. The client must set a <see cref="ModelContextProtocol.Client.McpClientHandlers.ElicitationHandler"/> to process these requests.
1513
/// </para>
1614
/// </remarks>
1715
public sealed class ElicitationCapability
1816
{
1917
// Currently empty in the spec, but may be extended in the future.
20-
21-
/// <summary>
22-
/// Gets or sets the handler for processing <see cref="RequestMethods.ElicitationCreate"/> requests.
23-
/// </summary>
24-
/// <remarks>
25-
/// <para>
26-
/// This handler function is called when an MCP server requests the client to provide additional
27-
/// information during interactions. The client must set this property for the elicitation capability to work.
28-
/// </para>
29-
/// <para>
30-
/// The handler receives message parameters and a cancellation token.
31-
/// It should return a <see cref="ElicitResult"/> containing the response to the elicitation request.
32-
/// </para>
33-
/// </remarks>
34-
[JsonIgnore]
35-
public Func<ElicitRequestParams?, CancellationToken, ValueTask<ElicitResult>>? ElicitationHandler { get; set; }
3618
}
Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
using ModelContextProtocol.Server;
2-
using System.Text.Json.Serialization;
3-
41
namespace ModelContextProtocol.Protocol;
52

63
/// <summary>
@@ -13,10 +10,4 @@ namespace ModelContextProtocol.Protocol;
1310
public sealed class LoggingCapability
1411
{
1512
// Currently empty in the spec, but may be extended in the future
16-
17-
/// <summary>
18-
/// Gets or sets the handler for set logging level requests from clients.
19-
/// </summary>
20-
[JsonIgnore]
21-
public McpRequestHandler<SetLevelRequestParams, EmptyResult>? SetLoggingLevelHandler { get; set; }
2213
}

0 commit comments

Comments
 (0)