using System.Globalization;
using System.Runtime.InteropServices;
using ModelContextProtocol.Logging;
using ModelContextProtocol.Protocol.Transport;
using ModelContextProtocol.Utils;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using System.Reflection;
namespace ModelContextProtocol.Client;
/// Provides factory methods for creating MCP clients.
public static class McpClientFactory
{
/// Default client options to use when none are supplied.
private static readonly McpClientOptions s_defaultClientOptions = CreateDefaultClientOptions();
/// Creates default client options to use when no options are supplied.
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",
},
};
}
/// Creates an , connecting it to the specified server.
/// Configuration for the target server to which the client should connect.
///
/// A client configuration object which specifies client capabilities and protocol version.
/// If , details based on the current process will be employed.
///
/// An optional factory method which returns transport implementations based on a server configuration.
/// A logger factory for creating loggers for clients.
/// A token to cancel the operation.
/// An that's connected to the specified server.
/// is .
/// is .
/// contains invalid information.
/// returns an invalid transport.
public static async Task CreateAsync(
McpServerConfig serverConfig,
McpClientOptions? clientOptions = null,
Func? createTransportFunc = null,
ILoggerFactory? loggerFactory = null,
CancellationToken cancellationToken = default)
{
Throw.IfNull(serverConfig);
clientOptions ??= s_defaultClientOptions;
createTransportFunc ??= CreateTransport;
string endpointName = $"Client ({serverConfig.Id}: {serverConfig.Name})";
var logger = loggerFactory?.CreateLogger(typeof(McpClientFactory)) ?? NullLogger.Instance;
logger.CreatingClient(endpointName);
var transport =
createTransportFunc(serverConfig, loggerFactory) ??
throw new InvalidOperationException($"{nameof(createTransportFunc)} returned a null transport.");
try
{
McpClient client = new(transport, clientOptions, serverConfig, loggerFactory);
try
{
await client.ConnectAsync(cancellationToken).ConfigureAwait(false);
logger.ClientCreated(endpointName);
return client;
}
catch
{
await client.DisposeAsync().ConfigureAwait(false);
throw;
}
}
catch
{
if (transport is IAsyncDisposable asyncDisposableTransport)
{
await asyncDisposableTransport.DisposeAsync().ConfigureAwait(false);
}
else if (transport is IDisposable disposableTransport)
{
disposableTransport.Dispose();
}
throw;
}
}
private static IClientTransport CreateTransport(McpServerConfig serverConfig, ILoggerFactory? loggerFactory)
{
if (string.Equals(serverConfig.TransportType, TransportTypes.StdIo, StringComparison.OrdinalIgnoreCase))
{
string? command = serverConfig.TransportOptions?.GetValueOrDefault("command");
if (string.IsNullOrWhiteSpace(command))
{
command = serverConfig.Location;
if (string.IsNullOrWhiteSpace(command))
{
throw new ArgumentException("Command is required for stdio transport.", nameof(serverConfig));
}
}
string? arguments = serverConfig.TransportOptions?.GetValueOrDefault("arguments");
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) &&
serverConfig.TransportType.Equals(TransportTypes.StdIo, StringComparison.OrdinalIgnoreCase) &&
!string.IsNullOrEmpty(command) &&
!string.Equals(Path.GetFileName(command), "cmd.exe", StringComparison.OrdinalIgnoreCase))
{
// On Windows, for stdio, we need to wrap non-shell commands with cmd.exe /c {command} (usually npx or uvicorn).
// The stdio transport will not work correctly if the command is not run in a shell.
arguments = string.IsNullOrWhiteSpace(arguments) ?
$"/c {command}" :
$"/c {command} {arguments}";
command = "cmd.exe";
}
return new StdioClientTransport(new StdioClientTransportOptions
{
Command = command!,
Arguments = arguments,
WorkingDirectory = serverConfig.TransportOptions?.GetValueOrDefault("workingDirectory"),
EnvironmentVariables = serverConfig.TransportOptions?
.Where(kv => kv.Key.StartsWith("env:", StringComparison.Ordinal))
.ToDictionary(kv => kv.Key.Substring("env:".Length), kv => kv.Value),
ShutdownTimeout = TimeSpan.TryParse(serverConfig.TransportOptions?.GetValueOrDefault("shutdownTimeout"), CultureInfo.InvariantCulture, out var timespan) ? timespan : StdioClientTransportOptions.DefaultShutdownTimeout
}, serverConfig, loggerFactory);
}
if (string.Equals(serverConfig.TransportType, TransportTypes.Sse, StringComparison.OrdinalIgnoreCase) ||
string.Equals(serverConfig.TransportType, "http", StringComparison.OrdinalIgnoreCase))
{
return new SseClientTransport(new SseClientTransportOptions
{
ConnectionTimeout = TimeSpan.FromSeconds(ParseInt32OrDefault(serverConfig.TransportOptions, "connectionTimeout", 30)),
MaxReconnectAttempts = ParseInt32OrDefault(serverConfig.TransportOptions, "maxReconnectAttempts", 3),
ReconnectDelay = TimeSpan.FromSeconds(ParseInt32OrDefault(serverConfig.TransportOptions, "reconnectDelay", 5)),
AdditionalHeaders = serverConfig.TransportOptions?
.Where(kv => kv.Key.StartsWith("header.", StringComparison.Ordinal))
.ToDictionary(kv => kv.Key.Substring("header.".Length), kv => kv.Value)
}, serverConfig, loggerFactory);
static int ParseInt32OrDefault(Dictionary? options, string key, int defaultValue) =>
options?.TryGetValue(key, out var value) is not true ? defaultValue :
int.TryParse(value, out var result) ? result :
throw new ArgumentException($"Invalid value '{value}' for option '{key}' in transport options.", nameof(serverConfig));
}
throw new ArgumentException($"Unsupported transport type '{serverConfig.TransportType}'.", nameof(serverConfig));
}
}