Enable .NET SDK code analyzers (#41)
This commit is contained in:
parent
9c597aad90
commit
587e05f057
37 changed files with 301 additions and 151 deletions
|
|
@ -34,6 +34,11 @@ public class McpAIFunction : AIFunction
|
|||
/// <inheritdoc/>
|
||||
protected async override Task<object?> InvokeCoreAsync(IEnumerable<KeyValuePair<string, object?>> arguments, CancellationToken cancellationToken)
|
||||
{
|
||||
if (arguments is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(arguments));
|
||||
}
|
||||
|
||||
// Convert arguments to dictionary format expected by mcpdotnet
|
||||
Dictionary<string, object> argDict = [];
|
||||
foreach (var arg in arguments)
|
||||
|
|
@ -45,7 +50,7 @@ public class McpAIFunction : AIFunction
|
|||
}
|
||||
|
||||
// Call the tool through mcpdotnet
|
||||
var result = await _client.CallToolAsync(_tool.Name, argDict, cancellationToken);
|
||||
var result = await _client.CallToolAsync(_tool.Name, argDict, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Extract the text content from the result.
|
||||
return string.Join("\n", result.Content
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@
|
|||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>Latest</LangVersion>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<AnalysisMode>All</AnalysisMode>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ namespace McpDotNet.Extensions.AI;
|
|||
///
|
||||
/// Disposing the scope will dispose all clients and tools, and close all connections.
|
||||
/// </summary>
|
||||
public class McpSessionScope : IAsyncDisposable
|
||||
public sealed class McpSessionScope : IAsyncDisposable
|
||||
{
|
||||
private readonly List<IMcpClient> _clients = [];
|
||||
private bool _disposed;
|
||||
|
|
@ -39,9 +39,14 @@ public class McpSessionScope : IAsyncDisposable
|
|||
McpClientOptions? options = null,
|
||||
ILoggerFactory? loggerFactory = null)
|
||||
{
|
||||
if (serverConfig is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(serverConfig));
|
||||
}
|
||||
|
||||
var scope = new McpSessionScope();
|
||||
var client = await scope.AddClientAsync(serverConfig, options, loggerFactory);
|
||||
var tools = await client.ListToolsAsync();
|
||||
var client = await scope.AddClientAsync(serverConfig, options, loggerFactory).ConfigureAwait(false);
|
||||
var tools = await client.ListToolsAsync().ConfigureAwait(false);
|
||||
scope.Tools = tools.Tools.Select(t => new McpAIFunction(t, client)).ToList<AITool>();
|
||||
if (!string.IsNullOrEmpty(client.ServerInstructions))
|
||||
{
|
||||
|
|
@ -61,11 +66,16 @@ public class McpSessionScope : IAsyncDisposable
|
|||
McpClientOptions? options = null,
|
||||
ILoggerFactory? loggerFactory = null)
|
||||
{
|
||||
if (serverConfigs is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(serverConfigs));
|
||||
}
|
||||
|
||||
var scope = new McpSessionScope();
|
||||
foreach (var config in serverConfigs)
|
||||
{
|
||||
var client = await scope.AddClientAsync(config, options, loggerFactory);
|
||||
var tools = await client.ListToolsAsync();
|
||||
var client = await scope.AddClientAsync(config, options, loggerFactory).ConfigureAwait(false);
|
||||
var tools = await client.ListToolsAsync().ConfigureAwait(false);
|
||||
scope.Tools = scope.Tools.Concat(tools.Tools.Select(t => new McpAIFunction(t, client))).ToList();
|
||||
}
|
||||
scope.ServerInstructions = scope._clients.Select(c => c.ServerInstructions).Where(s => !string.IsNullOrEmpty(s)).ToList()!;
|
||||
|
|
@ -79,7 +89,7 @@ public class McpSessionScope : IAsyncDisposable
|
|||
var factory = new McpClientFactory([config],
|
||||
options ?? new() { ClientInfo = new() { Name = "AnonymousClient", Version = "1.0.0.0" } },
|
||||
loggerFactory ?? NullLoggerFactory.Instance);
|
||||
var client = await factory.GetClientAsync(config.Id);
|
||||
var client = await factory.GetClientAsync(config.Id).ConfigureAwait(false);
|
||||
_clients.Add(client);
|
||||
return client;
|
||||
}
|
||||
|
|
@ -92,7 +102,7 @@ public class McpSessionScope : IAsyncDisposable
|
|||
|
||||
foreach (var client in _clients)
|
||||
{
|
||||
await client.DisposeAsync();
|
||||
await client.DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
_clients.Clear();
|
||||
Tools = [];
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
namespace McpDotNet.Client;
|
||||
|
||||
using McpDotNet.Protocol.Messages;
|
||||
using McpDotNet.Protocol.Messages;
|
||||
using McpDotNet.Protocol.Types;
|
||||
|
||||
namespace McpDotNet.Client;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an instance of the MCP client connecting to a specific server.
|
||||
/// Use PingAsync, ListToolsAsync and CallToolAsync instead of SendRequestAsync for common operations.
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ internal class McpClient : McpJsonRpcEndpoint, IMcpClient
|
|||
throw new McpClientException("Sampling handler not configured");
|
||||
}
|
||||
|
||||
return await SamplingHandler(request, CancellationTokenSource?.Token ?? CancellationToken.None);
|
||||
return await SamplingHandler(request, CancellationTokenSource?.Token ?? CancellationToken.None).ConfigureAwait(false);
|
||||
});
|
||||
}
|
||||
if (options.Capabilities?.Roots != null)
|
||||
|
|
@ -69,7 +69,7 @@ internal class McpClient : McpJsonRpcEndpoint, IMcpClient
|
|||
_logger.RootsHandlerNotConfigured(EndpointName);
|
||||
throw new McpClientException("Roots handler not configured");
|
||||
}
|
||||
return await RootsHandler(request, CancellationTokenSource?.Token ?? CancellationToken.None);
|
||||
return await RootsHandler(request, CancellationTokenSource?.Token ?? CancellationToken.None).ConfigureAwait(false);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,13 @@ public class McpClientException : Exception
|
|||
/// </summary>
|
||||
public int? ErrorCode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="McpClientException"/> class.
|
||||
/// </summary>
|
||||
public McpClientException()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="McpClientException"/> class with a specified error message.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ public class McpClientFactory
|
|||
|
||||
var transport = _transportFactoryMethod(config);
|
||||
var client = _clientFactoryMethod(transport, config, _clientOptions);
|
||||
await client.ConnectAsync(cancellationToken);
|
||||
await client.ConnectAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
_logger.ClientCreated(endpointName);
|
||||
|
||||
|
|
@ -97,29 +97,36 @@ public class McpClientFactory
|
|||
|
||||
var options = string.Join(", ", config.TransportOptions?.Select(kv => $"{kv.Key}={kv.Value}") ?? []);
|
||||
_logger.CreatingTransport(endpointName, config.TransportType, options);
|
||||
return config.TransportType.ToLowerInvariant() switch
|
||||
|
||||
if (string.Equals(config.TransportType, TransportTypes.StdIo, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
TransportTypes.StdIo => new StdioClientTransport(new StdioClientTransportOptions
|
||||
return new StdioClientTransport(new StdioClientTransportOptions
|
||||
{
|
||||
Command = GetCommand(config),
|
||||
Arguments = config.TransportOptions?.GetValueOrDefault("arguments")?.Split(' '),
|
||||
WorkingDirectory = config.TransportOptions?.GetValueOrDefault("workingDirectory"),
|
||||
EnvironmentVariables = config.TransportOptions?
|
||||
.Where(kv => kv.Key.StartsWith("env:"))
|
||||
.Where(kv => kv.Key.StartsWith("env:", StringComparison.Ordinal))
|
||||
.ToDictionary(kv => kv.Key.Substring(4), kv => kv.Value)
|
||||
}, config, _loggerFactory),
|
||||
TransportTypes.Sse or "http" => new SseClientTransport(
|
||||
}, config, _loggerFactory);
|
||||
}
|
||||
|
||||
if (string.Equals(config.TransportType, TransportTypes.Sse, StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(config.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."))
|
||||
.Where(kv => kv.Key.StartsWith("header.", StringComparison.Ordinal))
|
||||
.ToDictionary(kv => kv.Key.Substring(7), kv => kv.Value)
|
||||
}, config, _loggerFactory),
|
||||
_ => throw new ArgumentException($"Unsupported transport type '{config.TransportType}'.", nameof(config))
|
||||
};
|
||||
}, config, _loggerFactory);
|
||||
}
|
||||
|
||||
throw new ArgumentException($"Unsupported transport type '{config.TransportType}'.", nameof(config));
|
||||
}
|
||||
|
||||
private static int ParseOrDefault(Dictionary<string, string>? options, string key, int defaultValue)
|
||||
|
|
|
|||
|
|
@ -17,6 +17,11 @@ 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));
|
||||
}
|
||||
|
||||
builder.Services.Configure<McpServerDelegates>(s => s.ListToolsHandler = handler);
|
||||
return builder;
|
||||
}
|
||||
|
|
@ -28,6 +33,11 @@ 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));
|
||||
}
|
||||
|
||||
builder.Services.Configure<McpServerDelegates>(s => s.CallToolHandler = handler);
|
||||
return builder;
|
||||
}
|
||||
|
|
@ -39,6 +49,11 @@ 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));
|
||||
}
|
||||
|
||||
builder.Services.Configure<McpServerDelegates>(s => s.ListPromptsHandler = handler);
|
||||
return builder;
|
||||
}
|
||||
|
|
@ -50,6 +65,11 @@ 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));
|
||||
}
|
||||
|
||||
builder.Services.Configure<McpServerDelegates>(s => s.GetPromptHandler = handler);
|
||||
return builder;
|
||||
}
|
||||
|
|
@ -61,6 +81,11 @@ 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));
|
||||
}
|
||||
|
||||
builder.Services.Configure<McpServerDelegates>(s => s.ListResourcesHandler = handler);
|
||||
return builder;
|
||||
}
|
||||
|
|
@ -72,6 +97,11 @@ 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));
|
||||
}
|
||||
|
||||
builder.Services.Configure<McpServerDelegates>(s => s.ReadResourceHandler = handler);
|
||||
return builder;
|
||||
}
|
||||
|
|
@ -83,6 +113,11 @@ public static partial class McpServerBuilderExtensions
|
|||
/// <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));
|
||||
}
|
||||
|
||||
builder.Services.Configure<McpServerDelegates>(s => s.GetCompletionHandler = handler);
|
||||
return builder;
|
||||
}
|
||||
|
|
@ -94,6 +129,11 @@ 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));
|
||||
}
|
||||
|
||||
builder.Services.Configure<McpServerDelegates>(s => s.SubscribeToResourcesHandler = handler);
|
||||
return builder;
|
||||
}
|
||||
|
|
@ -105,6 +145,11 @@ 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));
|
||||
}
|
||||
|
||||
builder.Services.Configure<McpServerDelegates>(s => s.UnsubscribeFromResourcesHandler = handler);
|
||||
return builder;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System.Reflection;
|
||||
using System.Globalization;
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using McpDotNet.Configuration;
|
||||
using McpDotNet.Protocol.Types;
|
||||
|
|
@ -37,6 +38,11 @@ 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));
|
||||
}
|
||||
|
||||
if (toolTypes is null || toolTypes.Length == 0)
|
||||
throw new ArgumentException("At least one tool type must be provided.", nameof(toolTypes));
|
||||
|
||||
|
|
@ -54,7 +60,7 @@ public static partial class McpServerBuilderExtensions
|
|||
var tool = CreateTool(method, attribute);
|
||||
tools.Add(tool);
|
||||
|
||||
callbacks.Add(tool.Name, async (request, cancellationToken) => await CallTool(request, method, cancellationToken));
|
||||
callbacks.Add(tool.Name, async (request, cancellationToken) => await CallTool(request, method, cancellationToken).ConfigureAwait(false));
|
||||
|
||||
// register type because method is not static and so we need an instance
|
||||
if (!method.IsStatic)
|
||||
|
|
@ -68,7 +74,7 @@ public static partial class McpServerBuilderExtensions
|
|||
builder.WithCallToolHandler(async (request, cancellationToken) =>
|
||||
{
|
||||
if (request.Params != null && callbacks.TryGetValue(request.Params.Name, out var callback))
|
||||
return await callback(request, cancellationToken);
|
||||
return await callback(request, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
throw new McpServerException($"Unknown tool: {request.Params?.Name}");
|
||||
});
|
||||
|
|
@ -219,7 +225,7 @@ public static partial class McpServerBuilderExtensions
|
|||
if (value is JsonElement element)
|
||||
value = JsonSerializer.Deserialize(element.GetRawText(), parameter.ParameterType);
|
||||
|
||||
parameters.Add(Convert.ChangeType(value, parameter.ParameterType));
|
||||
parameters.Add(Convert.ChangeType(value, parameter.ParameterType, CultureInfo.InvariantCulture));
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
|
|||
|
|
@ -15,6 +15,11 @@ 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));
|
||||
}
|
||||
|
||||
builder.Services.AddSingleton<IServerTransport, StdioServerTransport>();
|
||||
return builder;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,6 @@ public class McpServerHostedService : BackgroundService
|
|||
/// <inheritdoc />
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
await _server.StartAsync(stoppingToken);
|
||||
await _server.StartAsync(stoppingToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
namespace McpDotNet.Protocol.Messages;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
namespace McpDotNet.Protocol.Messages;
|
||||
|
||||
/// <summary>
|
||||
/// An error response message in the JSON-RPC protocol.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
namespace McpDotNet.Protocol.Messages;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
namespace McpDotNet.Protocol.Messages;
|
||||
|
||||
/// <summary>
|
||||
/// Detailed error information for JSON-RPC error responses.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
namespace McpDotNet.Protocol.Messages;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
namespace McpDotNet.Protocol.Messages;
|
||||
|
||||
/// <summary>
|
||||
/// A request message in the JSON-RPC protocol.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
namespace McpDotNet.Protocol.Messages;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
namespace McpDotNet.Protocol.Messages;
|
||||
|
||||
/// <summary>
|
||||
/// A notification message in the JSON-RPC protocol (a request that doesn't expect a response).
|
||||
|
|
|
|||
|
|
@ -23,5 +23,5 @@ public record JsonRpcResponse : IJsonRpcMessageWithId
|
|||
/// The result of the method invocation.
|
||||
/// </summary>
|
||||
[JsonPropertyName("result")]
|
||||
public required object Result { get; init; }
|
||||
public required object? Result { get; init; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
namespace McpDotNet.Protocol.Messages;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
namespace McpDotNet.Protocol.Messages;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a JSON-RPC request identifier which can be either a string or a number.
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
namespace McpDotNet.Protocol.Messages;
|
||||
|
||||
using System.Text.Json;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace McpDotNet.Protocol.Messages;
|
||||
|
||||
/// <summary>
|
||||
/// JSON converter for RequestId that handles both string and number values.
|
||||
/// </summary>
|
||||
|
|
@ -11,20 +11,22 @@ public class RequestIdConverter : JsonConverter<RequestId>
|
|||
/// <inheritdoc />
|
||||
public override RequestId Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
switch (reader.TokenType)
|
||||
return reader.TokenType switch
|
||||
{
|
||||
case JsonTokenType.String:
|
||||
return RequestId.FromString(reader.GetString()!);
|
||||
case JsonTokenType.Number:
|
||||
return RequestId.FromNumber(reader.GetInt64());
|
||||
default:
|
||||
throw new JsonException("RequestId must be either a string or a number");
|
||||
}
|
||||
JsonTokenType.String => RequestId.FromString(reader.GetString()!),
|
||||
JsonTokenType.Number => RequestId.FromNumber(reader.GetInt64()),
|
||||
_ => throw new JsonException("RequestId must be either a string or a number"),
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Write(Utf8JsonWriter writer, RequestId value, JsonSerializerOptions options)
|
||||
{
|
||||
if (writer is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(writer));
|
||||
}
|
||||
|
||||
if (value.IsString)
|
||||
writer.WriteStringValue(value.AsString);
|
||||
else
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ using McpDotNet.Logging;
|
|||
using McpDotNet.Protocol.Messages;
|
||||
using McpDotNet.Utils.Json;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
||||
namespace McpDotNet.Protocol.Transport;
|
||||
|
||||
|
|
@ -21,7 +22,7 @@ public sealed class SseClientTransport : TransportBase, IClientTransport
|
|||
private Uri? _messageEndpoint;
|
||||
private readonly CancellationTokenSource _connectionCts;
|
||||
private Task? _receiveTask;
|
||||
private readonly ILogger<SseClientTransport> _logger;
|
||||
private readonly ILogger _logger;
|
||||
private readonly McpServerConfig _serverConfig;
|
||||
private readonly JsonSerializerOptions _jsonOptions;
|
||||
private readonly TaskCompletionSource<bool> _connectionEstablished;
|
||||
|
|
@ -35,15 +36,25 @@ public sealed class SseClientTransport : TransportBase, IClientTransport
|
|||
/// <param name="transportOptions">Configuration options for the transport.</param>
|
||||
/// <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)
|
||||
public SseClientTransport(SseClientTransportOptions transportOptions, McpServerConfig serverConfig, ILoggerFactory? loggerFactory)
|
||||
: base(loggerFactory)
|
||||
{
|
||||
if (transportOptions is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(transportOptions));
|
||||
}
|
||||
|
||||
if (serverConfig is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(serverConfig));
|
||||
}
|
||||
|
||||
_options = transportOptions;
|
||||
_serverConfig = serverConfig;
|
||||
_sseEndpoint = new Uri(serverConfig.Location!);
|
||||
_httpClient = new HttpClient();
|
||||
_connectionCts = new CancellationTokenSource();
|
||||
_logger = loggerFactory.CreateLogger<SseClientTransport>();
|
||||
_logger = loggerFactory is not null ? loggerFactory.CreateLogger<SseClientTransport>() : NullLogger.Instance;
|
||||
_jsonOptions = JsonSerializerOptionsExtensions.DefaultOptions;
|
||||
_connectionEstablished = new TaskCompletionSource<bool>();
|
||||
}
|
||||
|
|
@ -64,7 +75,7 @@ public sealed class SseClientTransport : TransportBase, IClientTransport
|
|||
|
||||
_logger.TransportReadingMessages(EndpointName);
|
||||
|
||||
await _connectionEstablished.Task.WaitAsync(_options.ConnectionTimeout, cancellationToken);
|
||||
await _connectionEstablished.Task.WaitAsync(_options.ConnectionTimeout, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (McpTransportException)
|
||||
{
|
||||
|
|
@ -74,7 +85,7 @@ public sealed class SseClientTransport : TransportBase, IClientTransport
|
|||
catch (Exception ex)
|
||||
{
|
||||
_logger.TransportConnectFailed(EndpointName, ex);
|
||||
await CloseAsync();
|
||||
await CloseAsync().ConfigureAwait(false);
|
||||
throw new McpTransportException("Failed to connect transport", ex);
|
||||
}
|
||||
}
|
||||
|
|
@ -104,11 +115,11 @@ public sealed class SseClientTransport : TransportBase, IClientTransport
|
|||
_messageEndpoint,
|
||||
content,
|
||||
cancellationToken
|
||||
);
|
||||
).ConfigureAwait(false);
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var responseContent = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||
var responseContent = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Check if the message was an initialize request
|
||||
if (message is JsonRpcRequest request && request.Method == "initialize")
|
||||
|
|
@ -125,7 +136,7 @@ public sealed class SseClientTransport : TransportBase, IClientTransport
|
|||
throw new McpTransportException("Failed to initialize client");
|
||||
|
||||
_logger.TransportReceivedMessageParsed(EndpointName, messageId);
|
||||
await WriteMessageAsync(initializeResponse, cancellationToken);
|
||||
await WriteMessageAsync(initializeResponse, cancellationToken).ConfigureAwait(false);
|
||||
_logger.TransportMessageWritten(EndpointName, messageId);
|
||||
}
|
||||
return;
|
||||
|
|
@ -146,9 +157,9 @@ public sealed class SseClientTransport : TransportBase, IClientTransport
|
|||
/// <inheritdoc/>
|
||||
public async Task CloseAsync()
|
||||
{
|
||||
await _connectionCts.CancelAsync();
|
||||
await _connectionCts.CancelAsync().ConfigureAwait(false);
|
||||
if (_receiveTask != null)
|
||||
await _receiveTask;
|
||||
await _receiveTask.ConfigureAwait(false);
|
||||
|
||||
_httpClient.Dispose();
|
||||
_connectionCts.Dispose();
|
||||
|
|
@ -158,7 +169,7 @@ public sealed class SseClientTransport : TransportBase, IClientTransport
|
|||
/// <inheritdoc/>
|
||||
public override async ValueTask DisposeAsync()
|
||||
{
|
||||
await CloseAsync();
|
||||
await CloseAsync().ConfigureAwait(false);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
|
|
@ -181,16 +192,16 @@ public sealed class SseClientTransport : TransportBase, IClientTransport
|
|||
request,
|
||||
HttpCompletionOption.ResponseHeadersRead,
|
||||
cancellationToken
|
||||
);
|
||||
).ConfigureAwait(false);
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
using var stream = await response.Content.ReadAsStreamAsync(cancellationToken);
|
||||
using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Reset reconnect attempts on successful connection
|
||||
reconnectAttempts = 0;
|
||||
|
||||
await foreach (SseItem<string> sseEvent in SseParser.Create(stream).EnumerateAsync(cancellationToken))
|
||||
await foreach (SseItem<string> sseEvent in SseParser.Create(stream).EnumerateAsync(cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
switch (sseEvent.EventType)
|
||||
{
|
||||
|
|
@ -199,7 +210,7 @@ public sealed class SseClientTransport : TransportBase, IClientTransport
|
|||
break;
|
||||
|
||||
case "message":
|
||||
await ProcessSseMessage(sseEvent.Data, cancellationToken);
|
||||
await ProcessSseMessage(sseEvent.Data, cancellationToken).ConfigureAwait(false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
@ -224,7 +235,7 @@ public sealed class SseClientTransport : TransportBase, IClientTransport
|
|||
throw new McpTransportException("Exceeded reconnect limit", ex);
|
||||
}
|
||||
|
||||
await Task.Delay(_options.ReconnectDelay, cancellationToken);
|
||||
await Task.Delay(_options.ReconnectDelay, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -253,7 +264,7 @@ public sealed class SseClientTransport : TransportBase, IClientTransport
|
|||
}
|
||||
|
||||
_logger.TransportReceivedMessageParsed(EndpointName, messageId);
|
||||
await WriteMessageAsync(message, cancellationToken);
|
||||
await WriteMessageAsync(message, cancellationToken).ConfigureAwait(false);
|
||||
_logger.TransportMessageWritten(EndpointName, messageId);
|
||||
}
|
||||
catch (JsonException ex)
|
||||
|
|
@ -282,7 +293,7 @@ public sealed class SseClientTransport : TransportBase, IClientTransport
|
|||
{
|
||||
// If the endpoint is a relative URI, we need to combine it with the relative path of the SSE endpoint
|
||||
var hostUrl = _sseEndpoint.AbsoluteUri;
|
||||
if (hostUrl.EndsWith("/sse"))
|
||||
if (hostUrl.EndsWith("/sse", StringComparison.Ordinal))
|
||||
hostUrl = hostUrl[..^4];
|
||||
|
||||
var endpointUri = hostUrl + data;
|
||||
|
|
|
|||
|
|
@ -108,7 +108,7 @@ public sealed class StdioClientTransport : TransportBase, IClientTransport
|
|||
_process.BeginErrorReadLine();
|
||||
|
||||
// Start reading messages in the background
|
||||
_readTask = Task.Run(async () => await ReadMessagesAsync(_shutdownCts.Token), CancellationToken.None);
|
||||
_readTask = Task.Run(async () => await ReadMessagesAsync(_shutdownCts.Token).ConfigureAwait(false), CancellationToken.None);
|
||||
_logger.TransportReadingMessages(EndpointName);
|
||||
|
||||
SetConnected(true);
|
||||
|
|
@ -116,7 +116,7 @@ public sealed class StdioClientTransport : TransportBase, IClientTransport
|
|||
catch (Exception ex)
|
||||
{
|
||||
_logger.TransportConnectFailed(EndpointName, ex);
|
||||
await CleanupAsync(cancellationToken);
|
||||
await CleanupAsync(cancellationToken).ConfigureAwait(false);
|
||||
throw new McpTransportException("Failed to connect transport", ex);
|
||||
}
|
||||
}
|
||||
|
|
@ -142,8 +142,8 @@ public sealed class StdioClientTransport : TransportBase, IClientTransport
|
|||
_logger.TransportSendingMessage(EndpointName, id, json);
|
||||
|
||||
// Write the message followed by a newline
|
||||
await _process!.StandardInput.WriteLineAsync(json.AsMemory(), cancellationToken);
|
||||
await _process.StandardInput.FlushAsync(cancellationToken);
|
||||
await _process!.StandardInput.WriteLineAsync(json.AsMemory(), cancellationToken).ConfigureAwait(false);
|
||||
await _process.StandardInput.FlushAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
_logger.TransportSentMessage(EndpointName, id);
|
||||
}
|
||||
|
|
@ -157,7 +157,7 @@ public sealed class StdioClientTransport : TransportBase, IClientTransport
|
|||
/// <inheritdoc/>
|
||||
public override async ValueTask DisposeAsync()
|
||||
{
|
||||
await CleanupAsync(CancellationToken.None);
|
||||
await CleanupAsync(CancellationToken.None).ConfigureAwait(false);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
|
|
@ -172,7 +172,7 @@ public sealed class StdioClientTransport : TransportBase, IClientTransport
|
|||
while (!cancellationToken.IsCancellationRequested && !_process.HasExited)
|
||||
{
|
||||
_logger.TransportWaitingForMessage(EndpointName);
|
||||
var line = await reader.ReadLineAsync(cancellationToken);
|
||||
var line = await reader.ReadLineAsync(cancellationToken).ConfigureAwait(false);
|
||||
if (line == null)
|
||||
{
|
||||
_logger.TransportEndOfStream(EndpointName);
|
||||
|
|
@ -197,7 +197,7 @@ public sealed class StdioClientTransport : TransportBase, IClientTransport
|
|||
messageId = messageWithId.Id.ToString();
|
||||
}
|
||||
_logger.TransportReceivedMessageParsed(EndpointName, messageId);
|
||||
await WriteMessageAsync(message, cancellationToken);
|
||||
await WriteMessageAsync(message, cancellationToken).ConfigureAwait(false);
|
||||
_logger.TransportMessageWritten(EndpointName, messageId);
|
||||
}
|
||||
else
|
||||
|
|
@ -224,7 +224,7 @@ public sealed class StdioClientTransport : TransportBase, IClientTransport
|
|||
}
|
||||
finally
|
||||
{
|
||||
await CleanupAsync(cancellationToken);
|
||||
await CleanupAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -258,7 +258,7 @@ public sealed class StdioClientTransport : TransportBase, IClientTransport
|
|||
}
|
||||
|
||||
if (_shutdownCts != null)
|
||||
await _shutdownCts.CancelAsync();
|
||||
await _shutdownCts.CancelAsync().ConfigureAwait(false);
|
||||
_shutdownCts?.Dispose();
|
||||
_shutdownCts = null;
|
||||
|
||||
|
|
@ -267,7 +267,7 @@ public sealed class StdioClientTransport : TransportBase, IClientTransport
|
|||
try
|
||||
{
|
||||
_logger.TransportWaitingForReadTask(EndpointName);
|
||||
await _readTask.WaitAsync(TimeSpan.FromSeconds(5), cancellationToken);
|
||||
await _readTask.WaitAsync(TimeSpan.FromSeconds(5), cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (TimeoutException)
|
||||
{
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue