Skip to content

Commit 139ace2

Browse files
Initial attempt at re/moving handlers to injectable construction time values instead of runtime.
1 parent ab5a950 commit 139ace2

File tree

10 files changed

+194
-239
lines changed

10 files changed

+194
-239
lines changed

src/ModelContextProtocol/Client/McpClient.cs

Lines changed: 51 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -25,21 +25,55 @@ internal sealed class McpClient : McpJsonRpcEndpoint, IMcpClient
2525
/// <param name="options">Options for the client, defining protocol version and capabilities.</param>
2626
/// <param name="serverConfig">The server configuration.</param>
2727
/// <param name="loggerFactory">The logger factory.</param>
28-
public McpClient(IClientTransport clientTransport, McpClientOptions options, McpServerConfig serverConfig, ILoggerFactory? loggerFactory)
29-
: base(loggerFactory)
28+
/// <param name="requestHandlers">The request handlers.</param>
29+
/// <param name="notificationHandlers">The notification handlers.</param>
30+
public McpClient(
31+
IClientTransport clientTransport,
32+
McpClientOptions options,
33+
McpServerConfig serverConfig,
34+
ILoggerFactory? loggerFactory,
35+
RequestHandlers? requestHandlers = null,
36+
NotificationHandlers? notificationHandlers = null)
37+
: base(loggerFactory, requestHandlers, notificationHandlers)
3038
{
3139
_clientTransport = clientTransport;
3240
_options = options;
41+
requestHandlers ??= [];
42+
notificationHandlers ??= [];
3343

3444
EndpointName = $"Client ({serverConfig.Id}: {serverConfig.Name})";
45+
46+
void SetRequestHandler<TParams, TResult>(
47+
string method,
48+
Func<TParams, CancellationToken, Task<TResult>> handler)
49+
where TParams : class
50+
where TResult : class
51+
{
52+
requestHandlers.Add(method, async (request, cancellationToken)
53+
=> request.Params is not TParams { } parameters
54+
? throw new InvalidOperationException($"Request {method} was sent with invalid parameters.")
55+
: await handler(parameters, cancellationToken).ConfigureAwait(false));
56+
}
57+
58+
void SetNotificationHandlers(IReadOnlyDictionary<string, List<Func<JsonRpcNotification, Task>>> handlers)
59+
{
60+
foreach (var pairs in handlers)
61+
{
62+
var key = pairs.Key;
63+
var list = pairs.Value;
64+
foreach (var item in list)
65+
{
66+
notificationHandlers.Add(key, item);
67+
}
68+
}
69+
}
3570

3671
if (options.Capabilities?.Sampling is { } samplingCapability)
3772
{
3873
if (samplingCapability.SamplingHandler is not { } samplingHandler)
3974
{
4075
throw new InvalidOperationException($"Sampling capability was set but it did not provide a handler.");
4176
}
42-
4377
SetRequestHandler<CreateMessageRequestParams, CreateMessageResult>(
4478
RequestMethods.SamplingCreateMessage,
4579
(request, cancellationToken) => samplingHandler(
@@ -92,21 +126,21 @@ public async Task ConnectAsync(CancellationToken cancellationToken = default)
92126
using var initializationCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
93127
initializationCts.CancelAfter(_options.InitializationTimeout);
94128

95-
try
96-
{
97-
// Send initialize request
98-
var initializeResponse = await SendRequestAsync<InitializeResult>(
99-
new JsonRpcRequest
100-
{
101-
Method = RequestMethods.Initialize,
102-
Params = new InitializeRequestParams()
129+
try
130+
{
131+
// Send initialize request
132+
var initializeResponse = await SendRequestAsync<InitializeResult>(
133+
new JsonRpcRequest
103134
{
104-
ProtocolVersion = _options.ProtocolVersion,
105-
Capabilities = _options.Capabilities ?? new ClientCapabilities(),
106-
ClientInfo = _options.ClientInfo
107-
}
108-
},
109-
initializationCts.Token).ConfigureAwait(false);
135+
Method = RequestMethods.Initialize,
136+
Params = new InitializeRequestParams()
137+
{
138+
ProtocolVersion = _options.ProtocolVersion,
139+
Capabilities = _options.Capabilities ?? new ClientCapabilities(),
140+
ClientInfo = _options.ClientInfo
141+
}
142+
},
143+
initializationCts.Token).ConfigureAwait(false);
110144

111145
// Store server information
112146
_logger.ServerCapabilitiesReceived(EndpointName,
@@ -143,20 +177,6 @@ await SendMessageAsync(
143177
}
144178
}
145179

146-
private void SetNotificationHandlers(
147-
IReadOnlyDictionary<string, List<Func<JsonRpcNotification, Task>>> notificationHandlers)
148-
{
149-
foreach (var handlers in notificationHandlers)
150-
{
151-
var key = handlers.Key;
152-
var list = handlers.Value;
153-
foreach (var item in list)
154-
{
155-
AddNotificationHandler(key, item);
156-
}
157-
}
158-
}
159-
160180
/// <inheritdoc/>
161181
public override async ValueTask DisposeUnsynchronizedAsync()
162182
{

src/ModelContextProtocol/Server/McpServer.cs

Lines changed: 55 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,27 @@
88

99
namespace ModelContextProtocol.Server;
1010

11+
internal class SetMcpHandlerHelper(McpServerOptions options)
12+
{
13+
public void SetRequestHandler<TParams, TResult>(
14+
string method,
15+
Func<TParams, CancellationToken, Task<TResult>> handler)
16+
where TParams : class
17+
where TResult : class
18+
{
19+
options.RequestHandlers.Add(method, async (request, cancellationToken) =>
20+
request.Params is not TParams { } parameters
21+
? throw new InvalidOperationException($"Request {method} was sent with invalid parameters.")
22+
: await handler(parameters, cancellationToken).ConfigureAwait(false));
23+
}
24+
25+
public void AddNotificationHandler(string method, Func<JsonRpcNotification, Task> handler)
26+
{
27+
var list = options.NotificationHandlers.GetOrAdd(method, _ => []);
28+
list.Add(handler);
29+
}
30+
}
31+
1132
/// <inheritdoc />
1233
internal sealed class McpServer : McpJsonRpcEndpoint, IMcpServer
1334
{
@@ -56,7 +77,9 @@ public McpServer(
5677
});
5778
};
5879

59-
AddNotificationHandler(NotificationMethods.InitializedNotification, _ =>
80+
SetMcpHandlerHelper setHandlerHelper = new(options);
81+
82+
setHandlerHelper.AddNotificationHandler(NotificationMethods.InitializedNotification, _ =>
6083
{
6184
if (ServerOptions.Capabilities?.Tools?.ToolCollection is { } tools)
6285
{
@@ -71,14 +94,13 @@ public McpServer(
7194
return Task.CompletedTask;
7295
});
7396

74-
SetToolsHandler(options);
75-
SetInitializeHandler(options);
76-
SetCompletionHandler(options);
77-
SetPingHandler();
78-
SetPromptsHandler(options);
79-
SetResourcesHandler(options);
80-
SetSetLoggingLevelHandler(options);
81-
SetNotificationHandlers(options);
97+
SetToolsHandler(options, setHandlerHelper);
98+
SetInitializeHandler(options, setHandlerHelper);
99+
SetCompletionHandler(options, setHandlerHelper);
100+
SetPingHandler(setHandlerHelper);
101+
SetPromptsHandler(options, setHandlerHelper);
102+
SetResourcesHandler(options, setHandlerHelper);
103+
SetSetLoggingLevelHandler(options, setHandlerHelper);
82104
}
83105

84106
public ServerCapabilities? ServerCapabilities { get; set; }
@@ -128,15 +150,15 @@ public override async ValueTask DisposeUnsynchronizedAsync()
128150
await base.DisposeUnsynchronizedAsync().ConfigureAwait(false);
129151
}
130152

131-
private void SetPingHandler()
153+
private void SetPingHandler(SetMcpHandlerHelper helper)
132154
{
133-
SetRequestHandler<JsonNode, PingResult>(RequestMethods.Ping,
155+
helper.SetRequestHandler<JsonNode, PingResult>(RequestMethods.Ping,
134156
(request, _) => Task.FromResult(new PingResult()));
135157
}
136158

137-
private void SetInitializeHandler(McpServerOptions options)
159+
private void SetInitializeHandler(McpServerOptions options, SetMcpHandlerHelper helper)
138160
{
139-
SetRequestHandler<InitializeRequestParams, InitializeResult>(RequestMethods.Initialize,
161+
helper.SetRequestHandler<InitializeRequestParams, InitializeResult>(RequestMethods.Initialize,
140162
(request, _) =>
141163
{
142164
ClientCapabilities = request?.Capabilities ?? new();
@@ -156,16 +178,14 @@ private void SetInitializeHandler(McpServerOptions options)
156178
});
157179
}
158180

159-
private void SetCompletionHandler(McpServerOptions options)
181+
private void SetCompletionHandler(McpServerOptions options, SetMcpHandlerHelper helper)
160182
{
161183
// This capability is not optional, so return an empty result if there is no handler.
162-
SetRequestHandler<CompleteRequestParams, CompleteResult>(RequestMethods.CompletionComplete,
163-
options.GetCompletionHandler is { } handler ?
164-
(request, ct) => handler(new(this, request), ct) :
165-
(request, ct) => Task.FromResult(new CompleteResult() { Completion = new() { Values = [], Total = 0, HasMore = false } }));
184+
helper.SetRequestHandler<CompleteRequestParams, CompleteResult>(RequestMethods.CompletionComplete,
185+
(request, ct) => Task.FromResult(new CompleteResult() { Completion = new() { Values = [], Total = 0, HasMore = false } }));
166186
}
167187

168-
private void SetResourcesHandler(McpServerOptions options)
188+
private void SetResourcesHandler(McpServerOptions options, SetMcpHandlerHelper helper)
169189
{
170190
if (options.Capabilities?.Resources is not { } resourcesCapability)
171191
{
@@ -183,11 +203,11 @@ private void SetResourcesHandler(McpServerOptions options)
183203

184204
listResourcesHandler ??= (static (_, _) => Task.FromResult(new ListResourcesResult()));
185205

186-
SetRequestHandler<ListResourcesRequestParams, ListResourcesResult>(RequestMethods.ResourcesList, (request, ct) => listResourcesHandler(new(this, request), ct));
187-
SetRequestHandler<ReadResourceRequestParams, ReadResourceResult>(RequestMethods.ResourcesRead, (request, ct) => readResourceHandler(new(this, request), ct));
206+
helper.SetRequestHandler<ListResourcesRequestParams, ListResourcesResult>(RequestMethods.ResourcesList, (request, ct) => listResourcesHandler(new(this, request), ct));
207+
helper.SetRequestHandler<ReadResourceRequestParams, ReadResourceResult>(RequestMethods.ResourcesRead, (request, ct) => readResourceHandler(new(this, request), ct));
188208

189209
listResourceTemplatesHandler ??= (static (_, _) => Task.FromResult(new ListResourceTemplatesResult()));
190-
SetRequestHandler<ListResourceTemplatesRequestParams, ListResourceTemplatesResult>(RequestMethods.ResourcesTemplatesList, (request, ct) => listResourceTemplatesHandler(new(this, request), ct));
210+
helper.SetRequestHandler<ListResourceTemplatesRequestParams, ListResourceTemplatesResult>(RequestMethods.ResourcesTemplatesList, (request, ct) => listResourceTemplatesHandler(new(this, request), ct));
191211

192212
if (resourcesCapability.Subscribe is not true)
193213
{
@@ -201,11 +221,11 @@ private void SetResourcesHandler(McpServerOptions options)
201221
throw new McpServerException("Resources capability was enabled with subscribe support, but SubscribeToResources and/or UnsubscribeFromResources handlers were not specified.");
202222
}
203223

204-
SetRequestHandler<SubscribeRequestParams, EmptyResult>(RequestMethods.ResourcesSubscribe, (request, ct) => subscribeHandler(new(this, request), ct));
205-
SetRequestHandler<UnsubscribeRequestParams, EmptyResult>(RequestMethods.ResourcesUnsubscribe, (request, ct) => unsubscribeHandler(new(this, request), ct));
224+
helper.SetRequestHandler<SubscribeRequestParams, EmptyResult>(RequestMethods.ResourcesSubscribe, (request, ct) => subscribeHandler(new(this, request), ct));
225+
helper.SetRequestHandler<UnsubscribeRequestParams, EmptyResult>(RequestMethods.ResourcesUnsubscribe, (request, ct) => unsubscribeHandler(new(this, request), ct));
206226
}
207227

208-
private void SetPromptsHandler(McpServerOptions options)
228+
private void SetPromptsHandler(McpServerOptions options, SetMcpHandlerHelper helper)
209229
{
210230
PromptsCapability? promptsCapability = options.Capabilities?.Prompts;
211231
var listPromptsHandler = promptsCapability?.ListPromptsHandler;
@@ -285,11 +305,11 @@ await originalListPromptsHandler(request, cancellationToken).ConfigureAwait(fals
285305
}
286306
}
287307

288-
SetRequestHandler<ListPromptsRequestParams, ListPromptsResult>(RequestMethods.PromptsList, (request, ct) => listPromptsHandler(new(this, request), ct));
289-
SetRequestHandler<GetPromptRequestParams, GetPromptResult>(RequestMethods.PromptsGet, (request, ct) => getPromptHandler(new(this, request), ct));
308+
helper.SetRequestHandler<ListPromptsRequestParams, ListPromptsResult>(RequestMethods.PromptsList, (request, ct) => listPromptsHandler(new(this, request), ct));
309+
helper.SetRequestHandler<GetPromptRequestParams, GetPromptResult>(RequestMethods.PromptsGet, (request, ct) => getPromptHandler(new(this, request), ct));
290310
}
291311

292-
private void SetToolsHandler(McpServerOptions options)
312+
private void SetToolsHandler(McpServerOptions options, SetMcpHandlerHelper helper)
293313
{
294314
ToolsCapability? toolsCapability = options.Capabilities?.Tools;
295315
var listToolsHandler = toolsCapability?.ListToolsHandler;
@@ -369,11 +389,13 @@ await originalListToolsHandler(request, cancellationToken).ConfigureAwait(false)
369389
}
370390
}
371391

372-
SetRequestHandler<ListToolsRequestParams, ListToolsResult>(RequestMethods.ToolsList, (request, ct) => listToolsHandler(new(this, request), ct));
373-
SetRequestHandler<CallToolRequestParams, CallToolResponse>(RequestMethods.ToolsCall, (request, ct) => callToolHandler(new(this, request), ct));
392+
helper.SetRequestHandler<ListToolsRequestParams, ListToolsResult>(RequestMethods.ToolsList,
393+
(request, ct) => listToolsHandler(new(this, request), ct));
394+
helper.SetRequestHandler<CallToolRequestParams, CallToolResponse>(RequestMethods.ToolsCall,
395+
(request, ct) => callToolHandler(new(this, request), ct));
374396
}
375397

376-
private void SetSetLoggingLevelHandler(McpServerOptions options)
398+
private void SetSetLoggingLevelHandler(McpServerOptions options, SetMcpHandlerHelper helper)
377399
{
378400
if (options.Capabilities?.Logging is not { } loggingCapability)
379401
{
@@ -385,29 +407,7 @@ private void SetSetLoggingLevelHandler(McpServerOptions options)
385407
throw new McpServerException("Logging capability was enabled, but SetLoggingLevelHandler was not specified.");
386408
}
387409

388-
SetRequestHandler<SetLevelRequestParams, EmptyResult>(RequestMethods.LoggingSetLevel, (request, ct) => setLoggingLevelHandler(new(this, request), ct));
389-
}
390-
391-
private void SetNotificationHandlers(McpServerOptions options)
392-
{
393-
if (options.NotificationHandlers is not { } handlers)
394-
{
395-
throw new McpServerException("Experimental capability was enabled, but NotificationHandlers were not specified.");
396-
}
397-
398-
foreach (var handler in handlers)
399-
{
400-
var key = handler.Key;
401-
var list = handler.Value;
402-
foreach (var item in list)
403-
{
404-
SetNotificationHandler(key, item);
405-
}
406-
}
407-
}
408-
409-
private void SetNotificationHandler(string method, Func<JsonRpcNotification, Task> handler)
410-
{
411-
AddNotificationHandler(method, handler);
410+
helper.SetRequestHandler<SetLevelRequestParams, EmptyResult>(RequestMethods.LoggingSetLevel,
411+
(request, ct) => setLoggingLevelHandler(new(this, request), ct));
412412
}
413413
}

src/ModelContextProtocol/Server/McpServerHandlers.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,5 @@ internal void OverwriteWithSetHandlers(McpServerOptions options)
113113
options.Capabilities.Prompts = promptsCapability;
114114
options.Capabilities.Resources = resourcesCapability;
115115
options.Capabilities.Tools = toolsCapability;
116-
117-
options.GetCompletionHandler = GetCompletionHandler ?? options.GetCompletionHandler;
118116
}
119117
}

src/ModelContextProtocol/Server/McpServerOptions.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using ModelContextProtocol.Protocol.Messages;
33
using System.Text.Json.Serialization;
44
using ModelContextProtocol.Shared;
5+
using System.Collections.Concurrent;
56

67
namespace ModelContextProtocol.Server;
78

@@ -38,14 +39,14 @@ public class McpServerOptions
3839
public string ServerInstructions { get; set; } = string.Empty;
3940

4041
/// <summary>
41-
/// Gets or sets the handler for get completion requests.
42+
/// Gets or sets the handler for get notifications.
4243
/// </summary>
4344
[JsonIgnore]
44-
public Func<RequestContext<CompleteRequestParams>, CancellationToken, Task<CompleteResult>>? GetCompletionHandler { get; set; }
45+
public ConcurrentDictionary<string, List<Func<JsonRpcNotification, Task>>> NotificationHandlers { get; init; } = new NotificationHandlers();
4546

4647
/// <summary>
47-
/// Gets or sets the handler for get notifications.
48+
/// Gets or sets the handler for get requests.
4849
/// </summary>
4950
[JsonIgnore]
50-
public IReadOnlyDictionary<string, List<Func<JsonRpcNotification, Task>>> NotificationHandlers { get; init; } = new NotificationHandlers();
51+
public Dictionary<string, Func<JsonRpcRequest, CancellationToken, Task<object?>>> RequestHandlers { get; init; } = new RequestHandlers();
5152
}

0 commit comments

Comments
 (0)