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)); } }