Skip to content

Commit 08b0a04

Browse files
authored
Overhaul McpClientFactory/McpServerFactory (#87)
Handlers are now specified before connecting the client/server. Otherwise, race conditions exist. Handlers move into being a part of the McpClientOptions/McpServerOptions, so that they're provided to the factories. Where relevant, the handlers are specified as part of the capability descriptors, so that they go hand in hand. The factories are no longer stateful. Instead of allocating a factory and then calling a create method on it, you just call a static factory method.
1 parent 63b24a4 commit 08b0a04

Some content is hidden

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

47 files changed

+1306
-2243
lines changed

README.MD

Lines changed: 42 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,7 @@ McpServerConfig config = new()
7171
}
7272
};
7373

74-
var factory = new McpClientFactory([config], options, NullLoggerFactory.Instance);
75-
76-
var client = await factory.GetClientAsync("everything");
74+
var client = await McpClientFactory.CreateAsync(config, options);
7775

7876
// Print the list of tools available from the server.
7977
await foreach (var tool in client.ListToolsAsync())
@@ -136,63 +134,65 @@ using McpDotNet.Protocol.Types;
136134
using McpDotNet.Server;
137135
using Microsoft.Extensions.Logging.Abstractions;
138136

139-
var loggerFactory = NullLoggerFactory.Instance;
140137
McpServerOptions options = new()
141138
{
142139
ServerInfo = new() { Name = "MyServer", Version = "1.0.0" },
143-
Capabilities = new() { Tools = new() },
144-
};
145-
McpServerFactory factory = new(new StdioServerTransport("MyServer", loggerFactory), options, loggerFactory);
146-
IMcpServer server = factory.CreateServer();
147-
148-
server.SetListToolsHandler(async (request, cancellationToken) =>
149-
{
150-
return new ListToolsResult()
140+
Capabilities = new()
151141
{
152-
Tools =
153-
[
154-
new Tool()
142+
Tools = new()
143+
{
144+
ListToolsHandler = async (request, cancellationToken) =>
155145
{
156-
Name = "echo",
157-
Description = "Echoes the input back to the client.",
158-
InputSchema = new JsonSchema()
146+
return new ListToolsResult()
159147
{
160-
Type = "object",
161-
Properties = new Dictionary<string, JsonSchemaProperty>()
148+
Tools =
149+
[
150+
new Tool()
151+
{
152+
Name = "echo",
153+
Description = "Echoes the input back to the client.",
154+
InputSchema = new JsonSchema()
155+
{
156+
Type = "object",
157+
Properties = new Dictionary<string, JsonSchemaProperty>()
158+
{
159+
["message"] = new JsonSchemaProperty() { Type = "string", Description = "The input to echo back." }
160+
}
161+
},
162+
}
163+
]
164+
};
165+
},
166+
167+
CallToolHandler = async (request, cancellationToken) =>
168+
{
169+
if (request.Params?.Name == "echo")
170+
{
171+
if (request.Params.Arguments?.TryGetValue("message", out var message) is not true)
162172
{
163-
["message"] = new JsonSchemaProperty() { Type = "string", Description = "The input to echo back." }
173+
throw new McpServerException("Missing required argument 'message'");
164174
}
165-
},
166-
}
167-
]
168-
};
169-
});
170175

171-
server.SetCallToolHandler(async (request, cancellationToken) =>
172-
{
173-
if (request.Params?.Name == "echo")
174-
{
175-
if (request.Params.Arguments?.TryGetValue("message", out var message) is not true)
176-
{
177-
throw new McpServerException("Missing required argument 'message'");
178-
}
176+
return new CallToolResponse()
177+
{
178+
Content = [new Content() { Text = $"Echo: {message}", Type = "text" }]
179+
};
180+
}
179181

180-
return new CallToolResponse()
181-
{
182-
Content = [new Content() { Text = $"Echo: {message}", Type = "text" }]
183-
};
184-
}
182+
throw new McpServerException($"Unknown tool: '{request.Params?.Name}'");
183+
},
184+
}
185+
},
186+
};
185187

186-
throw new McpServerException($"Unknown tool: '{request.Params?.Name}'");
187-
});
188+
await using IMcpServer server = McpServerFactory.Create(new StdioServerTransport("MyServer"), options);
188189

189190
await server.StartAsync();
190191

191192
// Run until process is stopped by the client (parent process)
192193
await Task.Delay(Timeout.Infinite);
193194
```
194195

195-
196196
## Roadmap
197197

198198
- Expand documentation with detailed guides for:

samples/anthropic/tools/ToolsConsole/Program.cs

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,21 @@
11
using Anthropic.SDK;
22
using Anthropic.SDK.Constants;
33
using Anthropic.SDK.Messaging;
4-
using System.Linq;
54
using McpDotNet;
65
using McpDotNet.Client;
76
using McpDotNet.Configuration;
87
using McpDotNet.Protocol.Transport;
9-
using Microsoft.Extensions.Logging.Abstractions;
108

119
internal class Program
1210
{
1311
private static async Task<IMcpClient> GetMcpClientAsync()
1412
{
15-
16-
McpClientOptions options = new()
13+
McpClientOptions clientOptions = new()
1714
{
1815
ClientInfo = new() { Name = "SimpleToolsConsole", Version = "1.0.0" }
1916
};
2017

21-
var config = new McpServerConfig
18+
McpServerConfig serverConfig = new()
2219
{
2320
Id = "everything",
2421
Name = "Everything",
@@ -30,21 +27,15 @@ private static async Task<IMcpClient> GetMcpClientAsync()
3027
}
3128
};
3229

33-
var factory = new McpClientFactory(
34-
[config],
35-
options,
36-
NullLoggerFactory.Instance
37-
);
38-
39-
return await factory.GetClientAsync("everything");
30+
return await McpClientFactory.CreateAsync(serverConfig, clientOptions);
4031
}
4132

4233
private static async Task Main(string[] args)
4334
{
4435
try
4536
{
4637
Console.WriteLine("Initializing MCP 'everything' server");
47-
var client = await GetMcpClientAsync();
38+
await using var client = await GetMcpClientAsync();
4839
Console.WriteLine("MCP 'everything' server initialized");
4940
Console.WriteLine("Listing tools...");
5041
var tools = await client.ListToolsAsync().ToListAsync();
@@ -60,10 +51,10 @@ private static async Task Main(string[] args)
6051

6152
Console.WriteLine("Asking Claude to call the Echo Tool...");
6253

63-
var messages = new List<Message>
64-
{
54+
List<Message> messages =
55+
[
6556
new Message(RoleType.User, "Please call the echo tool with the string 'Hello MCP!' and show me the echoed response.")
66-
};
57+
];
6758

6859
var parameters = new MessageParameters()
6960
{

samples/microsoft.extensions.ai/tools/ToolsConsole/Program.cs

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,18 @@
33
using McpDotNet.Extensions.AI;
44
using McpDotNet.Protocol.Transport;
55
using Microsoft.Extensions.AI;
6-
using Microsoft.Extensions.Logging.Abstractions;
76
using OpenAI;
87

98
internal class Program
109
{
1110
private static async Task<IMcpClient> GetMcpClientAsync()
1211
{
13-
14-
McpClientOptions options = new()
12+
McpClientOptions clientOptions = new()
1513
{
1614
ClientInfo = new() { Name = "SimpleToolsConsole", Version = "1.0.0" }
1715
};
1816

19-
var config = new McpServerConfig
17+
McpServerConfig serverConfig = new()
2018
{
2119
Id = "everything",
2220
Name = "Everything",
@@ -28,21 +26,15 @@ private static async Task<IMcpClient> GetMcpClientAsync()
2826
}
2927
};
3028

31-
var factory = new McpClientFactory(
32-
[config],
33-
options,
34-
NullLoggerFactory.Instance
35-
);
36-
37-
return await factory.GetClientAsync("everything");
29+
return await McpClientFactory.CreateAsync(serverConfig, clientOptions);
3830
}
3931

4032
private static async Task Main(string[] args)
4133
{
4234
try
4335
{
4436
Console.WriteLine("Initializing MCP 'everything' server");
45-
var client = await GetMcpClientAsync();
37+
await using var client = await GetMcpClientAsync();
4638
Console.WriteLine("MCP 'everything' server initialized");
4739
Console.WriteLine("Listing tools...");
4840
var mappedTools = await client.ListToolsAsync().Select(t => t.ToAITool(client)).ToListAsync();
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace System.Runtime.CompilerServices;
5+
6+
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)]
7+
internal sealed class CallerArgumentExpressionAttribute : Attribute
8+
{
9+
public CallerArgumentExpressionAttribute(string parameterName)
10+
{
11+
ParameterName = parameterName;
12+
}
13+
14+
public string ParameterName { get; }
15+
}

src/Common/Polyfills/System/Threading/CancellationTokenSourceExtensions.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
using System.Text;
2-
31
namespace System.Threading.Tasks;
42

53
internal static class CancellationTokenSourceExtensions

src/McpDotNet.Extensions.AI/McpSessionScope.cs

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
using McpDotNet.Configuration;
33
using Microsoft.Extensions.AI;
44
using Microsoft.Extensions.Logging;
5-
using Microsoft.Extensions.Logging.Abstractions;
65

76
namespace McpDotNet.Extensions.AI;
87

@@ -45,7 +44,7 @@ public static async Task<McpSessionScope> CreateAsync(McpServerConfig serverConf
4544
}
4645

4746
var scope = new McpSessionScope();
48-
var client = await scope.AddClientAsync(serverConfig, options, loggerFactory).ConfigureAwait(false);
47+
var client = await AddClientAsync(serverConfig, options, loggerFactory).ConfigureAwait(false);
4948

5049
scope.Tools = [];
5150
await foreach (var tool in client.ListToolsAsync().ConfigureAwait(false))
@@ -81,7 +80,7 @@ public static async Task<McpSessionScope> CreateAsync(IEnumerable<McpServerConfi
8180

8281
foreach (var config in serverConfigs)
8382
{
84-
var client = await scope.AddClientAsync(config, options, loggerFactory).ConfigureAwait(false);
83+
var client = await AddClientAsync(config, options, loggerFactory).ConfigureAwait(false);
8584

8685
scope.Tools ??= [];
8786
await foreach (var tool in client.ListToolsAsync().ConfigureAwait(false))
@@ -95,17 +94,15 @@ public static async Task<McpSessionScope> CreateAsync(IEnumerable<McpServerConfi
9594
return scope;
9695
}
9796

98-
private async Task<IMcpClient> AddClientAsync(McpServerConfig config,
99-
McpClientOptions? options,
97+
private static Task<IMcpClient> AddClientAsync(
98+
McpServerConfig serverConfig,
99+
McpClientOptions? clientOptions,
100100
ILoggerFactory? loggerFactory = null)
101101
{
102-
using var factory = new McpClientFactory([config],
103-
options ?? new() { ClientInfo = new() { Name = "AnonymousClient", Version = "1.0.0.0" } },
104-
loggerFactory ?? NullLoggerFactory.Instance);
105-
factory.DisposeClientsOnDispose = false;
106-
var client = await factory.GetClientAsync(config.Id).ConfigureAwait(false);
107-
_clients.Add(client);
108-
return client;
102+
return McpClientFactory.CreateAsync(
103+
serverConfig,
104+
clientOptions ?? new() { ClientInfo = new() { Name = "AnonymousClient", Version = "1.0.0.0" } },
105+
loggerFactory: loggerFactory);
109106
}
110107

111108
/// <inheritdoc/>

src/mcpdotnet/Client/IMcpClient.cs

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -30,20 +30,6 @@ public interface IMcpClient : IAsyncDisposable
3030
/// </summary>
3131
string? ServerInstructions { get; }
3232

33-
/// <summary>Sets a handler for the named operation.</summary>
34-
/// <param name="operationName">The name of the operation.</param>
35-
/// <param name="handler">The handler. Each operation requires a specific delegate signature.</param>
36-
/// <remarks>
37-
/// <para>
38-
/// Each operation may have only a single handler. Setting a handler for an operation that already has one
39-
/// will replace the existing handler.
40-
/// </para>
41-
/// <para>
42-
/// <see cref="OperationNames"> provides constants for common operations.</see>
43-
/// </para>
44-
/// </remarks>
45-
void SetOperationHandler(string operationName, Delegate handler);
46-
4733
/// <summary>
4834
/// Adds a handler for server notifications of a specific method.
4935
/// </summary>
@@ -60,13 +46,6 @@ public interface IMcpClient : IAsyncDisposable
6046
/// </remarks>
6147
void AddNotificationHandler(string method, Func<JsonRpcNotification, Task> handler);
6248

63-
/// <summary>
64-
/// Establishes a connection to the server.
65-
/// </summary>
66-
/// <param name="cancellationToken">A token to cancel the operation.</param>
67-
/// <returns>A task representing the asynchronous operation.</returns>
68-
Task ConnectAsync(CancellationToken cancellationToken = default);
69-
7049
/// <summary>
7150
/// Sends a generic JSON-RPC request to the server.
7251
/// </summary>

0 commit comments

Comments
 (0)