Improve code coverage (#40)

* Add tests for tool generation

* Add couple of MCPServer tests

* Fix merge issues

* More tests for McpServer

* Some more tests

* Fix merge issues
This commit is contained in:
Toni Wenzel 2025-03-10 18:19:49 +01:00 • committed by GitHub
commit 24c706b4ff
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 1276 additions and 89 deletions

View file

@ -165,7 +165,7 @@ public static partial class McpServerBuilderExtensions
Type t when t == typeof(int) || t == typeof(double) || t == typeof(float) => "number",
Type t when t == typeof(bool) => "boolean",
Type t when t.IsArray => "array",
Type t when t == typeof(DateTime) => "string",
Type t when t == typeof(DateTime) || t == typeof(DateTimeOffset) => "string",
_ => "object"
};
}

View file

@ -122,6 +122,9 @@ internal static partial class Log
[LoggerMessage(Level = LogLevel.Warning, Message = "No request found for message with ID {messageWithId} for {endpointName}")]
internal static partial void NoRequestFoundForMessageWithId(this ILogger logger, string endpointName, string messageWithId);
[LoggerMessage(Level = LogLevel.Warning, Message = "The request has not valid message ID for {endpointName}")]
internal static partial void RequestHasInvalidId(this ILogger logger, string endpointName);
[LoggerMessage(Level = LogLevel.Error, Message = "Notification handler error for {endpointName} with method {method}")]
internal static partial void NotificationHandlerError(this ILogger logger, string endpointName, string method, Exception exception);

View file

@ -39,6 +39,11 @@ public readonly struct RequestId : IEquatable<RequestId>
/// </summary>
public bool IsNumber => _value is long;
/// <summary>
/// Checks if the request id is valid (has a value)
/// </summary>
public bool IsValid => _value != null;
/// <summary>
/// Gets the RequestId as a string.
/// </summary>

View file

@ -26,6 +26,7 @@ public sealed class SseClientTransport : TransportBase, IClientTransport
private readonly McpServerConfig _serverConfig;
private readonly JsonSerializerOptions _jsonOptions;
private readonly TaskCompletionSource<bool> _connectionEstablished;
private readonly bool _ownsHttpClient;
private string EndpointName => $"Client (SSE) for ({_serverConfig.Id}: {_serverConfig.Name})";
@ -37,6 +38,20 @@ public sealed class SseClientTransport : TransportBase, IClientTransport
/// <param name="serverConfig">The configuration object indicating which server to connect to.</param>
/// <param name="loggerFactory">Logger factory for creating loggers.</param>
public SseClientTransport(SseClientTransportOptions transportOptions, McpServerConfig serverConfig, ILoggerFactory? loggerFactory)
: this(transportOptions, serverConfig, new HttpClient(), loggerFactory, true)
{
}
/// <summary>
/// SSE transport for client endpoints. Unlike stdio it does not launch a process, but connects to an existing server.
/// The HTTP server can be local or remote, and must support the SSE protocol.
/// </summary>
/// <param name="transportOptions">Configuration options for the transport.</param>
/// <param name="serverConfig">The configuration object indicating which server to connect to.</param>
/// <param name="httpClient">The HTTP client instance used for requests.</param>
/// <param name="loggerFactory">Logger factory for creating loggers.</param>
/// <param name="ownsHttpClient">True to dispose HTTP client on close connection.</param>
public SseClientTransport(SseClientTransportOptions transportOptions, McpServerConfig serverConfig, HttpClient httpClient, ILoggerFactory? loggerFactory, bool ownsHttpClient = false)
: base(loggerFactory)
{
if (transportOptions is null)
@ -49,14 +64,20 @@ public sealed class SseClientTransport : TransportBase, IClientTransport
throw new ArgumentNullException(nameof(serverConfig));
}
if (httpClient is null)
{
throw new ArgumentNullException(nameof(httpClient));
}
_options = transportOptions;
_serverConfig = serverConfig;
_sseEndpoint = new Uri(serverConfig.Location!);
_httpClient = new HttpClient();
_httpClient = httpClient;
_connectionCts = new CancellationTokenSource();
_logger = loggerFactory is not null ? loggerFactory.CreateLogger<SseClientTransport>() : NullLogger.Instance;
_jsonOptions = JsonSerializerOptionsExtensions.DefaultOptions;
_connectionEstablished = new TaskCompletionSource<bool>();
_ownsHttpClient = ownsHttpClient;
}
/// <inheritdoc/>
@ -161,15 +182,26 @@ public sealed class SseClientTransport : TransportBase, IClientTransport
if (_receiveTask != null)
await _receiveTask.ConfigureAwait(false);
_httpClient.Dispose();
_connectionCts.Dispose();
SetConnected(false);
}
/// <inheritdoc/>
public override async ValueTask DisposeAsync()
{
await CloseAsync().ConfigureAwait(false);
try
{
await CloseAsync().ConfigureAwait(false);
}
catch (Exception)
{
// Ignore exceptions on close
}
if (_ownsHttpClient)
_httpClient?.Dispose();
_connectionCts?.Dispose();
GC.SuppressFinalize(this);
}

View file

@ -229,7 +229,6 @@ public sealed class StdioClientTransport : TransportBase, IClientTransport
catch (JsonException ex)
{
_logger.TransportMessageParseFailed(EndpointName, line, ex);
// Continue reading even if we fail to parse a message
}
}

View file

@ -30,7 +30,7 @@ internal class McpServer : McpJsonRpcEndpoint, IMcpServer
: base(transport, loggerFactory)
{
_serverTransport = transport;
_options = options;
_options = options ?? throw new ArgumentNullException(nameof(options));
_logger = loggerFactory.CreateLogger<McpServer>();
ServerInstructions = options.ServerInstructions;
ServiceProvider = serviceProvider;

View file

@ -33,11 +33,14 @@ internal abstract class McpJsonRpcEndpoint : IAsyncDisposable
/// <param name="loggerFactory">The logger factory.</param>
protected McpJsonRpcEndpoint(ITransport transport, ILoggerFactory loggerFactory)
{
_transport = transport;
_transport = transport ?? throw new ArgumentNullException(nameof(transport));
_pendingRequests = new();
_notificationHandlers = new();
_nextRequestId = 1;
_jsonOptions = JsonSerializerOptionsExtensions.DefaultOptions;
if (loggerFactory is null)
throw new ArgumentNullException(nameof(loggerFactory));
_logger = loggerFactory.CreateLogger<McpClient>();
}
@ -137,7 +140,9 @@ internal abstract class McpJsonRpcEndpoint : IAsyncDisposable
private void HandleMessageWithId(IJsonRpcMessage message, IJsonRpcMessageWithId messageWithId)
{
if (_pendingRequests.TryRemove(messageWithId.Id, out var tcs))
if (!messageWithId.Id.IsValid)
_logger.RequestHasInvalidId(EndpointName);
else if (_pendingRequests.TryRemove(messageWithId.Id, out var tcs))
{
_logger.ResponseMatchedPendingRequest(EndpointName, messageWithId.Id.ToString());
tcs.TrySetResult(message);