Make client config optional to McpClientFactory.CreateAsync (#65)

* Make client config optional to McpClientFactory.CreateAsync

Unless you're specifying capabilities (sampling/rooting) or need a specific client name/version, the client information can be inferred from the assembly, simplifying the getting-started experience.

* Cache default options

* Update src/ModelContextProtocol/Client/McpClientFactory.cs

Co-authored-by: Eirik Tsarpalis <eirik.tsarpalis@gmail.com>

---------

Co-authored-by: Eirik Tsarpalis <eirik.tsarpalis@gmail.com>
This commit is contained in:
Stephen Toub 2025-03-24 11:01:13 -04:00 • committed by GitHub
commit 729e688b25
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 50 additions and 23 deletions

View file

@ -20,16 +20,10 @@ For more information about MCP:
## Getting Started (Client) ## Getting Started (Client)
To get started writing a client, the `McpClientFactory.CreateAsync` method is used to instantiate and connect an `IMcpClient` To get started writing a client, the `McpClientFactory.CreateAsync` method is used to instantiate and connect an `IMcpClient`
to a server, with details about the client and server specified in `McpClientOptions` and `McpServerConfig` objects. to a server. Once you have an `IMcpClient`, you can interact with it, such as to enumerate all available tools and invoke tools.
Once you have an `IMcpClient`, you can interact with it, such as to enumerate all available tools and invoke tools.
```csharp ```csharp
McpClientOptions options = new() var client = await McpClientFactory.CreateAsync(new()
{
ClientInfo = new() { Name = "TestClient", Version = "1.0.0" }
};
McpServerConfig config = new()
{ {
Id = "everything", Id = "everything",
Name = "Everything", Name = "Everything",
@ -39,9 +33,7 @@ McpServerConfig config = new()
["command"] = "npx", ["command"] = "npx",
["arguments"] = "-y @modelcontextprotocol/server-everything", ["arguments"] = "-y @modelcontextprotocol/server-everything",
} }
}; });
var client = await McpClientFactory.CreateAsync(config, options);
// Print the list of tools available from the server. // Print the list of tools available from the server.
await foreach (var tool in client.ListToolsAsync()) await foreach (var tool in client.ListToolsAsync())

View file

@ -15,8 +15,7 @@ var mcpClient = await McpClientFactory.CreateAsync(
{ {
["command"] = "npx", ["arguments"] = "-y @modelcontextprotocol/server-everything", ["command"] = "npx", ["arguments"] = "-y @modelcontextprotocol/server-everything",
} }
}, });
new() { ClientInfo = new() { Name = "ChatClient", Version = "1.0.0" } });
// Get all available tools // Get all available tools
Console.WriteLine("Tools available:"); Console.WriteLine("Tools available:");

View file

@ -6,15 +6,36 @@ using ModelContextProtocol.Protocol.Transport;
using ModelContextProtocol.Utils; using ModelContextProtocol.Utils;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Logging.Abstractions;
using System.Reflection;
namespace ModelContextProtocol.Client; namespace ModelContextProtocol.Client;
/// <summary>Provides factory methods for creating MCP clients.</summary> /// <summary>Provides factory methods for creating MCP clients.</summary>
public static class McpClientFactory public static class McpClientFactory
{ {
/// <summary>Default client options to use when none are supplied.</summary>
private static readonly McpClientOptions s_defaultClientOptions = CreateDefaultClientOptions();
/// <summary>Creates default client options to use when no options are supplied.</summary>
private static McpClientOptions CreateDefaultClientOptions()
{
var asmName = (Assembly.GetEntryAssembly() ?? Assembly.GetExecutingAssembly()).GetName();
return new()
{
ClientInfo = new()
{
Name = asmName.Name ?? "McpClient",
Version = asmName.Version?.ToString() ?? "1.0.0",
},
};
}
/// <summary>Creates an <see cref="IMcpClient"/>, connecting it to the specified server.</summary> /// <summary>Creates an <see cref="IMcpClient"/>, connecting it to the specified server.</summary>
/// <param name="serverConfig">Configuration for the target server to which the client should connect.</param> /// <param name="serverConfig">Configuration for the target server to which the client should connect.</param>
/// <param name="clientOptions">A client configuration object which specifies client capabilities and protocol version.</param> /// <param name="clientOptions">
/// A client configuration object which specifies client capabilities and protocol version.
/// If <see langword="null"/>, details based on the current process will be employed.
/// </param>
/// <param name="createTransportFunc">An optional factory method which returns transport implementations based on a server configuration.</param> /// <param name="createTransportFunc">An optional factory method which returns transport implementations based on a server configuration.</param>
/// <param name="loggerFactory">A logger factory for creating loggers for clients.</param> /// <param name="loggerFactory">A logger factory for creating loggers for clients.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param> /// <param name="cancellationToken">A token to cancel the operation.</param>
@ -25,14 +46,14 @@ public static class McpClientFactory
/// <exception cref="InvalidOperationException"><paramref name="createTransportFunc"/> returns an invalid transport.</exception> /// <exception cref="InvalidOperationException"><paramref name="createTransportFunc"/> returns an invalid transport.</exception>
public static async Task<IMcpClient> CreateAsync( public static async Task<IMcpClient> CreateAsync(
McpServerConfig serverConfig, McpServerConfig serverConfig,
McpClientOptions clientOptions, McpClientOptions? clientOptions = null,
Func<McpServerConfig, ILoggerFactory?, IClientTransport>? createTransportFunc = null, Func<McpServerConfig, ILoggerFactory?, IClientTransport>? createTransportFunc = null,
ILoggerFactory? loggerFactory = null, ILoggerFactory? loggerFactory = null,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
{ {
Throw.IfNull(serverConfig); Throw.IfNull(serverConfig);
Throw.IfNull(clientOptions);
clientOptions ??= s_defaultClientOptions;
createTransportFunc ??= CreateTransport; createTransportFunc ??= CreateTransport;
string endpointName = $"Client ({serverConfig.Id}: {serverConfig.Name})"; string endpointName = $"Client ({serverConfig.Id}: {serverConfig.Name})";

View file

@ -19,13 +19,6 @@ public class McpClientFactoryTests
{ {
await Assert.ThrowsAsync<ArgumentNullException>("serverConfig", () => McpClientFactory.CreateAsync((McpServerConfig)null!, _defaultOptions, cancellationToken: TestContext.Current.CancellationToken)); await Assert.ThrowsAsync<ArgumentNullException>("serverConfig", () => McpClientFactory.CreateAsync((McpServerConfig)null!, _defaultOptions, cancellationToken: TestContext.Current.CancellationToken));
await Assert.ThrowsAsync<ArgumentNullException>("clientOptions", () => McpClientFactory.CreateAsync(new McpServerConfig()
{
Name = "name",
Id = "id",
TransportType = TransportTypes.StdIo,
}, (McpClientOptions)null!, cancellationToken: TestContext.Current.CancellationToken));
await Assert.ThrowsAsync<ArgumentException>("serverConfig", () => McpClientFactory.CreateAsync(new McpServerConfig() await Assert.ThrowsAsync<ArgumentException>("serverConfig", () => McpClientFactory.CreateAsync(new McpServerConfig()
{ {
Name = "name", Name = "name",
@ -41,6 +34,28 @@ public class McpClientFactoryTests
}, _defaultOptions, (_, __) => null!, cancellationToken: TestContext.Current.CancellationToken)); }, _defaultOptions, (_, __) => null!, cancellationToken: TestContext.Current.CancellationToken));
} }
[Fact]
public async Task CreateAsync_NullOptions_EntryAssemblyInferred()
{
// Arrange
var serverConfig = new McpServerConfig
{
Id = "test-server",
Name = "Test Server",
TransportType = TransportTypes.StdIo,
Location = "/path/to/server",
};
// Act
await using var client = await McpClientFactory.CreateAsync(
serverConfig,
null,
(_, __) => new NopTransport(),
cancellationToken: TestContext.Current.CancellationToken);
Assert.NotNull(client);
}
[Fact] [Fact]
public async Task CreateAsync_WithValidStdioConfig_CreatesNewClient() public async Task CreateAsync_WithValidStdioConfig_CreatesNewClient()
{ {