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.
This commit is contained in:
Stephen Toub 2025-03-16 12:08:52 -04:00 • committed by GitHub
commit 08b0a04dab
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
47 changed files with 1306 additions and 2243 deletions

View file

@ -71,9 +71,7 @@ McpServerConfig config = new()
}
};
var factory = new McpClientFactory([config], options, NullLoggerFactory.Instance);
var client = await factory.GetClientAsync("everything");
var client = await McpClientFactory.CreateAsync(config, options);
// Print the list of tools available from the server.
await foreach (var tool in client.ListToolsAsync())
@ -136,55 +134,58 @@ using McpDotNet.Protocol.Types;
using McpDotNet.Server;
using Microsoft.Extensions.Logging.Abstractions;
var loggerFactory = NullLoggerFactory.Instance;
McpServerOptions options = new()
{
ServerInfo = new() { Name = "MyServer", Version = "1.0.0" },
Capabilities = new() { Tools = new() },
};
McpServerFactory factory = new(new StdioServerTransport("MyServer", loggerFactory), options, loggerFactory);
IMcpServer server = factory.CreateServer();
server.SetListToolsHandler(async (request, cancellationToken) =>
{
return new ListToolsResult()
Capabilities = new()
{
Tools =
[
new Tool()
Tools = new()
{
ListToolsHandler = async (request, cancellationToken) =>
{
Name = "echo",
Description = "Echoes the input back to the client.",
InputSchema = new JsonSchema()
return new ListToolsResult()
{
Type = "object",
Properties = new Dictionary<string, JsonSchemaProperty>()
Tools =
[
new Tool()
{
Name = "echo",
Description = "Echoes the input back to the client.",
InputSchema = new JsonSchema()
{
Type = "object",
Properties = new Dictionary<string, JsonSchemaProperty>()
{
["message"] = new JsonSchemaProperty() { Type = "string", Description = "The input to echo back." }
}
},
}
]
};
},
CallToolHandler = async (request, cancellationToken) =>
{
if (request.Params?.Name == "echo")
{
if (request.Params.Arguments?.TryGetValue("message", out var message) is not true)
{
["message"] = new JsonSchemaProperty() { Type = "string", Description = "The input to echo back." }
throw new McpServerException("Missing required argument 'message'");
}
},
}
]
};
});
server.SetCallToolHandler(async (request, cancellationToken) =>
{
if (request.Params?.Name == "echo")
{
if (request.Params.Arguments?.TryGetValue("message", out var message) is not true)
{
throw new McpServerException("Missing required argument 'message'");
return new CallToolResponse()
{
Content = [new Content() { Text = $"Echo: {message}", Type = "text" }]
};
}
throw new McpServerException($"Unknown tool: '{request.Params?.Name}'");
},
}
},
};
return new CallToolResponse()
{
Content = [new Content() { Text = $"Echo: {message}", Type = "text" }]
};
}
throw new McpServerException($"Unknown tool: '{request.Params?.Name}'");
});
await using IMcpServer server = McpServerFactory.Create(new StdioServerTransport("MyServer"), options);
await server.StartAsync();
@ -192,7 +193,6 @@ await server.StartAsync();
await Task.Delay(Timeout.Infinite);
```
## Roadmap
- Expand documentation with detailed guides for:

View file

@ -1,24 +1,21 @@
using Anthropic.SDK;
using Anthropic.SDK.Constants;
using Anthropic.SDK.Messaging;
using System.Linq;
using McpDotNet;
using McpDotNet.Client;
using McpDotNet.Configuration;
using McpDotNet.Protocol.Transport;
using Microsoft.Extensions.Logging.Abstractions;
internal class Program
{
private static async Task<IMcpClient> GetMcpClientAsync()
{
McpClientOptions options = new()
McpClientOptions clientOptions = new()
{
ClientInfo = new() { Name = "SimpleToolsConsole", Version = "1.0.0" }
};
var config = new McpServerConfig
McpServerConfig serverConfig = new()
{
Id = "everything",
Name = "Everything",
@ -30,13 +27,7 @@ internal class Program
}
};
var factory = new McpClientFactory(
[config],
options,
NullLoggerFactory.Instance
);
return await factory.GetClientAsync("everything");
return await McpClientFactory.CreateAsync(serverConfig, clientOptions);
}
private static async Task Main(string[] args)
@ -44,7 +35,7 @@ internal class Program
try
{
Console.WriteLine("Initializing MCP 'everything' server");
var client = await GetMcpClientAsync();
await using var client = await GetMcpClientAsync();
Console.WriteLine("MCP 'everything' server initialized");
Console.WriteLine("Listing tools...");
var tools = await client.ListToolsAsync().ToListAsync();
@ -60,10 +51,10 @@ internal class Program
Console.WriteLine("Asking Claude to call the Echo Tool...");
var messages = new List<Message>
{
List<Message> messages =
[
new Message(RoleType.User, "Please call the echo tool with the string 'Hello MCP!' and show me the echoed response.")
};
];
var parameters = new MessageParameters()
{

View file

@ -3,20 +3,18 @@ using McpDotNet.Configuration;
using McpDotNet.Extensions.AI;
using McpDotNet.Protocol.Transport;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Logging.Abstractions;
using OpenAI;
internal class Program
{
private static async Task<IMcpClient> GetMcpClientAsync()
{
McpClientOptions options = new()
McpClientOptions clientOptions = new()
{
ClientInfo = new() { Name = "SimpleToolsConsole", Version = "1.0.0" }
};
var config = new McpServerConfig
McpServerConfig serverConfig = new()
{
Id = "everything",
Name = "Everything",
@ -28,13 +26,7 @@ internal class Program
}
};
var factory = new McpClientFactory(
[config],
options,
NullLoggerFactory.Instance
);
return await factory.GetClientAsync("everything");
return await McpClientFactory.CreateAsync(serverConfig, clientOptions);
}
private static async Task Main(string[] args)
@ -42,7 +34,7 @@ internal class Program
try
{
Console.WriteLine("Initializing MCP 'everything' server");
var client = await GetMcpClientAsync();
await using var client = await GetMcpClientAsync();
Console.WriteLine("MCP 'everything' server initialized");
Console.WriteLine("Listing tools...");
var mappedTools = await client.ListToolsAsync().Select(t => t.ToAITool(client)).ToListAsync();

View file

@ -0,0 +1,15 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
namespace System.Runtime.CompilerServices;
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)]
internal sealed class CallerArgumentExpressionAttribute : Attribute
{
public CallerArgumentExpressionAttribute(string parameterName)
{
ParameterName = parameterName;
}
public string ParameterName { get; }
}

View file

@ -1,5 +1,3 @@
using System.Text;
namespace System.Threading.Tasks;
internal static class CancellationTokenSourceExtensions

View file

@ -2,7 +2,6 @@
using McpDotNet.Configuration;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
namespace McpDotNet.Extensions.AI;
@ -45,7 +44,7 @@ public sealed class McpSessionScope : IAsyncDisposable
}
var scope = new McpSessionScope();
var client = await scope.AddClientAsync(serverConfig, options, loggerFactory).ConfigureAwait(false);
var client = await AddClientAsync(serverConfig, options, loggerFactory).ConfigureAwait(false);
scope.Tools = [];
await foreach (var tool in client.ListToolsAsync().ConfigureAwait(false))
@ -81,7 +80,7 @@ public sealed class McpSessionScope : IAsyncDisposable
foreach (var config in serverConfigs)
{
var client = await scope.AddClientAsync(config, options, loggerFactory).ConfigureAwait(false);
var client = await AddClientAsync(config, options, loggerFactory).ConfigureAwait(false);
scope.Tools ??= [];
await foreach (var tool in client.ListToolsAsync().ConfigureAwait(false))
@ -95,17 +94,15 @@ public sealed class McpSessionScope : IAsyncDisposable
return scope;
}
private async Task<IMcpClient> AddClientAsync(McpServerConfig config,
McpClientOptions? options,
private static Task<IMcpClient> AddClientAsync(
McpServerConfig serverConfig,
McpClientOptions? clientOptions,
ILoggerFactory? loggerFactory = null)
{
using var factory = new McpClientFactory([config],
options ?? new() { ClientInfo = new() { Name = "AnonymousClient", Version = "1.0.0.0" } },
loggerFactory ?? NullLoggerFactory.Instance);
factory.DisposeClientsOnDispose = false;
var client = await factory.GetClientAsync(config.Id).ConfigureAwait(false);
_clients.Add(client);
return client;
return McpClientFactory.CreateAsync(
serverConfig,
clientOptions ?? new() { ClientInfo = new() { Name = "AnonymousClient", Version = "1.0.0.0" } },
loggerFactory: loggerFactory);
}
/// <inheritdoc/>

View file

@ -30,20 +30,6 @@ public interface IMcpClient : IAsyncDisposable
/// </summary>
string? ServerInstructions { get; }
/// <summary>Sets a handler for the named operation.</summary>
/// <param name="operationName">The name of the operation.</param>
/// <param name="handler">The handler. Each operation requires a specific delegate signature.</param>
/// <remarks>
/// <para>
/// Each operation may have only a single handler. Setting a handler for an operation that already has one
/// will replace the existing handler.
/// </para>
/// <para>
/// <see cref="OperationNames"> provides constants for common operations.</see>
/// </para>
/// </remarks>
void SetOperationHandler(string operationName, Delegate handler);
/// <summary>
/// Adds a handler for server notifications of a specific method.
/// </summary>
@ -60,13 +46,6 @@ public interface IMcpClient : IAsyncDisposable
/// </remarks>
void AddNotificationHandler(string method, Func<JsonRpcNotification, Task> handler);
/// <summary>
/// Establishes a connection to the server.
/// </summary>
/// <param name="cancellationToken">A token to cancel the operation.</param>
/// <returns>A task representing the asynchronous operation.</returns>
Task ConnectAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Sends a generic JSON-RPC request to the server.
/// </summary>

View file

@ -6,8 +6,7 @@ using McpDotNet.Protocol.Transport;
using McpDotNet.Protocol.Types;
using McpDotNet.Shared;
using Microsoft.Extensions.Logging;
#pragma warning disable CA1508 // Avoid dead conditional code
using Microsoft.Extensions.Logging.Abstractions;
namespace McpDotNet.Client;
@ -15,15 +14,11 @@ namespace McpDotNet.Client;
internal sealed class McpClient : McpJsonRpcEndpoint, IMcpClient
{
private readonly McpClientOptions _options;
private readonly McpServerConfig _serverConfig;
private readonly ILogger<McpClient> _logger;
private readonly ILogger _logger;
private readonly IClientTransport _clientTransport;
private volatile bool _isInitializing;
private Func<CreateMessageRequestParams?, CancellationToken, Task<CreateMessageResult>>? _samplingHandler;
private Func<ListRootsRequestParams?, CancellationToken, Task<ListRootsResult>>? _rootsHandler;
/// <summary>
/// Initializes a new instance of the <see cref="McpClient"/> class.
/// </summary>
@ -31,26 +26,37 @@ internal sealed class McpClient : McpJsonRpcEndpoint, IMcpClient
/// <param name="options">Options for the client, defining protocol version and capabilities.</param>
/// <param name="serverConfig">The server configuration.</param>
/// <param name="loggerFactory">The logger factory.</param>
public McpClient(IClientTransport transport, McpClientOptions options, McpServerConfig serverConfig, ILoggerFactory loggerFactory)
public McpClient(IClientTransport transport, McpClientOptions options, McpServerConfig serverConfig, ILoggerFactory? loggerFactory)
: base(transport, loggerFactory)
{
_options = options;
_serverConfig = serverConfig;
_logger = loggerFactory.CreateLogger<McpClient>();
_logger = (ILogger?)loggerFactory?.CreateLogger<McpClient>() ?? NullLogger.Instance;
_clientTransport = transport;
EndpointName = $"Client ({serverConfig.Id}: {serverConfig.Name})";
if (options.Capabilities?.Sampling is not null)
if (options.Capabilities?.Sampling is { } samplingCapability)
{
if (samplingCapability.SamplingHandler is not { } samplingHandler)
{
throw new InvalidOperationException($"Sampling capability was set but it did not provide a handler.");
}
SetRequestHandler<CreateMessageRequestParams, CreateMessageResult>(
"sampling/createMessage", request => HandleRequest("Sampling", _samplingHandler, request));
"sampling/createMessage",
request => samplingHandler(request, CancellationTokenSource?.Token ?? default));
}
if (options.Capabilities?.Roots is not null)
if (options.Capabilities?.Roots is { } rootsCapability)
{
if (rootsCapability.RootsHandler is not { } rootsHandler)
{
throw new InvalidOperationException($"Roots capability was set but it did not provide a handler.");
}
SetRequestHandler<ListRootsRequestParams, ListRootsResult>(
"roots/list", request => HandleRequest("Roots", _rootsHandler, request));
"roots/list",
request => rootsHandler(request, CancellationTokenSource?.Token ?? default));
}
}
@ -66,47 +72,6 @@ internal sealed class McpClient : McpJsonRpcEndpoint, IMcpClient
/// <inheritdoc/>
public override string EndpointName { get; }
public void SetOperationHandler(string operationName, Delegate handler)
{
if (operationName is null)
{
throw new ArgumentNullException(nameof(operationName));
}
if (handler is null)
{
throw new ArgumentNullException(nameof(handler));
}
if (!TrySetOperationHandler(OperationNames.Sampling, operationName, handler, ref _samplingHandler) &&
!TrySetOperationHandler(OperationNames.Roots, operationName, handler, ref _rootsHandler))
{
throw new ArgumentException($"Unknown operation '{operationName}'", nameof(operationName));
}
static bool TrySetOperationHandler<TFieldRequest, TFieldResponse>(
string targetOperationName,
string operationName,
Delegate handler,
ref Func<TFieldRequest, CancellationToken, Task<TFieldResponse>>? field)
{
if (operationName == targetOperationName)
{
if (handler is Func<TFieldRequest, CancellationToken, Task<TFieldResponse>> typed)
{
field = typed;
return true;
}
throw new ArgumentException(
$"Handler must be of type {typeof(Func<TFieldRequest, CancellationToken, Task<TFieldResponse>>)}",
nameof(handler));
}
return false;
}
}
/// <inheritdoc/>
public async Task ConnectAsync(CancellationToken cancellationToken = default)
{
@ -195,19 +160,4 @@ internal sealed class McpClient : McpJsonRpcEndpoint, IMcpClient
throw new McpClientException("Initialization timed out");
}
}
private async Task<TResponse> HandleRequest<TRequest, TResponse>(
string friendlyName,
Func<TRequest, CancellationToken, Task<TResponse>>? handler,
TRequest args)
{
if (handler is not null)
{
return await handler(args, CancellationTokenSource?.Token ?? CancellationToken.None).ConfigureAwait(false);
}
// Setting the capability, but not a handler means we have nothing to return to the server
_logger.HandlerNotConfigured(friendlyName, EndpointName);
throw new McpClientException($"{friendlyName} handler not configured.");
}
}

View file

@ -1,8 +1,7 @@
using McpDotNet.Protocol.Messages;
using McpDotNet.Protocol.Types;
using Microsoft.Extensions.Logging;
using McpDotNet.Utils;
using System.Runtime.CompilerServices;
using System.Xml.XPath;
namespace McpDotNet.Client;
@ -20,10 +19,7 @@ public static class McpClientExtensions
/// <param name="cancellationToken">A token to cancel the operation.</param>
public static Task SendNotificationAsync(this IMcpClient client, string method, object? parameters = null, CancellationToken cancellationToken = default)
{
if (client is null)
{
throw new ArgumentNullException(nameof(client));
}
Throw.IfNull(client);
return client.SendMessageAsync(
new JsonRpcNotification { Method = method, Params = parameters },
@ -38,10 +34,7 @@ public static class McpClientExtensions
/// <returns>A task that completes when the ping is successful.</returns>
public static Task PingAsync(this IMcpClient client, CancellationToken cancellationToken = default)
{
if (client is null)
{
throw new ArgumentNullException(nameof(client));
}
Throw.IfNull(client);
return client.SendRequestAsync<dynamic>(
CreateRequest("ping", null),
@ -80,10 +73,7 @@ public static class McpClientExtensions
/// <returns>A task containing the server's response with tool information.</returns>
public static Task<ListToolsResult> ListToolsAsync(this IMcpClient client, string? cursor, CancellationToken cancellationToken = default)
{
if (client is null)
{
throw new ArgumentNullException(nameof(client));
}
Throw.IfNull(client);
return client.SendRequestAsync<ListToolsResult>(
CreateRequest("tools/list", CreateCursorDictionary(cursor)),
@ -122,10 +112,7 @@ public static class McpClientExtensions
/// <returns>A task containing the server's response with prompt information.</returns>
public static Task<ListPromptsResult> ListPromptsAsync(this IMcpClient client, string? cursor, CancellationToken cancellationToken = default)
{
if (client is null)
{
throw new ArgumentNullException(nameof(client));
}
Throw.IfNull(client);
return client.SendRequestAsync<ListPromptsResult>(
CreateRequest("prompts/list", CreateCursorDictionary(cursor)),
@ -142,10 +129,7 @@ public static class McpClientExtensions
/// <returns>A task containing the prompt's content and messages.</returns>
public static Task<GetPromptResult> GetPromptAsync(this IMcpClient client, string name, Dictionary<string, object>? arguments = null, CancellationToken cancellationToken = default)
{
if (client is null)
{
throw new ArgumentNullException(nameof(client));
}
Throw.IfNull(client);
return client.SendRequestAsync<GetPromptResult>(
CreateRequest("prompts/get", CreateParametersDictionary(name, arguments)),
@ -183,10 +167,7 @@ public static class McpClientExtensions
/// <param name="cancellationToken">A token to cancel the operation.</param>
public static Task<ListResourcesResult> ListResourcesAsync(this IMcpClient client, string? cursor, CancellationToken cancellationToken = default)
{
if (client is null)
{
throw new ArgumentNullException(nameof(client));
}
Throw.IfNull(client);
return client.SendRequestAsync<ListResourcesResult>(
CreateRequest("resources/list", CreateCursorDictionary(cursor)),
@ -201,10 +182,7 @@ public static class McpClientExtensions
/// <param name="cancellationToken">A token to cancel the operation.</param>
public static Task<ReadResourceResult> ReadResourceAsync(this IMcpClient client, string uri, CancellationToken cancellationToken = default)
{
if (client is null)
{
throw new ArgumentNullException(nameof(client));
}
Throw.IfNull(client);
return client.SendRequestAsync<ReadResourceResult>(
CreateRequest("resources/read", new() { ["uri"] = uri }),
@ -221,22 +199,9 @@ public static class McpClientExtensions
/// <param name="cancellationToken">A token to cancel the operation.</param>
public static Task<CompleteResult> GetCompletionAsync(this IMcpClient client, Reference reference, string argumentName, string argumentValue, CancellationToken cancellationToken = default)
{
if (client is null)
{
throw new ArgumentNullException(nameof(client));
}
if (reference is null)
{
throw new ArgumentNullException(nameof(reference));
}
if (string.IsNullOrWhiteSpace(argumentName))
{
throw argumentName is null ?
new ArgumentNullException(nameof(argumentName)) :
new ArgumentException("Argument name cannot be empty.", nameof(argumentName));
}
Throw.IfNull(client);
Throw.IfNull(reference);
Throw.IfNullOrWhiteSpace(argumentName);
if (!reference.Validate(out string? validationMessage))
{
@ -260,10 +225,7 @@ public static class McpClientExtensions
/// <param name="cancellationToken">A token to cancel the operation.</param>
public static Task SubscribeToResourceAsync(this IMcpClient client, string uri, CancellationToken cancellationToken = default)
{
if (client is null)
{
throw new ArgumentNullException(nameof(client));
}
Throw.IfNull(client);
return client.SendRequestAsync<dynamic>(
CreateRequest("resources/subscribe", new() { ["uri"] = uri }),
@ -278,10 +240,7 @@ public static class McpClientExtensions
/// <param name="cancellationToken">A token to cancel the operation.</param>
public static Task UnsubscribeFromResourceAsync(this IMcpClient client, string uri, CancellationToken cancellationToken = default)
{
if (client is null)
{
throw new ArgumentNullException(nameof(client));
}
Throw.IfNull(client);
return client.SendRequestAsync<dynamic>(
CreateRequest("resources/unsubscribe", new() { ["uri"] = uri }),
@ -298,42 +257,13 @@ public static class McpClientExtensions
/// <returns>A task containing the tool's response.</returns>
public static Task<CallToolResponse> CallToolAsync(this IMcpClient client, string toolName, Dictionary<string, object> arguments, CancellationToken cancellationToken = default)
{
if (client is null)
{
throw new ArgumentNullException(nameof(client));
}
Throw.IfNull(client);
return client.SendRequestAsync<CallToolResponse>(
CreateRequest("tools/call", CreateParametersDictionary(toolName, arguments)),
cancellationToken);
}
/// <summary>Sets the handler for server sampling requests.</summary>
/// <param name="client">The client.</param>
/// <param name="handler">The sampling request handler.</param>
public static void SetSamplingHandler(this IMcpClient client, Func<CreateMessageRequestParams?, CancellationToken, Task<CreateMessageResult>> handler)
{
if (client is null)
{
throw new ArgumentNullException(nameof(client));
}
client.SetOperationHandler(OperationNames.Sampling, handler);
}
/// <summary>Sets the handler for server roots requests.</summary>
/// <param name="client">The client.</param>
/// <param name="handler">The roots request handler.</param>
public static void SetRootsHandler(this IMcpClient client, Func<ListRootsRequestParams?, CancellationToken, Task<ListRootsResult>> handler)
{
if (client is null)
{
throw new ArgumentNullException(nameof(client));
}
client.SetOperationHandler(OperationNames.Roots, handler);
}
private static JsonRpcRequest CreateRequest(string method, Dictionary<string, object?>? parameters) =>
new JsonRpcRequest
{

View file

@ -3,236 +3,129 @@ using System.Runtime.InteropServices;
using McpDotNet.Configuration;
using McpDotNet.Logging;
using McpDotNet.Protocol.Transport;
using McpDotNet.Utils;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
namespace McpDotNet.Client;
/// <summary>
/// Factory for creating MCP clients based on configuration. It caches clients for reuse, so it is safe to call GetClientAsync multiple times.
/// Call GetClientAsync to get a client for a specific server (by ID), which will create a new client and connect if it doesn't already exist.
/// All server configurations must be passed in the constructor.
/// Capabilities (as defined in client options) are shared across all clients, as the client host can always decide not to use certain capabilities.
/// </summary>
public class McpClientFactory : IDisposable
/// <summary>Provides factory methods for creating MCP clients.</summary>
public static class McpClientFactory
{
private const string ARGUMENTS_OPTIONS_KEY = "arguments";
private const string COMMAND_OPTIONS_KEY = "command";
private readonly Dictionary<string, McpServerConfig> _serverConfigs;
private readonly McpClientOptions _clientOptions;
private readonly Dictionary<string, IMcpClient> _clients = [];
private readonly Func<McpServerConfig, IClientTransport> _transportFactoryMethod;
private readonly Func<IClientTransport, McpServerConfig, McpClientOptions, IMcpClient> _clientFactoryMethod;
private readonly ILoggerFactory _loggerFactory;
private readonly ILogger<McpClientFactory> _logger;
private bool _isDisposed;
/// <summary>
/// Gets or sets a value indicating whether clients should be disposed when the factory is disposed.
/// </summary>
public bool DisposeClientsOnDispose { get; set; } = true;
/// <summary>
/// Initializes a new instance of the <see cref="McpClientFactory"/> class.
/// It is not necessary to pass factory methods for creating transports and clients, as default implementations are provided.
/// Custom factory methods can be provided for mocking or to use custom transport or client implementations.
/// </summary>
/// <param name="serverConfigs">Configuration objects for each server the factory should support.</param>
/// <param name="clientOptions">A configuration object which specifies client capabilities and protocol version.</param>
/// <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="clientOptions">A client configuration object which specifies client capabilities and protocol version.</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="transportFactoryMethod">An optional factory method which returns transport implementations based on a server configuration.</param>
/// <param name="clientFactoryMethod">An optional factory method which creates a client based on client options and transport implementation. </param>
public McpClientFactory(
IEnumerable<McpServerConfig> serverConfigs,
McpClientOptions clientOptions,
ILoggerFactory? loggerFactory = null,
Func<McpServerConfig, IClientTransport>? transportFactoryMethod = null,
Func<IClientTransport, McpServerConfig, McpClientOptions, IMcpClient>? clientFactoryMethod = null)
{
if (serverConfigs is null)
{
throw new ArgumentNullException(nameof(serverConfigs));
}
if (clientOptions is null)
{
throw new ArgumentNullException(nameof(clientOptions));
}
loggerFactory ??= NullLoggerFactory.Instance;
_serverConfigs = serverConfigs.ToDictionary(c => c.Id);
_clientOptions = clientOptions;
_loggerFactory = loggerFactory;
_logger = loggerFactory.CreateLogger<McpClientFactory>();
_transportFactoryMethod = transportFactoryMethod ?? CreateTransport;
_clientFactoryMethod = clientFactoryMethod ?? ((transport, serverConfig, options) => new McpClient(transport, options, serverConfig, loggerFactory));
// Initialize commands for stdio transport, this is to run commands in a shell even if specified directly, as otherwise
// the stdio protocol will not work correctly.
_logger.InitializingStdioCommands();
foreach (var config in _serverConfigs.Values.Where(c => c.TransportType.Equals(TransportTypes.StdIo, StringComparison.OrdinalIgnoreCase)))
{
InitializeCommand(config);
}
}
/// <summary>
/// Gets or creates a client for the specified server. The first time a server is requested, a new client is created and connected.
/// Note that this will often spawn the server process during connection, so in some cases you want to call this method only when needed.
/// In other cases, you may want to call it ahead of time to ensure the server is ready when needed, as it may take some time to start up.
/// </summary>
/// <param name="serverId">The ID of the server to connect to. It must have been passed in the serverConfigs when constructing the factory.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
public async Task<IMcpClient> GetClientAsync(string serverId, CancellationToken cancellationToken = default)
/// <returns>An <see cref="IMcpClient"/> that's connected to the specified server.</returns>
/// <exception cref="ArgumentNullException"><paramref name="serverConfig"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentNullException"><paramref name="clientOptions"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException"><paramref name="serverConfig"/> contains invalid information.</exception>
/// <exception cref="InvalidOperationException"><paramref name="createTransportFunc"/> returns an invalid transport.</exception>
public static async Task<IMcpClient> CreateAsync(
McpServerConfig serverConfig,
McpClientOptions clientOptions,
Func<McpServerConfig, ILoggerFactory?, IClientTransport>? createTransportFunc = null,
ILoggerFactory? loggerFactory = null,
CancellationToken cancellationToken = default)
{
if (!_serverConfigs.TryGetValue(serverId, out var config))
Throw.IfNull(serverConfig);
Throw.IfNull(clientOptions);
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
{
_logger.ServerNotFound(serverId);
throw new ArgumentException($"Server with ID '{serverId}' not found.", nameof(serverId));
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;
}
}
string endpointName = $"Client ({serverId}: {config.Name})";
if (_clients.TryGetValue(serverId, out var existingClient))
catch
{
_logger.ClientExists(endpointName);
return existingClient;
await transport.DisposeAsync().ConfigureAwait(false);
throw;
}
_logger.CreatingClient(endpointName);
var transport = _transportFactoryMethod(config);
var client = _clientFactoryMethod(transport, config, _clientOptions);
await client.ConnectAsync(cancellationToken).ConfigureAwait(false);
_logger.ClientCreated(endpointName);
_clients[serverId] = client;
return client;
}
internal Func<McpServerConfig, IClientTransport> TransportFactoryMethod => _transportFactoryMethod;
private IClientTransport CreateTransport(McpServerConfig config)
private static IClientTransport CreateTransport(McpServerConfig serverConfig, ILoggerFactory? loggerFactory)
{
string endpointName = $"Client ({config.Id}: {config.Name})";
var options = string.Join(", ", config.TransportOptions?.Select(kv => $"{kv.Key}={kv.Value}") ?? []);
_logger.CreatingTransport(endpointName, config.TransportType, options);
if (string.Equals(config.TransportType, TransportTypes.StdIo, StringComparison.OrdinalIgnoreCase))
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 = GetCommand(config),
Arguments = config.TransportOptions?.GetValueOrDefault(ARGUMENTS_OPTIONS_KEY)?.Split(' '),
WorkingDirectory = config.TransportOptions?.GetValueOrDefault("workingDirectory"),
EnvironmentVariables = config.TransportOptions?
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(4), kv => kv.Value),
ShutdownTimeout = TimeSpan.TryParse(config.TransportOptions?.GetValueOrDefault("shutdownTimeout"), CultureInfo.InvariantCulture, out var timespan) ? timespan : StdioClientTransportOptions.DefaultShutdownTimeout
}, config, _loggerFactory);
.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(config.TransportType, TransportTypes.Sse, StringComparison.OrdinalIgnoreCase) ||
string.Equals(config.TransportType, "http", StringComparison.OrdinalIgnoreCase))
if (string.Equals(serverConfig.TransportType, TransportTypes.Sse, StringComparison.OrdinalIgnoreCase) ||
string.Equals(serverConfig.TransportType, "http", StringComparison.OrdinalIgnoreCase))
{
return new SseClientTransport(
new SseClientTransportOptions
{
ConnectionTimeout = TimeSpan.FromSeconds(ParseOrDefault(config.TransportOptions, "connectionTimeout", 30)),
MaxReconnectAttempts = ParseOrDefault(config.TransportOptions, "maxReconnectAttempts", 3),
ReconnectDelay = TimeSpan.FromSeconds(ParseOrDefault(config.TransportOptions, "reconnectDelay", 5)),
AdditionalHeaders = config.TransportOptions?
.Where(kv => kv.Key.StartsWith("header.", StringComparison.Ordinal))
.ToDictionary(kv => kv.Key.Substring(7), kv => kv.Value)
}, config, _loggerFactory);
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<string, string>? 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 '{config.TransportType}'.", nameof(config));
}
private static int ParseOrDefault(Dictionary<string, string>? options, string key, int defaultValue)
{
if (options?.TryGetValue(key, out var value) ?? false)
{
if (!int.TryParse(value, out var result))
throw new FormatException($"Invalid value '{value}' for option '{key}'");
return result;
}
return defaultValue;
}
private static string GetCommand(McpServerConfig config)
{
var command = config.TransportOptions?.GetValueOrDefault(COMMAND_OPTIONS_KEY);
if (string.IsNullOrEmpty(command))
return config.Location!;
return RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "cmd.exe" : command!;
}
/// <summary>
/// Initializes a non-shell command by injecting a /c {command} argument, as the command will be run in a shell.
/// </summary>
private void InitializeCommand(McpServerConfig config)
{
string endpointName = $"Client ({config.Id}: {config.Name})";
// If the command is empty or already contains cmd.exe, we don't need to do anything
var command = config.TransportOptions?.GetValueOrDefault(COMMAND_OPTIONS_KEY);
if (string.IsNullOrEmpty(command) || !RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return;
}
// On Windows, we need to wrap non-shell commands with cmd.exe /c
if (command!.IndexOf("cmd.exe", StringComparison.OrdinalIgnoreCase) >= 0)
{
_logger.SkippingShellWrapper(endpointName);
return;
}
// If the command is not empty and does not contain cmd.exe, we need to inject /c {command} (usually npx or uvicorn)
// This is because the stdio transport will not work correctly if the command is not run in a shell
_logger.PromotingCommandToShellArgumentForStdio(endpointName, command, config.TransportOptions!.GetValueOrDefault(ARGUMENTS_OPTIONS_KEY) ?? "");
config.TransportOptions![ARGUMENTS_OPTIONS_KEY] = config.TransportOptions.TryGetValue(ARGUMENTS_OPTIONS_KEY, out var args)
? $"/c {command} {args}"
: $"/c {command}";
}
/// <summary>
/// Disposes all clients created by the factory.
/// </summary>
/// <param name="disposing"></param>
protected virtual void Dispose(bool disposing)
{
if (!_isDisposed)
{
if (disposing && DisposeClientsOnDispose)
DisposeClients();
_isDisposed = true;
}
}
private void DisposeClients()
{
foreach (var client in _clients.Values)
{
client?.DisposeAsync().AsTask().Wait();
}
_clients.Clear();
}
/// <inheritdoc />
public void Dispose()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
Dispose(disposing: true);
GC.SuppressFinalize(this);
throw new ArgumentException($"Unsupported transport type '{serverConfig.TransportType}'.", nameof(serverConfig));
}
}

View file

@ -1,4 +1,5 @@
using Microsoft.Extensions.DependencyInjection;
using McpDotNet.Utils;
using Microsoft.Extensions.DependencyInjection;
namespace McpDotNet.Configuration;
@ -17,6 +18,8 @@ internal class DefaultMcpServerBuilder : IMcpServerBuilder
/// <exception cref="ArgumentNullException"></exception>
public DefaultMcpServerBuilder(IServiceCollection services)
{
Services = services ?? throw new ArgumentNullException(nameof(services));
Throw.IfNull(services);
Services = services;
}
}

View file

@ -1,6 +1,7 @@
using McpDotNet.Configuration;
using McpDotNet.Protocol.Types;
using McpDotNet.Server;
using McpDotNet.Utils;
using Microsoft.Extensions.DependencyInjection;
namespace McpDotNet;
@ -17,12 +18,9 @@ public static partial class McpServerBuilderExtensions
/// <param name="handler">The handler.</param>
public static IMcpServerBuilder WithListToolsHandler(this IMcpServerBuilder builder, Func<RequestContext<ListToolsRequestParams>, CancellationToken, Task<ListToolsResult>> handler)
{
if (builder is null)
{
throw new ArgumentNullException(nameof(builder));
}
Throw.IfNull(builder);
builder.Services.Configure<McpServerDelegates>(s => s.ListToolsHandler = handler);
builder.Services.Configure<McpServerHandlers>(s => s.ListToolsHandler = handler);
return builder;
}
@ -33,12 +31,9 @@ public static partial class McpServerBuilderExtensions
/// <param name="handler">The handler.</param>
public static IMcpServerBuilder WithCallToolHandler(this IMcpServerBuilder builder, Func<RequestContext<CallToolRequestParams>, CancellationToken, Task<CallToolResponse>> handler)
{
if (builder is null)
{
throw new ArgumentNullException(nameof(builder));
}
Throw.IfNull(builder);
builder.Services.Configure<McpServerDelegates>(s => s.CallToolHandler = handler);
builder.Services.Configure<McpServerHandlers>(s => s.CallToolHandler = handler);
return builder;
}
@ -49,12 +44,9 @@ public static partial class McpServerBuilderExtensions
/// <param name="handler">The handler.</param>
public static IMcpServerBuilder WithListPromptsHandler(this IMcpServerBuilder builder, Func<RequestContext<ListPromptsRequestParams>, CancellationToken, Task<ListPromptsResult>> handler)
{
if (builder is null)
{
throw new ArgumentNullException(nameof(builder));
}
Throw.IfNull(builder);
builder.Services.Configure<McpServerDelegates>(s => s.ListPromptsHandler = handler);
builder.Services.Configure<McpServerHandlers>(s => s.ListPromptsHandler = handler);
return builder;
}
@ -65,12 +57,9 @@ public static partial class McpServerBuilderExtensions
/// <param name="handler">The handler.</param>
public static IMcpServerBuilder WithGetPromptHandler(this IMcpServerBuilder builder, Func<RequestContext<GetPromptRequestParams>, CancellationToken, Task<GetPromptResult>> handler)
{
if (builder is null)
{
throw new ArgumentNullException(nameof(builder));
}
Throw.IfNull(builder);
builder.Services.Configure<McpServerDelegates>(s => s.GetPromptHandler = handler);
builder.Services.Configure<McpServerHandlers>(s => s.GetPromptHandler = handler);
return builder;
}
@ -81,12 +70,9 @@ public static partial class McpServerBuilderExtensions
/// <param name="handler">The handler.</param>
public static IMcpServerBuilder WithListResourcesHandler(this IMcpServerBuilder builder, Func<RequestContext<ListResourcesRequestParams>, CancellationToken, Task<ListResourcesResult>> handler)
{
if (builder is null)
{
throw new ArgumentNullException(nameof(builder));
}
Throw.IfNull(builder);
builder.Services.Configure<McpServerDelegates>(s => s.ListResourcesHandler = handler);
builder.Services.Configure<McpServerHandlers>(s => s.ListResourcesHandler = handler);
return builder;
}
@ -97,28 +83,22 @@ public static partial class McpServerBuilderExtensions
/// <param name="handler">The handler.</param>
public static IMcpServerBuilder WithReadResourceHandler(this IMcpServerBuilder builder, Func<RequestContext<ReadResourceRequestParams>, CancellationToken, Task<ReadResourceResult>> handler)
{
if (builder is null)
{
throw new ArgumentNullException(nameof(builder));
}
Throw.IfNull(builder);
builder.Services.Configure<McpServerDelegates>(s => s.ReadResourceHandler = handler);
builder.Services.Configure<McpServerHandlers>(s => s.ReadResourceHandler = handler);
return builder;
}
/// <summary>
/// Sets the handler for get resources requests.
/// Sets the handler for get completion requests.
/// </summary>
/// <param name="builder">The builder instance.</param>
/// <param name="handler">The handler.</param>
public static IMcpServerBuilder WithGetCompletionHandler(this IMcpServerBuilder builder, Func<RequestContext<CompleteRequestParams>, CancellationToken, Task<CompleteResult>> handler)
{
if (builder is null)
{
throw new ArgumentNullException(nameof(builder));
}
Throw.IfNull(builder);
builder.Services.Configure<McpServerDelegates>(s => s.GetCompletionHandler = handler);
builder.Services.Configure<McpServerHandlers>(s => s.GetCompletionHandler = handler);
return builder;
}
@ -129,12 +109,9 @@ public static partial class McpServerBuilderExtensions
/// <param name="handler">The handler.</param>
public static IMcpServerBuilder WithSubscribeToResourcesHandler(this IMcpServerBuilder builder, Func<RequestContext<string>, CancellationToken, Task> handler)
{
if (builder is null)
{
throw new ArgumentNullException(nameof(builder));
}
Throw.IfNull(builder);
builder.Services.Configure<McpServerDelegates>(s => s.SubscribeToResourcesHandler = handler);
builder.Services.Configure<McpServerHandlers>(s => s.SubscribeToResourcesHandler = handler);
return builder;
}
@ -145,12 +122,9 @@ public static partial class McpServerBuilderExtensions
/// <param name="handler">The handler.</param>
public static IMcpServerBuilder WithUnsubscribeFromResourcesHandler(this IMcpServerBuilder builder, Func<RequestContext<string>, CancellationToken, Task> handler)
{
if (builder is null)
{
throw new ArgumentNullException(nameof(builder));
}
Throw.IfNull(builder);
builder.Services.Configure<McpServerDelegates>(s => s.UnsubscribeFromResourcesHandler = handler);
builder.Services.Configure<McpServerHandlers>(s => s.UnsubscribeFromResourcesHandler = handler);
return builder;
}
}

View file

@ -6,6 +6,7 @@ using System.Text.Json;
using McpDotNet.Configuration;
using McpDotNet.Protocol.Types;
using McpDotNet.Server;
using McpDotNet.Utils;
using Microsoft.Extensions.DependencyInjection;
namespace McpDotNet;
@ -40,15 +41,14 @@ public static partial class McpServerBuilderExtensions
/// <param name="toolTypes">Types with marked methods to add as tools to the server.</param>
public static IMcpServerBuilder WithTools(this IMcpServerBuilder builder, params Type[] toolTypes)
{
if (builder is null)
{
throw new ArgumentNullException(nameof(builder));
}
Throw.IfNull(builder);
if (toolTypes is null || toolTypes.Length == 0)
{
throw new ArgumentException("At least one tool type must be provided.", nameof(toolTypes));
}
var tools = new List<Tool>();
List<Tool> tools = [];
Dictionary<string, Func<RequestContext<CallToolRequestParams>, CancellationToken, Task<CallToolResponse>>> callbacks = [];
foreach (var type in toolTypes)
@ -114,7 +114,9 @@ public static partial class McpServerBuilderExtensions
}
if (toolTypes.Count == 0)
{
throw new ArgumentException("No types with marked methods found in the assembly.", nameof(assembly));
}
return WithTools(builder, [.. toolTypes]);
}

View file

@ -1,5 +1,6 @@
using McpDotNet.Configuration;
using McpDotNet.Protocol.Transport;
using McpDotNet.Utils;
using Microsoft.Extensions.DependencyInjection;
namespace McpDotNet;
@ -15,10 +16,7 @@ public static partial class McpServerBuilderExtensions
/// <param name="builder">The builder instance.</param>
public static IMcpServerBuilder WithStdioServerTransport(this IMcpServerBuilder builder)
{
if (builder is null)
{
throw new ArgumentNullException(nameof(builder));
}
Throw.IfNull(builder);
builder.Services.AddSingleton<IServerTransport, StdioServerTransport>();
return builder;

View file

@ -1,10 +1,12 @@
using System.Reflection;
using McpDotNet.Configuration;
using McpDotNet.Configuration;
using McpDotNet.Hosting;
using McpDotNet.Protocol.Transport;
using McpDotNet.Protocol.Types;
using McpDotNet.Server;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System.Reflection;
namespace McpDotNet;
@ -36,11 +38,21 @@ public static class McpServerServiceCollectionExtension
public static IMcpServerBuilder AddMcpServer(this IServiceCollection services, McpServerOptions serverOptions)
{
services.AddSingleton(serverOptions);
services.AddSingleton<IMcpServerFactory, McpServerFactory>();
services.AddHostedService<McpServerHostedService>();
services.AddOptions();
services.AddSingleton(services =>
{
IServerTransport serverTransport = services.GetRequiredService<IServerTransport>();
McpServerOptions options = services.GetRequiredService<McpServerOptions>();
ILoggerFactory? loggerFactory = services.GetService<ILoggerFactory>();
services.AddSingleton<IMcpServer>(sp => sp.GetRequiredService<IMcpServerFactory>().CreateServer());
if (services.GetService<IOptions<McpServerHandlers>>() is { } handlersOptions)
{
options = handlersOptions.Value.OverwriteWithSetHandlers(options);
}
return McpServerFactory.Create(serverTransport, options, loggerFactory, services);
});
return new DefaultMcpServerBuilder(services);
}

View file

@ -1,4 +1,5 @@
using McpDotNet.Server;
using McpDotNet.Utils;
using Microsoft.Extensions.Hosting;
namespace McpDotNet.Hosting;
@ -17,7 +18,9 @@ public class McpServerHostedService : BackgroundService
/// <exception cref="ArgumentNullException"></exception>
public McpServerHostedService(IMcpServer server)
{
_server = server ?? throw new ArgumentNullException(nameof(server));
Throw.IfNull(server);
_server = server;
}
/// <inheritdoc />

View file

@ -8,60 +8,15 @@ namespace McpDotNet.Logging;
/// </summary>
internal static partial class Log
{
[LoggerMessage(Level = LogLevel.Debug, Message = "Client {clientId} initializing connection to server {serverId}")]
internal static partial void ClientConnecting(this ILogger logger, string clientId, string serverId);
[LoggerMessage(Level = LogLevel.Information, Message = "Server {endpointName} capabilities received: {capabilities}, server info: {serverInfo}")]
internal static partial void ServerCapabilitiesReceived(this ILogger logger, string endpointName, string capabilities, string serverInfo);
[LoggerMessage(Level = LogLevel.Warning, Message = "Request {requestId} timed out")]
internal static partial void RequestTimeout(this ILogger logger, int requestId);
[LoggerMessage(Level = LogLevel.Debug, Message = "Command for {endpointName} already contains shell wrapper, skipping argument injection")]
internal static partial void SkippingShellWrapper(this ILogger logger, string endpointName);
[LoggerMessage(Level = LogLevel.Error, Message = "Server connection config for Id={serverId} not found")]
internal static partial void ServerNotFound(this ILogger logger, string serverId);
[LoggerMessage(Level = LogLevel.Debug, Message = "Client for {endpointName} already created, returning cached client")]
internal static partial void ClientExists(this ILogger logger, string endpointName);
[LoggerMessage(Level = LogLevel.Information, Message = "Creating client for {endpointName}")]
internal static partial void CreatingClient(this ILogger logger, string endpointName);
[LoggerMessage(Level = LogLevel.Information, Message = "Client for {endpointName} created and connected")]
internal static partial void ClientCreated(this ILogger logger, string endpointName);
[LoggerMessage(Level = LogLevel.Information, Message = "Creating transport for {endpointName} with type {transportType} and options {options}")]
internal static partial void CreatingTransport(this ILogger logger, string endpointName, string transportType, string options);
[LoggerMessage(Level = LogLevel.Debug, Message = "Promoting command for {endpointName} to shell argument for stdio transport with command {command} and arguments {arguments}")]
internal static partial void PromotingCommandToShellArgumentForStdio(this ILogger logger, string endpointName, string command, string arguments);
[LoggerMessage(Level = LogLevel.Debug, Message = "Initializing stdio commands")]
internal static partial void InitializingStdioCommands(this ILogger logger);
[LoggerMessage(Level = LogLevel.Error, Message = "{handlerName} handler not configured for server {endpointName}, always set a handler when using this capability")]
internal static partial void HandlerNotConfigured(this ILogger logger, string handlerName, string endpointName);
[LoggerMessage(Level = LogLevel.Error, Message = "List tools handler not configured for server {endpointName}, always set a handler when using this capability")]
internal static partial void ListToolsHandlerNotConfigured(this ILogger logger, string endpointName);
[LoggerMessage(Level = LogLevel.Error, Message = "Call tool handler not configured for server {endpointName}, always set a handler when using this capability")]
internal static partial void CallToolHandlerNotConfigured(this ILogger logger, string endpointName);
[LoggerMessage(Level = LogLevel.Error, Message = "List prompts handler not configured for server {endpointName}, always set a handler when using this capability")]
internal static partial void ListPromptsHandlerNotConfigured(this ILogger logger, string endpointName);
[LoggerMessage(Level = LogLevel.Error, Message = "Get prompt handler not configured for server {endpointName}, always set a handler when using this capability")]
internal static partial void GetPromptHandlerNotConfigured(this ILogger logger, string endpointName);
[LoggerMessage(Level = LogLevel.Error, Message = "List resources handler not configured for server {endpointName}, always set a handler when using this capability")]
internal static partial void ListResourcesHandlerNotConfigured(this ILogger logger, string endpointName);
[LoggerMessage(Level = LogLevel.Error, Message = "Read resource handler not configured for server {endpointName}, always set a handler when using this capability")]
internal static partial void ReadResourceHandlerNotConfigured(this ILogger logger, string endpointName);
[LoggerMessage(Level = LogLevel.Error, Message = "Client server {endpointName} already initializing")]
internal static partial void ClientAlreadyInitializing(this ILogger logger, string endpointName);
@ -77,33 +32,6 @@ internal static partial class Log
[LoggerMessage(Level = LogLevel.Error, Message = "Client {endpointName} initialization timeout")]
internal static partial void ClientInitializationTimeout(this ILogger logger, string endpointName);
[LoggerMessage(Level = LogLevel.Information, Message = "Pinging sent ({endpointName})")]
internal static partial void PingingServer(this ILogger logger, string endpointName);
[LoggerMessage(Level = LogLevel.Information, Message = "Listing tools for {endpointName} with cursor {cursor}")]
internal static partial void ListingTools(this ILogger logger, string endpointName, string cursor);
[LoggerMessage(Level = LogLevel.Information, Message = "Listing prompts for {endpointName} with cursor {cursor}")]
internal static partial void ListingPrompts(this ILogger logger, string endpointName, string cursor);
[LoggerMessage(Level = LogLevel.Information, Message = "Getting prompt {name} for {endpointName} with arguments {arguments}")]
internal static partial void GettingPrompt(this ILogger logger, string endpointName, string name, string arguments);
[LoggerMessage(Level = LogLevel.Information, Message = "Listing resources for {endpointName} with cursor {cursor}")]
internal static partial void ListingResources(this ILogger logger, string endpointName, string cursor);
[LoggerMessage(Level = LogLevel.Information, Message = "Reading resource {uri} for {endpointName}")]
internal static partial void ReadingResource(this ILogger logger, string endpointName, string uri);
[LoggerMessage(Level = LogLevel.Information, Message = "Subscribing to resource {uri} for {endpointName}")]
internal static partial void SubscribingToResource(this ILogger logger, string endpointName, string uri);
[LoggerMessage(Level = LogLevel.Information, Message = "Unsubscribing from resource {uri} for {endpointName}")]
internal static partial void UnsubscribingFromResource(this ILogger logger, string endpointName, string uri);
[LoggerMessage(Level = LogLevel.Information, Message = "Calling tool {toolName} for {endpointName} with arguments {arguments}")]
internal static partial void CallingTool(this ILogger logger, string endpointName, string toolName, string arguments);
[LoggerMessage(Level = LogLevel.Information, Message = "Endpoint message processing cancelled for {endpointName}")]
internal static partial void EndpointMessageProcessingCancelled(this ILogger logger, string endpointName);
@ -152,9 +80,6 @@ internal static partial class Log
[LoggerMessage(Level = LogLevel.Error, Message = "Request invalid response type for {endpointName} with method {method}")]
internal static partial void RequestInvalidResponseType(this ILogger logger, string endpointName, string method);
[LoggerMessage(Level = LogLevel.Error, Message = "Request params type conversion error for {endpointName} with method {method}: expected {expectedType}")]
internal static partial void RequestParamsTypeConversionError(this ILogger logger, string endpointName, string method, Type expectedType);
[LoggerMessage(Level = LogLevel.Information, Message = "Cleaning up endpoint {endpointName}")]
internal static partial void CleaningUpEndpoint(this ILogger logger, string endpointName);
@ -168,7 +93,7 @@ internal static partial class Log
internal static partial void TransportConnecting(this ILogger logger, string endpointName);
[LoggerMessage(Level = LogLevel.Information, Message = "Creating process for transport for {endpointName} with command {command}, arguments {arguments}, environment {environment}, working directory {workingDirectory}, shutdown timeout {shutdownTimeout}")]
internal static partial void CreateProcessForTransport(this ILogger logger, string endpointName, string command, string arguments, string environment, string workingDirectory, string shutdownTimeout);
internal static partial void CreateProcessForTransport(this ILogger logger, string endpointName, string command, string? arguments, string environment, string workingDirectory, string shutdownTimeout);
[LoggerMessage(Level = LogLevel.Error, Message = "Transport for {endpointName} error: {data}")]
internal static partial void TransportError(this ILogger logger, string endpointName, string data);
@ -239,9 +164,6 @@ internal static partial class Log
[LoggerMessage(Level = LogLevel.Debug, Message = "Transport waiting for shutdown for {endpointName}")]
internal static partial void TransportWaitingForShutdown(this ILogger logger, string endpointName);
[LoggerMessage(Level = LogLevel.Warning, Message = "Transport killing process for {endpointName}")]
internal static partial void TransportKillingProcess(this ILogger logger, string endpointName);
[LoggerMessage(Level = LogLevel.Error, Message = "Transport shutdown failed for {endpointName}")]
internal static partial void TransportShutdownFailed(this ILogger logger, string endpointName, Exception exception);
@ -266,9 +188,6 @@ internal static partial class Log
[LoggerMessage(Level = LogLevel.Debug, Message = "Sending message to {endpointName}: {message}")]
internal static partial void SendingMessage(this ILogger logger, string endpointName, string message);
[LoggerMessage(Level = LogLevel.Information, Message = "Sending notification for {endpointName}: {method}")]
internal static partial void SendingNotification(this ILogger logger, string endpointName, string method);
[LoggerMessage(
EventId = 7000,
Level = LogLevel.Error,
@ -310,50 +229,6 @@ internal static partial class Log
string data,
Exception exception);
[LoggerMessage(
EventId = 7004,
Level = LogLevel.Trace,
Message = "Invalid completion reference {reference} for {endpointName}: {validationMessage}"
)]
public static partial void InvalidCompletionReference(
this ILogger logger,
string endpointName,
string reference,
string validationMessage);
[LoggerMessage(
EventId = 7005,
Level = LogLevel.Trace,
Message = "Invalid completion argument name {argumentName} for {endpointName}"
)]
public static partial void InvalidCompletionArgumentName(
this ILogger logger,
string endpointName,
string argumentName);
[LoggerMessage(
EventId = 7006,
Level = LogLevel.Trace,
Message = "Invalid completion argument value {argumentValue} for {argumentName} for {endpointName}"
)]
public static partial void InvalidCompletionArgumentValue(
this ILogger logger,
string endpointName,
string argumentValue,
string argumentName);
[LoggerMessage(
EventId = 7007,
Level = LogLevel.Debug,
Message = "Getting completion for {endpointName} with reference {reference}, argument name {argumentName}, argument value {argumentValue}"
)]
public static partial void GettingCompletion(
this ILogger logger,
string endpointName,
string reference,
string argumentName,
string argumentValue);
[LoggerMessage(
EventId = 7008,
Level = LogLevel.Error,

View file

@ -1,8 +1,4 @@
using McpDotNet.Protocol.Messages;
using McpDotNet.Protocol.Types;
using Microsoft.Extensions.Logging;
namespace McpDotNet.Protocol.Messages;
namespace McpDotNet.Protocol.Messages;
/// <summary>Provides names of standard operations for use with registering handlers.</summary>
/// <remarks>

View file

@ -1,4 +1,5 @@
using System.Text.Json;
using McpDotNet.Utils;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace McpDotNet.Protocol.Messages;
@ -22,14 +23,15 @@ public class RequestIdConverter : JsonConverter<RequestId>
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, RequestId value, JsonSerializerOptions options)
{
if (writer is null)
{
throw new ArgumentNullException(nameof(writer));
}
Throw.IfNull(writer);
if (value.IsString)
{
writer.WriteStringValue(value.AsString);
}
else
{
writer.WriteNumberValue(value.AsNumber);
}
}
}

View file

@ -1,102 +0,0 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// Copied from:
// https://github.com/dotnet/runtime/blob/d2650b6ae7023a2d9d2c74c56116f1f18472ab04/src/libraries/System.Private.CoreLib/src/System/PasteArguments.cs
// and changed from using ValueStringBuilder to StringBuilder.
using System.Text;
namespace McpDotNet.Protocol.Transport
{
internal static partial class PasteArguments
{
internal static void AppendArgument(StringBuilder stringBuilder, string argument)
{
if (stringBuilder.Length != 0)
{
stringBuilder.Append(' ');
}
// Parsing rules for non-argv[0] arguments:
// - Backslash is a normal character except followed by a quote.
// - 2N backslashes followed by a quote ==> N literal backslashes followed by unescaped quote
// - 2N+1 backslashes followed by a quote ==> N literal backslashes followed by a literal quote
// - Parsing stops at first whitespace outside of quoted region.
// - (post 2008 rule): A closing quote followed by another quote ==> literal quote, and parsing remains in quoting mode.
if (argument.Length != 0 && ContainsNoWhitespaceOrQuotes(argument))
{
// Simple case - no quoting or changes needed.
stringBuilder.Append(argument);
}
else
{
stringBuilder.Append(Quote);
int idx = 0;
while (idx < argument.Length)
{
char c = argument[idx++];
if (c == Backslash)
{
int numBackSlash = 1;
while (idx < argument.Length && argument[idx] == Backslash)
{
idx++;
numBackSlash++;
}
if (idx == argument.Length)
{
// We'll emit an end quote after this so must double the number of backslashes.
stringBuilder.Append(Backslash, numBackSlash * 2);
}
else if (argument[idx] == Quote)
{
// Backslashes will be followed by a quote. Must double the number of backslashes.
stringBuilder.Append(Backslash, numBackSlash * 2 + 1);
stringBuilder.Append(Quote);
idx++;
}
else
{
// Backslash will not be followed by a quote, so emit as normal characters.
stringBuilder.Append(Backslash, numBackSlash);
}
continue;
}
if (c == Quote)
{
// Escape the quote so it appears as a literal. This also guarantees that we won't end up generating a closing quote followed
// by another quote (which parses differently pre-2008 vs. post-2008.)
stringBuilder.Append(Backslash);
stringBuilder.Append(Quote);
continue;
}
stringBuilder.Append(c);
}
stringBuilder.Append(Quote);
}
}
private static bool ContainsNoWhitespaceOrQuotes(string s)
{
for (int i = 0; i < s.Length; i++)
{
char c = s[i];
if (char.IsWhiteSpace(c) || c == Quote)
{
return false;
}
}
return true;
}
private const char Quote = '\"';
private const char Backslash = '\\';
}
}

Some files were not shown because too many files have changed in this diff Show more