Skip to content

Commit 0a0ff59

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. Also, Fix ListResourceTemplatesHandler issue.
1 parent ab6d3e1 commit 0a0ff59

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+1390
-1368
lines changed

README.md

Lines changed: 34 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -174,54 +174,50 @@ using System.Text.Json;
174174
McpServerOptions options = new()
175175
{
176176
ServerInfo = new Implementation { Name = "MyServer", Version = "1.0.0" },
177-
Capabilities = new ServerCapabilities
177+
Handlers = new McpServerHandlers()
178178
{
179-
Tools = new ToolsCapability
179+
ListToolsHandler = (request, cancellationToken) =>
180+
ValueTask.FromResult(new ListToolsResult
180181
{
181-
ListToolsHandler = (request, cancellationToken) =>
182-
ValueTask.FromResult(new ListToolsResult
182+
Tools =
183+
[
184+
new Tool
183185
{
184-
Tools =
185-
[
186-
new Tool
186+
Name = "echo",
187+
Description = "Echoes the input back to the client.",
188+
InputSchema = JsonSerializer.Deserialize<JsonElement>("""
187189
{
188-
Name = "echo",
189-
Description = "Echoes the input back to the client.",
190-
InputSchema = JsonSerializer.Deserialize<JsonElement>("""
191-
{
192-
"type": "object",
193-
"properties": {
194-
"message": {
195-
"type": "string",
196-
"description": "The input to echo back"
197-
}
198-
},
199-
"required": ["message"]
200-
}
201-
"""),
190+
"type": "object",
191+
"properties": {
192+
"message": {
193+
"type": "string",
194+
"description": "The input to echo back"
195+
}
196+
},
197+
"required": ["message"]
202198
}
203-
]
204-
}),
205-
206-
CallToolHandler = (request, cancellationToken) =>
199+
"""),
200+
}
201+
]
202+
}),
203+
CallToolHandler = (request, cancellationToken) =>
204+
{
205+
if (request.Params?.Name == "echo")
207206
{
208-
if (request.Params?.Name == "echo")
207+
if (request.Params.Arguments?.TryGetValue("message", out var message) is not true)
209208
{
210-
if (request.Params.Arguments?.TryGetValue("message", out var message) is not true)
211-
{
212-
throw new McpException("Missing required argument 'message'");
213-
}
214-
215-
return ValueTask.FromResult(new CallToolResult
216-
{
217-
Content = [new TextContentBlock { Text = $"Echo: {message}", Type = "text" }]
218-
});
209+
throw new McpException("Missing required argument 'message'");
219210
}
220211

221-
throw new McpException($"Unknown tool: '{request.Params?.Name}'");
222-
},
212+
return ValueTask.FromResult(new CallToolResult
213+
{
214+
Content = [new TextContentBlock { Text = $"Echo: {message}", Type = "text" }]
215+
});
216+
}
217+
218+
throw new McpException($"Unknown tool: '{request.Params?.Name}'");
223219
}
224-
},
220+
}
225221
};
226222

227223
await using McpServer server = McpServer.Create(new StdioServerTransport("MyServer"), options);

docs/concepts/elicitation/samples/client/Program.cs

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,9 @@
1818
Name = "ElicitationClient",
1919
Version = "1.0.0"
2020
},
21-
Capabilities = new()
21+
Handlers = new()
2222
{
23-
Elicitation = new()
24-
{
25-
ElicitationHandler = HandleElicitationAsync
26-
}
23+
ElicitationHandler = HandleElicitationAsync
2724
}
2825
};
2926

