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:
parent
709b2e0a74
commit
24c706b4ff
7 changed files with 1276 additions and 89 deletions
|
|
@ -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"
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in a new issue