samples/AspNetCoreMcpPerSessionTools/Program.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
{
2525
mcpOptions.Capabilities = new();
2626
mcpOptions.Capabilities.Tools = new();
27-
var toolCollection = mcpOptions.Capabilities.Tools.ToolCollection = new();
27+
var toolCollection = mcpOptions.ToolCollection = new();
2828

2929
foreach (var tool in tools)
3030
{

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.Methods.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -631,11 +631,11 @@ internal static CreateMessageResult ToCreateMessageResult(ChatResponse chatRespo
631631
}
632632

633633
/// <summary>
634-
/// Creates a sampling handler for use with <see cref="SamplingCapability.SamplingHandler"/> that will
634+
/// Creates a sampling handler for use with <see cref="McpClientHandlers.SamplingHandler"/> that will
635635
/// satisfy sampling requests using the specified <see cref="IChatClient"/>.
636636
/// </summary>
637637
/// <param name="chatClient">The <see cref="IChatClient"/> with which to satisfy sampling requests.</param>
638-
/// <returns>The created handler delegate that can be assigned to <see cref="SamplingCapability.SamplingHandler"/>.</returns>
638+
/// <returns>The created handler delegate that can be assigned to <see cref="McpClientHandlers.SamplingHandler"/>.</returns>
639639
/// <exception cref="ArgumentNullException"><paramref name="chatClient"/> is <see langword="null"/>.</exception>
640640
public static Func<CreateMessageRequestParams?, IProgress<ProgressNotificationValue>, CancellationToken, ValueTask<CreateMessageResult>> CreateSamplingHandler(
641641
IChatClient chatClient)

src/ModelContextProtocol.Core/Client/McpClientExtensions.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,11 @@ namespace ModelContextProtocol.Client;
1919
public static class McpClientExtensions
2020
{
2121
/// <summary>
22-
/// Creates a sampling handler for use with <see cref="SamplingCapability.SamplingHandler"/> that will
22+
/// Creates a sampling handler for use with <see cref="McpClientHandlers.SamplingHandler"/> that will
2323
/// satisfy sampling requests using the specified <see cref="IChatClient"/>.
2424
/// </summary>
2525
/// <param name="chatClient">The <see cref="IChatClient"/> with which to satisfy sampling requests.</param>
26-
/// <returns>The created handler delegate that can be assigned to <see cref="SamplingCapability.SamplingHandler"/>.</returns>
26+
/// <returns>The created handler delegate that can be assigned to <see cref="McpClientHandlers.SamplingHandler"/>.</returns>
2727
/// <remarks>
2828
/// <para>
2929
/// 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/McpClientImpl.cs

Lines changed: 16 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -49,28 +49,20 @@ internal McpClientImpl(ITransport transport, string endpointName, McpClientOptio
4949
var notificationHandlers = new NotificationHandlers();
5050
var requestHandlers = new RequestHandlers();
5151

52-
if (options.Capabilities is { } capabilities)
53-
{
54-
RegisterHandlers(capabilities, notificationHandlers, requestHandlers);
55-
}
52+
RegisterHandlers(options.Handlers, notificationHandlers, requestHandlers);
5653

5754
_sessionHandler = new McpSessionHandler(isServer: false, transport, endpointName, requestHandlers, notificationHandlers, _logger);
5855
}
5956

60-
private void RegisterHandlers(ClientCapabilities capabilities, NotificationHandlers notificationHandlers, RequestHandlers requestHandlers)
57+
private void RegisterHandlers(McpClientHandlers handlers, NotificationHandlers notificationHandlers, RequestHandlers requestHandlers)
6158
{
62-
if (capabilities.NotificationHandlers is { } notificationHandlersFromCapabilities)
59+
if (handlers.NotificationHandlers is { } notificationHandlersFromOptions)
6360
{
64-
notificationHandlers.RegisterRange(notificationHandlersFromCapabilities);
61+
notificationHandlers.RegisterRange(notificationHandlersFromOptions);
6562
}
6663

67-
if (capabilities.Sampling is { } samplingCapability)
64+
if (handlers.SamplingHandler is { } samplingHandler)
6865
{
69-
if (samplingCapability.SamplingHandler is not { } samplingHandler)
70-
{
71-
throw new InvalidOperationException("Sampling capability was set but it did not provide a handler.");
72-
}
73-
7466
requestHandlers.Set(
7567
RequestMethods.SamplingCreateMessage,
7668
(request, _, cancellationToken) => samplingHandler(
@@ -79,34 +71,33 @@ private void RegisterHandlers(ClientCapabilities capabilities, NotificationHandl
7971
cancellationToken),
8072
McpJsonUtilities.JsonContext.Default.CreateMessageRequestParams,
8173
McpJsonUtilities.JsonContext.Default.CreateMessageResult);
74+
75+
_options.Capabilities ??= new();
76+
_options.Capabilities.Sampling ??= new();
8277
}
8378

84-
if (capabilities.Roots is { } rootsCapability)
79+
if (handlers.RootsHandler is { } rootsHandler)
8580
{
86-
if (rootsCapability.RootsHandler is not { } rootsHandler)
87-
{
88-
throw new InvalidOperationException("Roots capability was set but it did not provide a handler.");
89-
}
90-
9181
requestHandlers.Set(
9282
RequestMethods.RootsList,
9383
(request, _, cancellationToken) => rootsHandler(request, cancellationToken),
9484
McpJsonUtilities.JsonContext.Default.ListRootsRequestParams,
9585
McpJsonUtilities.JsonContext.Default.ListRootsResult);
86+
87+
_options.Capabilities ??= new();
88+
_options.Capabilities.Roots ??= new();
9689
}
9790

98-
if (capabilities.Elicitation is { } elicitationCapability)
91+
if (handlers.ElicitationHandler is { } elicitationHandler)
9992
{
100-
if (elicitationCapability.ElicitationHandler is not { } elicitationHandler)
101-
{
102-
throw new InvalidOperationException("Elicitation capability was set but it did not provide a handler.");
103-
}
104-
10593
requestHandlers.Set(
10694
RequestMethods.ElicitationCreate,
10795
(request, _, cancellationToken) => elicitationHandler(request, cancellationToken),
10896
McpJsonUtilities.JsonContext.Default.ElicitRequestParams,
10997
McpJsonUtilities.JsonContext.Default.ElicitResult);
98+
99+
_options.Capabilities ??= new();
100+
_options.Capabilities.Elicitation ??= new();
110101
}
111102
}
112103

src/ModelContextProtocol.Core/Client/McpClientOptions.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ namespace ModelContextProtocol.Client;
1111
/// </remarks>
1212
public sealed class McpClientOptions
1313
{
14+
private McpClientHandlers? _handlers;
15+
1416
/// <summary>
1517
/// Gets or sets information about this client implementation, including its name and version.
1618
/// </summary>
@@ -63,4 +65,17 @@ public sealed class McpClientOptions
6365
/// <para>The default value is 60 seconds.</para>
6466
/// </remarks>
6567
public TimeSpan InitializationTimeout { get; set; } = TimeSpan.FromSeconds(60);
68+
69+
/// <summary>
70+
/// Gets or sets the container of handlers used by the client for processing protocol messages.
71+
/// </summary>
72+
public McpClientHandlers Handlers
73+
{
74+
get => _handlers ??= new();
75+
set
76+
{
77+
Throw.IfNull(value);
78+
_handlers = value;
79+
}
80+
}
6681
}

0 commit comments

Comments
 (0)