diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..c260088 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,18 @@ +# Remove the line below if you want to inherit .editorconfig settings from higher directories +root = true + +# C# files +[*.cs] + +# Code Analysis +dotnet_diagnostic.CA1002.severity = none # CA1002: Do not expose generic lists +dotnet_diagnostic.CA1031.severity = none # CA1031: Do not catch general exception types +dotnet_diagnostic.CA1054.severity = none # CA1054: URI-like parameters should not be strings +dotnet_diagnostic.CA1056.severity = none # CA1056: URI-like properties should not be strings +dotnet_diagnostic.CA1062.severity = suggestion # CA1062: Validate arguments of public methods +dotnet_diagnostic.CA1510.severity = suggestion # CA1510: Use ArgumentNullException throw helper +dotnet_diagnostic.CA1849.severity = suggestion # CA1849: Call async methods when in an async method +dotnet_diagnostic.CA2000.severity = suggestion # CA2000: Dispose objects before losing scope +dotnet_diagnostic.CA2227.severity = suggestion # CA2227: Collection properties should be read only +dotnet_diagnostic.CA2249.severity = suggestion # CA2249: Use 'string.Contains' instead of 'string.IndexOf' +dotnet_diagnostic.CA1819.severity = suggestion # CA1819: Properties should not return arrays \ No newline at end of file diff --git a/samples/TestServerWithHosting/Tools/EchoTool.cs b/samples/TestServerWithHosting/Tools/EchoTool.cs index 4b3ba0d..c215746 100644 --- a/samples/TestServerWithHosting/Tools/EchoTool.cs +++ b/samples/TestServerWithHosting/Tools/EchoTool.cs @@ -5,7 +5,7 @@ namespace TestServerWithHosting.Tools; [McpToolType] public static class EchoTool { - [McpTool(Description = "Echoes the input back to the client.")] + [McpTool(description: "Echoes the input back to the client.")] public static string Echo([McpParameter(true)] string message) { return "hello " + message; diff --git a/samples/TestServerWithHosting/Tools/SampleLlmTool.cs b/samples/TestServerWithHosting/Tools/SampleLlmTool.cs index 2ed764e..3bd2fed 100644 --- a/samples/TestServerWithHosting/Tools/SampleLlmTool.cs +++ b/samples/TestServerWithHosting/Tools/SampleLlmTool.cs @@ -16,8 +16,11 @@ public class SampleLlmTool _server = server ?? throw new ArgumentNullException(nameof(server)); } - [McpTool(Name = "sampleLLM", Description = "Samples from an LLM using MCP's sampling feature")] - public async Task SampleLLM([McpParameter(true, Description = "The prompt to send to the LLM")] string prompt, [McpParameter(true, Description = "Maximum number of tokens to generate")] int maxTokens, CancellationToken cancellationToken) + [McpTool(name: "sampleLLM", description: "Samples from an LLM using MCP's sampling feature")] + public async Task SampleLLM( + [McpParameter(true, description: "The prompt to send to the LLM")] string prompt, + [McpParameter(true, description: "Maximum number of tokens to generate")] int maxTokens, + CancellationToken cancellationToken) { var samplingParams = CreateRequestSamplingParams(prompt ?? string.Empty, "sampleLLM", maxTokens); var sampleResult = await _server.RequestSamplingAsync(samplingParams, cancellationToken); diff --git a/samples/anthropic/tools/ToolsConsole/McpToolExtensions.cs b/samples/anthropic/tools/ToolsConsole/McpToolExtensions.cs index 45c0cf9..e299682 100644 --- a/samples/anthropic/tools/ToolsConsole/McpToolExtensions.cs +++ b/samples/anthropic/tools/ToolsConsole/McpToolExtensions.cs @@ -2,10 +2,17 @@ using System.Text.Json; using System.Text.Json.Nodes; +namespace McpDotNet; + public static class McpToolExtensions { public static IList ToAnthropicTools(this IEnumerable tools) { + if (tools is null) + { + throw new ArgumentNullException(nameof(tools)); + } + List result = []; foreach (var tool in tools) { diff --git a/samples/anthropic/tools/ToolsConsole/Program.cs b/samples/anthropic/tools/ToolsConsole/Program.cs index a889988..8801ffd 100644 --- a/samples/anthropic/tools/ToolsConsole/Program.cs +++ b/samples/anthropic/tools/ToolsConsole/Program.cs @@ -1,6 +1,7 @@ using Anthropic.SDK; using Anthropic.SDK.Constants; using Anthropic.SDK.Messaging; +using McpDotNet; using McpDotNet.Client; using McpDotNet.Configuration; using McpDotNet.Protocol.Transport; diff --git a/samples/microsoft.extensions.ai/tools/ToolsConsole/McpAIFunction.cs b/samples/microsoft.extensions.ai/tools/ToolsConsole/McpAIFunction.cs index 6719701..4cd2e23 100644 --- a/samples/microsoft.extensions.ai/tools/ToolsConsole/McpAIFunction.cs +++ b/samples/microsoft.extensions.ai/tools/ToolsConsole/McpAIFunction.cs @@ -29,6 +29,11 @@ public class McpAIFunction : AIFunction protected async override Task InvokeCoreAsync(IEnumerable> arguments, CancellationToken cancellationToken) { + if (arguments is null) + { + throw new ArgumentNullException(nameof(arguments)); + } + // Convert arguments to dictionary format expected by mcpdotnet Dictionary argDict = []; foreach (var arg in arguments) diff --git a/src/McpDotNet.Extensions.AI/McpAIFunction.cs b/src/McpDotNet.Extensions.AI/McpAIFunction.cs index 4829052..d1e292d 100644 --- a/src/McpDotNet.Extensions.AI/McpAIFunction.cs +++ b/src/McpDotNet.Extensions.AI/McpAIFunction.cs @@ -34,6 +34,11 @@ public class McpAIFunction : AIFunction /// protected async override Task InvokeCoreAsync(IEnumerable> arguments, CancellationToken cancellationToken) { + if (arguments is null) + { + throw new ArgumentNullException(nameof(arguments)); + } + // Convert arguments to dictionary format expected by mcpdotnet Dictionary 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 diff --git a/src/McpDotNet.Extensions.AI/McpDotNet.Extensions.AI.csproj b/src/McpDotNet.Extensions.AI/McpDotNet.Extensions.AI.csproj index b7134ee..16b545f 100644 --- a/src/McpDotNet.Extensions.AI/McpDotNet.Extensions.AI.csproj +++ b/src/McpDotNet.Extensions.AI/McpDotNet.Extensions.AI.csproj @@ -5,6 +5,8 @@ enable enable Latest + true + All diff --git a/src/McpDotNet.Extensions.AI/McpSessionScope.cs b/src/McpDotNet.Extensions.AI/McpSessionScope.cs index cf5bd5e..ca21cc3 100644 --- a/src/McpDotNet.Extensions.AI/McpSessionScope.cs +++ b/src/McpDotNet.Extensions.AI/McpSessionScope.cs @@ -12,7 +12,7 @@ namespace McpDotNet.Extensions.AI; /// /// Disposing the scope will dispose all clients and tools, and close all connections. /// -public class McpSessionScope : IAsyncDisposable +public sealed class McpSessionScope : IAsyncDisposable { private readonly List _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(); 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 = []; diff --git a/src/mcpdotnet/Client/IMcpClient.cs b/src/mcpdotnet/Client/IMcpClient.cs index e6e11ce..8a42a8f 100644 --- a/src/mcpdotnet/Client/IMcpClient.cs +++ b/src/mcpdotnet/Client/IMcpClient.cs @@ -1,8 +1,8 @@ -namespace McpDotNet.Client; - -using McpDotNet.Protocol.Messages; +using McpDotNet.Protocol.Messages; using McpDotNet.Protocol.Types; +namespace McpDotNet.Client; + /// /// Represents an instance of the MCP client connecting to a specific server. /// Use PingAsync, ListToolsAsync and CallToolAsync instead of SendRequestAsync for common operations. diff --git a/src/mcpdotnet/Client/McpClient.cs b/src/mcpdotnet/Client/McpClient.cs index c02451a..acf6036 100644 --- a/src/mcpdotnet/Client/McpClient.cs +++ b/src/mcpdotnet/Client/McpClient.cs @@ -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); }); } } diff --git a/src/mcpdotnet/Client/McpClientException.cs b/src/mcpdotnet/Client/McpClientException.cs index f6692d5..5a39dfb 100644 --- a/src/mcpdotnet/Client/McpClientException.cs +++ b/src/mcpdotnet/Client/McpClientException.cs @@ -10,6 +10,13 @@ public class McpClientException : Exception /// public int? ErrorCode { get; } + /// + /// Initializes a new instance of the class. + /// + public McpClientException() + { + } + /// /// Initializes a new instance of the class with a specified error message. /// diff --git a/src/mcpdotnet/Client/McpClientFactory.cs b/src/mcpdotnet/Client/McpClientFactory.cs index a1d5472..99699b3 100644 --- a/src/mcpdotnet/Client/McpClientFactory.cs +++ b/src/mcpdotnet/Client/McpClientFactory.cs @@ -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? options, string key, int defaultValue) diff --git a/src/mcpdotnet/Configuration/McpServerBuilderExtensions.Handler.cs b/src/mcpdotnet/Configuration/McpServerBuilderExtensions.Handler.cs index 22bdd8f..fe0b955 100644 --- a/src/mcpdotnet/Configuration/McpServerBuilderExtensions.Handler.cs +++ b/src/mcpdotnet/Configuration/McpServerBuilderExtensions.Handler.cs @@ -17,6 +17,11 @@ public static partial class McpServerBuilderExtensions /// The handler. public static IMcpServerBuilder WithListToolsHandler(this IMcpServerBuilder builder, Func, CancellationToken, Task> handler) { + if (builder is null) + { + throw new ArgumentNullException(nameof(builder)); + } + builder.Services.Configure(s => s.ListToolsHandler = handler); return builder; } @@ -28,6 +33,11 @@ public static partial class McpServerBuilderExtensions /// The handler. public static IMcpServerBuilder WithCallToolHandler(this IMcpServerBuilder builder, Func, CancellationToken, Task> handler) { + if (builder is null) + { + throw new ArgumentNullException(nameof(builder)); + } + builder.Services.Configure(s => s.CallToolHandler = handler); return builder; } @@ -39,6 +49,11 @@ public static partial class McpServerBuilderExtensions /// The handler. public static IMcpServerBuilder WithListPromptsHandler(this IMcpServerBuilder builder, Func, CancellationToken, Task> handler) { + if (builder is null) + { + throw new ArgumentNullException(nameof(builder)); + } + builder.Services.Configure(s => s.ListPromptsHandler = handler); return builder; } @@ -50,6 +65,11 @@ public static partial class McpServerBuilderExtensions /// The handler. public static IMcpServerBuilder WithGetPromptHandler(this IMcpServerBuilder builder, Func, CancellationToken, Task> handler) { + if (builder is null) + { + throw new ArgumentNullException(nameof(builder)); + } + builder.Services.Configure(s => s.GetPromptHandler = handler); return builder; } @@ -61,6 +81,11 @@ public static partial class McpServerBuilderExtensions /// The handler. public static IMcpServerBuilder WithListResourcesHandler(this IMcpServerBuilder builder, Func, CancellationToken, Task> handler) { + if (builder is null) + { + throw new ArgumentNullException(nameof(builder)); + } + builder.Services.Configure(s => s.ListResourcesHandler = handler); return builder; } @@ -72,6 +97,11 @@ public static partial class McpServerBuilderExtensions /// The handler. public static IMcpServerBuilder WithReadResourceHandler(this IMcpServerBuilder builder, Func, CancellationToken, Task> handler) { + if (builder is null) + { + throw new ArgumentNullException(nameof(builder)); + } + builder.Services.Configure(s => s.ReadResourceHandler = handler); return builder; } @@ -83,6 +113,11 @@ public static partial class McpServerBuilderExtensions /// The handler. public static IMcpServerBuilder WithGetCompletionHandler(this IMcpServerBuilder builder, Func, CancellationToken, Task> handler) { + if (builder is null) + { + throw new ArgumentNullException(nameof(builder)); + } + builder.Services.Configure(s => s.GetCompletionHandler = handler); return builder; } @@ -94,6 +129,11 @@ public static partial class McpServerBuilderExtensions /// The handler. public static IMcpServerBuilder WithSubscribeToResourcesHandler(this IMcpServerBuilder builder, Func, CancellationToken, Task> handler) { + if (builder is null) + { + throw new ArgumentNullException(nameof(builder)); + } + builder.Services.Configure(s => s.SubscribeToResourcesHandler = handler); return builder; } @@ -105,6 +145,11 @@ public static partial class McpServerBuilderExtensions /// The handler. public static IMcpServerBuilder WithUnsubscribeFromResourcesHandler(this IMcpServerBuilder builder, Func, CancellationToken, Task> handler) { + if (builder is null) + { + throw new ArgumentNullException(nameof(builder)); + } + builder.Services.Configure(s => s.UnsubscribeFromResourcesHandler = handler); return builder; } diff --git a/src/mcpdotnet/Configuration/McpServerBuilderExtensions.Tools.cs b/src/mcpdotnet/Configuration/McpServerBuilderExtensions.Tools.cs index 43b3368..a272b5a 100644 --- a/src/mcpdotnet/Configuration/McpServerBuilderExtensions.Tools.cs +++ b/src/mcpdotnet/Configuration/McpServerBuilderExtensions.Tools.cs @@ -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 /// Types with marked methods to add as tools to the server. 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 { diff --git a/src/mcpdotnet/Configuration/McpServerBuilderExtensions.Transports.cs b/src/mcpdotnet/Configuration/McpServerBuilderExtensions.Transports.cs index f64cc14..6a1a165 100644 --- a/src/mcpdotnet/Configuration/McpServerBuilderExtensions.Transports.cs +++ b/src/mcpdotnet/Configuration/McpServerBuilderExtensions.Transports.cs @@ -15,6 +15,11 @@ public static partial class McpServerBuilderExtensions /// The builder instance. public static IMcpServerBuilder WithStdioServerTransport(this IMcpServerBuilder builder) { + if (builder is null) + { + throw new ArgumentNullException(nameof(builder)); + } + builder.Services.AddSingleton(); return builder; } diff --git a/src/mcpdotnet/Hosting/McpServerHostedService.cs b/src/mcpdotnet/Hosting/McpServerHostedService.cs index d9b0b0f..11d4006 100644 --- a/src/mcpdotnet/Hosting/McpServerHostedService.cs +++ b/src/mcpdotnet/Hosting/McpServerHostedService.cs @@ -23,6 +23,6 @@ public class McpServerHostedService : BackgroundService /// protected override async Task ExecuteAsync(CancellationToken stoppingToken) { - await _server.StartAsync(stoppingToken); + await _server.StartAsync(stoppingToken).ConfigureAwait(false); } } diff --git a/src/mcpdotnet/Protocol/Messages/JsonRpcError.cs b/src/mcpdotnet/Protocol/Messages/JsonRpcError.cs index 6b1cf6b..d7ee6fd 100644 --- a/src/mcpdotnet/Protocol/Messages/JsonRpcError.cs +++ b/src/mcpdotnet/Protocol/Messages/JsonRpcError.cs @@ -1,6 +1,6 @@ -namespace McpDotNet.Protocol.Messages; +using System.Text.Json.Serialization; -using System.Text.Json.Serialization; +namespace McpDotNet.Protocol.Messages; /// /// An error response message in the JSON-RPC protocol. diff --git a/src/mcpdotnet/Protocol/Messages/JsonRpcErrorDetail.cs b/src/mcpdotnet/Protocol/Messages/JsonRpcErrorDetail.cs index 572797d..5b8a7cb 100644 --- a/src/mcpdotnet/Protocol/Messages/JsonRpcErrorDetail.cs +++ b/src/mcpdotnet/Protocol/Messages/JsonRpcErrorDetail.cs @@ -1,6 +1,6 @@ -namespace McpDotNet.Protocol.Messages; +using System.Text.Json.Serialization; -using System.Text.Json.Serialization; +namespace McpDotNet.Protocol.Messages; /// /// Detailed error information for JSON-RPC error responses. diff --git a/src/mcpdotnet/Protocol/Messages/JsonRpcMessage.cs b/src/mcpdotnet/Protocol/Messages/JsonRpcMessage.cs index 4290c9b..2edb3a0 100644 --- a/src/mcpdotnet/Protocol/Messages/JsonRpcMessage.cs +++ b/src/mcpdotnet/Protocol/Messages/JsonRpcMessage.cs @@ -1,6 +1,6 @@ -namespace McpDotNet.Protocol.Messages; +using System.Text.Json.Serialization; -using System.Text.Json.Serialization; +namespace McpDotNet.Protocol.Messages; /// /// A request message in the JSON-RPC protocol. diff --git a/src/mcpdotnet/Protocol/Messages/JsonRpcNotification.cs b/src/mcpdotnet/Protocol/Messages/JsonRpcNotification.cs index 33b889c..1f271c5 100644 --- a/src/mcpdotnet/Protocol/Messages/JsonRpcNotification.cs +++ b/src/mcpdotnet/Protocol/Messages/JsonRpcNotification.cs @@ -1,6 +1,6 @@ -namespace McpDotNet.Protocol.Messages; +using System.Text.Json.Serialization; -using System.Text.Json.Serialization; +namespace McpDotNet.Protocol.Messages; /// /// A notification message in the JSON-RPC protocol (a request that doesn't expect a response). diff --git a/src/mcpdotnet/Protocol/Messages/JsonRpcResponse.cs b/src/mcpdotnet/Protocol/Messages/JsonRpcResponse.cs index 42f7d95..67b0516 100644 --- a/src/mcpdotnet/Protocol/Messages/JsonRpcResponse.cs +++ b/src/mcpdotnet/Protocol/Messages/JsonRpcResponse.cs @@ -23,5 +23,5 @@ public record JsonRpcResponse : IJsonRpcMessageWithId /// The result of the method invocation. /// [JsonPropertyName("result")] - public required object Result { get; init; } + public required object? Result { get; init; } } diff --git a/src/mcpdotnet/Protocol/Messages/RequestId.cs b/src/mcpdotnet/Protocol/Messages/RequestId.cs index df3d142..f627185 100644 --- a/src/mcpdotnet/Protocol/Messages/RequestId.cs +++ b/src/mcpdotnet/Protocol/Messages/RequestId.cs @@ -1,6 +1,6 @@ -namespace McpDotNet.Protocol.Messages; +using System.Text.Json.Serialization; -using System.Text.Json.Serialization; +namespace McpDotNet.Protocol.Messages; /// /// Represents a JSON-RPC request identifier which can be either a string or a number. diff --git a/src/mcpdotnet/Protocol/Messages/RequestIdConverter.cs b/src/mcpdotnet/Protocol/Messages/RequestIdConverter.cs index 88c0408..4599e95 100644 --- a/src/mcpdotnet/Protocol/Messages/RequestIdConverter.cs +++ b/src/mcpdotnet/Protocol/Messages/RequestIdConverter.cs @@ -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; + /// /// JSON converter for RequestId that handles both string and number values. /// @@ -11,20 +11,22 @@ public class RequestIdConverter : JsonConverter /// 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"), + }; } /// 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 diff --git a/src/mcpdotnet/Protocol/Transport/SseClientTransport.cs b/src/mcpdotnet/Protocol/Transport/SseClientTransport.cs index 5541e64..bb1f49c 100644 --- a/src/mcpdotnet/Protocol/Transport/SseClientTransport.cs +++ b/src/mcpdotnet/Protocol/Transport/SseClientTransport.cs @@ -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 _logger; + private readonly ILogger _logger; private readonly McpServerConfig _serverConfig; private readonly JsonSerializerOptions _jsonOptions; private readonly TaskCompletionSource _connectionEstablished; @@ -35,15 +36,25 @@ public sealed class SseClientTransport : TransportBase, IClientTransport /// Configuration options for the transport. /// The configuration object indicating which server to connect to. /// Logger factory for creating loggers. - 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(); + _logger = loggerFactory is not null ? loggerFactory.CreateLogger() : NullLogger.Instance; _jsonOptions = JsonSerializerOptionsExtensions.DefaultOptions; _connectionEstablished = new TaskCompletionSource(); } @@ -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 /// 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 /// 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 sseEvent in SseParser.Create(stream).EnumerateAsync(cancellationToken)) + await foreach (SseItem 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; diff --git a/src/mcpdotnet/Protocol/Transport/StdioClientTransport.cs b/src/mcpdotnet/Protocol/Transport/StdioClientTransport.cs index c05f190..d70d16a 100644 --- a/src/mcpdotnet/Protocol/Transport/StdioClientTransport.cs +++ b/src/mcpdotnet/Protocol/Transport/StdioClientTransport.cs @@ -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 /// 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) { diff --git a/src/mcpdotnet/Protocol/Transport/StdioServerTransport.cs b/src/mcpdotnet/Protocol/Transport/StdioServerTransport.cs index 9be51e7..ac91819 100644 --- a/src/mcpdotnet/Protocol/Transport/StdioServerTransport.cs +++ b/src/mcpdotnet/Protocol/Transport/StdioServerTransport.cs @@ -4,6 +4,7 @@ using McpDotNet.Protocol.Messages; using McpDotNet.Server; using McpDotNet.Utils.Json; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; namespace McpDotNet.Protocol.Transport; @@ -13,7 +14,7 @@ namespace McpDotNet.Protocol.Transport; public sealed class StdioServerTransport : TransportBase, IServerTransport { private readonly string _serverName; - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly JsonSerializerOptions _jsonOptions; private Task? _readTask; private CancellationTokenSource? _shutdownCts; @@ -25,8 +26,8 @@ public sealed class StdioServerTransport : TransportBase, IServerTransport /// /// The server options. /// A logger factory for creating loggers. - public StdioServerTransport(McpServerOptions serverOptions, ILoggerFactory loggerFactory) - : this(serverOptions.ServerInfo.Name, loggerFactory) + public StdioServerTransport(McpServerOptions serverOptions, ILoggerFactory? loggerFactory) + : this(serverOptions is not null ? serverOptions.ServerInfo.Name : throw new ArgumentNullException(nameof(serverOptions)), loggerFactory) { } @@ -35,11 +36,11 @@ public sealed class StdioServerTransport : TransportBase, IServerTransport /// /// The name of the server. /// A logger factory for creating loggers. - public StdioServerTransport(string serverName, ILoggerFactory loggerFactory) + public StdioServerTransport(string serverName, ILoggerFactory? loggerFactory) : base(loggerFactory) { _serverName = serverName; - _logger = loggerFactory.CreateLogger(); + _logger = loggerFactory is not null ? loggerFactory.CreateLogger() : NullLogger.Instance; _jsonOptions = JsonSerializerOptionsExtensions.DefaultOptions; } @@ -48,7 +49,7 @@ public sealed class StdioServerTransport : TransportBase, IServerTransport { _shutdownCts = new CancellationTokenSource(); - _readTask = Task.Run(async () => await ReadMessagesAsync(_shutdownCts.Token), CancellationToken.None); + _readTask = Task.Run(async () => await ReadMessagesAsync(_shutdownCts.Token).ConfigureAwait(false), CancellationToken.None); SetConnected(true); @@ -76,11 +77,8 @@ public sealed class StdioServerTransport : TransportBase, IServerTransport var json = JsonSerializer.Serialize(message, _jsonOptions); _logger.TransportSendingMessage(EndpointName, id, json); - using (Console.OpenStandardOutput()) - { - await Console.Out.WriteLineAsync(json.AsMemory(), cancellationToken); - await Console.Out.FlushAsync(cancellationToken); - } + await Console.Out.WriteLineAsync(json.AsMemory(), cancellationToken).ConfigureAwait(false); + await Console.Out.FlushAsync(cancellationToken).ConfigureAwait(false); _logger.TransportSentMessage(EndpointName, id); } @@ -94,7 +92,7 @@ public sealed class StdioServerTransport : TransportBase, IServerTransport /// public override async ValueTask DisposeAsync() { - await CleanupAsync(CancellationToken.None); + await CleanupAsync(CancellationToken.None).ConfigureAwait(false); GC.SuppressFinalize(this); } @@ -110,7 +108,7 @@ public sealed class StdioServerTransport : TransportBase, IServerTransport using (Console.OpenStandardInput()) { var reader = Console.In; - var line = await reader.ReadLineAsync(cancellationToken); + var line = await reader.ReadLineAsync(cancellationToken).ConfigureAwait(false); if (line == null) { _logger.TransportEndOfStream(EndpointName); @@ -135,7 +133,7 @@ public sealed class StdioServerTransport : TransportBase, IServerTransport messageId = messageWithId.Id.ToString(); } _logger.TransportReceivedMessageParsed(EndpointName, messageId); - await WriteMessageAsync(message, cancellationToken); + await WriteMessageAsync(message, cancellationToken).ConfigureAwait(false); _logger.TransportMessageWritten(EndpointName, messageId); } else @@ -163,7 +161,7 @@ public sealed class StdioServerTransport : TransportBase, IServerTransport } finally { - await CleanupAsync(cancellationToken); + await CleanupAsync(cancellationToken).ConfigureAwait(false); } } @@ -180,7 +178,7 @@ public sealed class StdioServerTransport : TransportBase, IServerTransport try { _logger.TransportWaitingForReadTask(EndpointName); - await _readTask.WaitAsync(TimeSpan.FromSeconds(5), cancellationToken); + await _readTask.WaitAsync(TimeSpan.FromSeconds(5), cancellationToken).ConfigureAwait(false); } catch (TimeoutException) { diff --git a/src/mcpdotnet/Protocol/Transport/TransportBase.cs b/src/mcpdotnet/Protocol/Transport/TransportBase.cs index c0b559a..d55807b 100644 --- a/src/mcpdotnet/Protocol/Transport/TransportBase.cs +++ b/src/mcpdotnet/Protocol/Transport/TransportBase.cs @@ -2,6 +2,7 @@ using Microsoft.Extensions.Logging; using McpDotNet.Logging; using McpDotNet.Protocol.Messages; +using Microsoft.Extensions.Logging.Abstractions; namespace McpDotNet.Protocol.Transport; @@ -11,13 +12,13 @@ namespace McpDotNet.Protocol.Transport; public abstract class TransportBase : ITransport { private readonly Channel _messageChannel; - private readonly ILogger _logger; + private readonly ILogger _logger; private bool _isConnected; /// /// Initializes a new instance of the class. /// - protected TransportBase(ILoggerFactory loggerFactory) + protected TransportBase(ILoggerFactory? loggerFactory) { // Unbounded channel to prevent blocking on writes _messageChannel = Channel.CreateUnbounded(new UnboundedChannelOptions @@ -25,7 +26,7 @@ public abstract class TransportBase : ITransport SingleReader = true, SingleWriter = true, }); - _logger = loggerFactory.CreateLogger(); + _logger = loggerFactory is not null ? loggerFactory.CreateLogger() : NullLogger.Instance; } /// @@ -53,7 +54,7 @@ public abstract class TransportBase : ITransport } _logger.TransportWritingMessageToChannel(message); - await _messageChannel.Writer.WriteAsync(message, cancellationToken); + await _messageChannel.Writer.WriteAsync(message, cancellationToken).ConfigureAwait(false); _logger.TransportMessageWrittenToChannel(); } diff --git a/src/mcpdotnet/Protocol/Types/Completion.cs b/src/mcpdotnet/Protocol/Types/Completion.cs index c55931d..e645278 100644 --- a/src/mcpdotnet/Protocol/Types/Completion.cs +++ b/src/mcpdotnet/Protocol/Types/Completion.cs @@ -12,7 +12,7 @@ public class Completion /// An array of completion values. Must not exceed 100 items. /// [JsonPropertyName("values")] - public string[] Values { get; set; } = new string[0]; + public string[] Values { get; set; } = []; /// /// The total number of completion options available. This can exceed the number of values actually sent in the response. diff --git a/src/mcpdotnet/Protocol/Types/Implementation.cs b/src/mcpdotnet/Protocol/Types/Implementation.cs index bc860c9..a052e60 100644 --- a/src/mcpdotnet/Protocol/Types/Implementation.cs +++ b/src/mcpdotnet/Protocol/Types/Implementation.cs @@ -1,6 +1,6 @@ -namespace McpDotNet.Protocol.Types; +using System.Text.Json.Serialization; -using System.Text.Json.Serialization; +namespace McpDotNet.Protocol.Types; /// /// Describes the name and version of an MCP implementation. diff --git a/src/mcpdotnet/Protocol/Types/InitializeRequestParams.cs b/src/mcpdotnet/Protocol/Types/InitializeRequestParams.cs index 417da14..782c30c 100644 --- a/src/mcpdotnet/Protocol/Types/InitializeRequestParams.cs +++ b/src/mcpdotnet/Protocol/Types/InitializeRequestParams.cs @@ -1,6 +1,6 @@ -namespace McpDotNet.Protocol.Types; +using System.Text.Json.Serialization; -using System.Text.Json.Serialization; +namespace McpDotNet.Protocol.Types; /// /// Parameters for an initialization request sent to the server. diff --git a/src/mcpdotnet/Protocol/Types/InitializeResult.cs b/src/mcpdotnet/Protocol/Types/InitializeResult.cs index f319303..6295bbf 100644 --- a/src/mcpdotnet/Protocol/Types/InitializeResult.cs +++ b/src/mcpdotnet/Protocol/Types/InitializeResult.cs @@ -1,6 +1,6 @@ -namespace McpDotNet.Protocol.Types; +using System.Text.Json.Serialization; -using System.Text.Json.Serialization; +namespace McpDotNet.Protocol.Types; /// /// Result of the initialization request sent to the server. diff --git a/src/mcpdotnet/Protocol/Types/ModelPreferences.cs b/src/mcpdotnet/Protocol/Types/ModelPreferences.cs index e93e80c..be0879d 100644 --- a/src/mcpdotnet/Protocol/Types/ModelPreferences.cs +++ b/src/mcpdotnet/Protocol/Types/ModelPreferences.cs @@ -59,21 +59,25 @@ public class ModelPreferences { bool valid = true; List errors = []; - if (CostPriority < 0 || CostPriority > 1) + + if (CostPriority is < 0 or > 1) { errors.Add("CostPriority must be between 0 and 1"); valid = false; } - if (SpeedPriority < 0 || SpeedPriority > 1) + + if (SpeedPriority is < 0 or > 1) { errors.Add("SpeedPriority must be between 0 and 1"); valid = false; } - if (IntelligencePriority < 0 || IntelligencePriority > 1) + + if (IntelligencePriority is < 0 or > 1) { errors.Add("IntelligencePriority must be between 0 and 1"); valid = false; } + if (!valid) { errorMessage = string.Join(", ", errors); @@ -82,6 +86,7 @@ public class ModelPreferences { errorMessage = ""; } + return valid; } } diff --git a/src/mcpdotnet/Protocol/Types/Resources.cs b/src/mcpdotnet/Protocol/Types/Resources.cs index 7390fcd..ea9f3e7 100644 --- a/src/mcpdotnet/Protocol/Types/Resources.cs +++ b/src/mcpdotnet/Protocol/Types/Resources.cs @@ -1,6 +1,6 @@ -namespace McpDotNet.Protocol.Types; +using System.Text.Json.Serialization; -using System.Text.Json.Serialization; +namespace McpDotNet.Protocol.Types; /// /// Represents a known resource that the server is capable of reading. diff --git a/src/mcpdotnet/Protocol/Types/ServerCapabilities.cs b/src/mcpdotnet/Protocol/Types/ServerCapabilities.cs index 243c998..a1f039a 100644 --- a/src/mcpdotnet/Protocol/Types/ServerCapabilities.cs +++ b/src/mcpdotnet/Protocol/Types/ServerCapabilities.cs @@ -1,6 +1,6 @@ -namespace McpDotNet.Protocol.Types; +using System.Text.Json.Serialization; -using System.Text.Json.Serialization; +namespace McpDotNet.Protocol.Types; /// /// Represents the capabilities that a server may support. diff --git a/src/mcpdotnet/Server/McpParameterAttribute.cs b/src/mcpdotnet/Server/McpParameterAttribute.cs index 6bdf9e3..3d8f0a6 100644 --- a/src/mcpdotnet/Server/McpParameterAttribute.cs +++ b/src/mcpdotnet/Server/McpParameterAttribute.cs @@ -4,17 +4,17 @@ /// Attribute to mark a method parameter /// [AttributeUsage(AttributeTargets.Parameter)] -public class McpParameterAttribute : Attribute +public sealed class McpParameterAttribute : Attribute { /// /// Defines if the parameter is required. /// - public bool Required { get; set; } + public bool Required { get; } /// /// A description of the parameter. /// - public string? Description { get; set; } + public string? Description { get; } /// /// Attribute to mark a method as an MCP tool. diff --git a/src/mcpdotnet/Server/McpServer.cs b/src/mcpdotnet/Server/McpServer.cs index 8d27bd8..2b1aa68 100644 --- a/src/mcpdotnet/Server/McpServer.cs +++ b/src/mcpdotnet/Server/McpServer.cs @@ -217,7 +217,7 @@ internal class McpServer : McpJsonRpcEndpoint, IMcpServer }; } - return await GetCompletionHandler(new(this, request), CancellationTokenSource?.Token ?? CancellationToken.None); + return await GetCompletionHandler(new(this, request), CancellationTokenSource?.Token ?? CancellationToken.None).ConfigureAwait(false); }); } @@ -235,7 +235,7 @@ internal class McpServer : McpJsonRpcEndpoint, IMcpServer throw new McpServerException("ListResources handler not configured"); } - return await ListResourcesHandler(new(this, request), CancellationTokenSource?.Token ?? CancellationToken.None); + return await ListResourcesHandler(new(this, request), CancellationTokenSource?.Token ?? CancellationToken.None).ConfigureAwait(false); }); SetRequestHandler("resources/read", @@ -248,7 +248,7 @@ internal class McpServer : McpJsonRpcEndpoint, IMcpServer throw new McpServerException("ReadResource handler not configured"); } - return await ReadResourceHandler(new(this, request), CancellationTokenSource?.Token ?? CancellationToken.None); + return await ReadResourceHandler(new(this, request), CancellationTokenSource?.Token ?? CancellationToken.None).ConfigureAwait(false); }); } } @@ -267,7 +267,7 @@ internal class McpServer : McpJsonRpcEndpoint, IMcpServer throw new McpServerException("ListPrompts handler not configured"); } - return await ListPromptsHandler(new(this, request), CancellationTokenSource?.Token ?? CancellationToken.None); + return await ListPromptsHandler(new(this, request), CancellationTokenSource?.Token ?? CancellationToken.None).ConfigureAwait(false); }); SetRequestHandler("prompts/get", @@ -280,7 +280,7 @@ internal class McpServer : McpJsonRpcEndpoint, IMcpServer throw new McpServerException("GetPrompt handler not configured"); } - return await GetPromptHandler(new(this, request), CancellationTokenSource?.Token ?? CancellationToken.None); + return await GetPromptHandler(new(this, request), CancellationTokenSource?.Token ?? CancellationToken.None).ConfigureAwait(false); }); } } @@ -299,7 +299,7 @@ internal class McpServer : McpJsonRpcEndpoint, IMcpServer throw new McpServerException("ListTools handler not configured"); } - return await ListToolsHandler(new(this, request), CancellationTokenSource?.Token ?? CancellationToken.None); + return await ListToolsHandler(new(this, request), CancellationTokenSource?.Token ?? CancellationToken.None).ConfigureAwait(false); }); SetRequestHandler("tools/call", @@ -312,7 +312,7 @@ internal class McpServer : McpJsonRpcEndpoint, IMcpServer throw new McpServerException("CallTool handler not configured"); } - return await CallToolHandler(new(this, request), CancellationTokenSource?.Token ?? CancellationToken.None); + return await CallToolHandler(new(this, request), CancellationTokenSource?.Token ?? CancellationToken.None).ConfigureAwait(false); }); } } diff --git a/src/mcpdotnet/Server/McpServerDelegates.cs b/src/mcpdotnet/Server/McpServerDelegates.cs index e9f3d76..5c2a539 100644 --- a/src/mcpdotnet/Server/McpServerDelegates.cs +++ b/src/mcpdotnet/Server/McpServerDelegates.cs @@ -58,6 +58,11 @@ public class McpServerDelegates /// public void Apply(IMcpServer server) { + if (server is null) + { + throw new ArgumentNullException(nameof(server)); + } + if (ListToolsHandler != null) server.ListToolsHandler = ListToolsHandler; diff --git a/src/mcpdotnet/Server/McpServerException.cs b/src/mcpdotnet/Server/McpServerException.cs index 87232dd..be7b967 100644 --- a/src/mcpdotnet/Server/McpServerException.cs +++ b/src/mcpdotnet/Server/McpServerException.cs @@ -12,6 +12,13 @@ public class McpServerException : Exception /// public int? ErrorCode { get; } + /// + /// Initializes a new instance of the class with a specified error message. + /// + public McpServerException() + { + } + /// /// Initializes a new instance of the class with a specified error message. /// diff --git a/src/mcpdotnet/Server/McpToolAttribute.cs b/src/mcpdotnet/Server/McpToolAttribute.cs index 5e0fd1f..9f14a36 100644 --- a/src/mcpdotnet/Server/McpToolAttribute.cs +++ b/src/mcpdotnet/Server/McpToolAttribute.cs @@ -4,15 +4,15 @@ /// Attribute to mark a method as an MCP tool. /// [AttributeUsage(AttributeTargets.Method)] -public class McpToolAttribute : Attribute +public sealed class McpToolAttribute : Attribute { /// The name of the tool. If not provided, the class name will be used. - public string? Name { get; set; } + public string? Name { get; } /// /// A description of the tool. /// - public string? Description { get; set; } + public string? Description { get; } /// /// Attribute to mark a method as an MCP tool. diff --git a/src/mcpdotnet/Server/McpToolTypeAttribute.cs b/src/mcpdotnet/Server/McpToolTypeAttribute.cs index 01e8015..4571303 100644 --- a/src/mcpdotnet/Server/McpToolTypeAttribute.cs +++ b/src/mcpdotnet/Server/McpToolTypeAttribute.cs @@ -4,6 +4,4 @@ /// Attribute to mark a type as container for MCP tools. /// [AttributeUsage(AttributeTargets.Class)] -public class McpToolTypeAttribute : Attribute -{ -} +public sealed class McpToolTypeAttribute : Attribute; diff --git a/src/mcpdotnet/Shared/McpJsonRpcEndpoint.cs b/src/mcpdotnet/Shared/McpJsonRpcEndpoint.cs index c55d87d..3a406f2 100644 --- a/src/mcpdotnet/Shared/McpJsonRpcEndpoint.cs +++ b/src/mcpdotnet/Shared/McpJsonRpcEndpoint.cs @@ -16,7 +16,7 @@ namespace McpDotNet.Shared; /// This is especially true as a client represents a connection to one and only one server, and vice versa. /// Any multi-client or multi-server functionality should be implemented at a higher level of abstraction. /// -internal abstract class McpJsonRpcEndpoint +internal abstract class McpJsonRpcEndpoint : IAsyncDisposable { private readonly ITransport _transport; private readonly ConcurrentDictionary> _pendingRequests; @@ -94,7 +94,7 @@ internal abstract class McpJsonRpcEndpoint var payload = JsonSerializer.Serialize(message); _logger.MessageHandlerError(EndpointName, message.GetType().Name, payload, t.Exception); } - }, TaskContinuationOptions.OnlyOnFaulted); + }, CancellationToken.None, TaskContinuationOptions.OnlyOnFaulted, TaskScheduler.Default); } private async Task HandleMessageAsync(IJsonRpcMessage message, CancellationToken cancellationToken) @@ -107,14 +107,14 @@ internal abstract class McpJsonRpcEndpoint try { _logger.RequestHandlerCalled(EndpointName, request.Method); - var result = await handler(request); + var result = await handler(request).ConfigureAwait(false); _logger.RequestHandlerCompleted(EndpointName, request.Method); await _transport.SendMessageAsync(new JsonRpcResponse { Id = request.Id, JsonRpc = "2.0", Result = result - }, cancellationToken); + }, cancellationToken).ConfigureAwait(false); } catch (Exception ex) { @@ -129,7 +129,7 @@ internal abstract class McpJsonRpcEndpoint Code = -32000, // Implementation defined error Message = ex.Message } - }, cancellationToken); + }, cancellationToken).ConfigureAwait(false); } } else @@ -362,7 +362,7 @@ internal abstract class McpJsonRpcEndpoint var jsonString = JsonSerializer.Serialize(request.Params); var typedRequest = JsonSerializer.Deserialize(jsonString, _jsonOptions); - return await handler(typedRequest); + return await handler(typedRequest).ConfigureAwait(false); }; } @@ -385,7 +385,7 @@ internal abstract class McpJsonRpcEndpoint _logger.CleaningUpEndpoint(EndpointName); if (CancellationTokenSource != null) - await CancellationTokenSource.CancelAsync(); + await CancellationTokenSource.CancelAsync().ConfigureAwait(false); if (MessageProcessingTask != null) { diff --git a/src/mcpdotnet/mcpdotnet.csproj b/src/mcpdotnet/mcpdotnet.csproj index 797d83e..d568905 100644 --- a/src/mcpdotnet/mcpdotnet.csproj +++ b/src/mcpdotnet/mcpdotnet.csproj @@ -5,6 +5,8 @@ enable enable Latest + true + All diff --git a/tests/McpDotNet.Extensions.AI.Tests/AIFunctionUtilitiesTests.cs b/tests/McpDotNet.Extensions.AI.Tests/AIFunctionUtilitiesTests.cs index abcca31..e3a8e0f 100644 --- a/tests/McpDotNet.Extensions.AI.Tests/AIFunctionUtilitiesTests.cs +++ b/tests/McpDotNet.Extensions.AI.Tests/AIFunctionUtilitiesTests.cs @@ -89,7 +89,7 @@ public class AIFunctionUtilitiesTests Assert.Equal("object", resultObj.GetProperty("type").GetString()); Assert.Equal("minimal", resultObj.GetProperty("title").GetString()); Assert.Equal("Minimal tool", resultObj.GetProperty("description").GetString()); - Assert.True(resultObj.GetProperty("properties").GetRawText() == "{}"); + Assert.Equal("{}", resultObj.GetProperty("properties").GetRawText()); Assert.Empty(resultObj.GetProperty("required").EnumerateArray()); } } \ No newline at end of file diff --git a/tests/mcpdotnet.TestServer/Program.cs b/tests/mcpdotnet.TestServer/Program.cs index 70744df..408cced 100644 --- a/tests/mcpdotnet.TestServer/Program.cs +++ b/tests/mcpdotnet.TestServer/Program.cs @@ -27,7 +27,7 @@ internal static class Program { Log.Logger.Information("Starting server..."); - McpServerOptions options = new McpServerOptions() + McpServerOptions options = new() { ServerInfo = new Implementation() { Name = "TestServer", Version = "1.0.0" }, Capabilities = new ServerCapabilities() @@ -41,7 +41,7 @@ internal static class Program }; var loggerFactory = CreateLoggerFactory(); - McpServerFactory factory = new McpServerFactory(new StdioServerTransport("TestServer", loggerFactory), options, loggerFactory); + McpServerFactory factory = new(new StdioServerTransport("TestServer", loggerFactory), options, loggerFactory); IMcpServer server = factory.CreateServer(); Log.Logger.Information("Server object created, registering handlers."); diff --git a/tests/mcpdotnet.Tests/ClientIntegrationTests.cs b/tests/mcpdotnet.Tests/ClientIntegrationTests.cs index a01271c..1316300 100644 --- a/tests/mcpdotnet.Tests/ClientIntegrationTests.cs +++ b/tests/mcpdotnet.Tests/ClientIntegrationTests.cs @@ -225,7 +225,7 @@ public class ClientIntegrationTests : IClassFixture @@ -256,7 +256,7 @@ public class ClientIntegrationTests : IClassFixture Echo([McpParameter(true)] string message) { return Task.FromResult("hello " + message); diff --git a/tests/mcpdotnet.Tests/Utils/TestSseServer.cs b/tests/mcpdotnet.Tests/Utils/TestSseServer.cs index d49625c..df421af 100644 --- a/tests/mcpdotnet.Tests/Utils/TestSseServer.cs +++ b/tests/mcpdotnet.Tests/Utils/TestSseServer.cs @@ -179,7 +179,7 @@ public sealed class TestSseServer : IAsyncDisposable /// Handle POST /message endpoint. /// Echo the content back to the caller and broadcast it over SSE as well. /// - private async Task HandlePostMessageAsync(HttpListenerContext context, CancellationToken ct) + private async Task HandlePostMessageAsync(HttpListenerContext context, CancellationToken cancellationToken) { var request = context.Request; var response = context.Response; @@ -187,7 +187,7 @@ public sealed class TestSseServer : IAsyncDisposable try { using var reader = new StreamReader(request.InputStream); - string content = await reader.ReadToEndAsync(ct); + string content = await reader.ReadToEndAsync(cancellationToken); var jsonRpcNotification = JsonSerializer.Deserialize(content); if (jsonRpcNotification != null && jsonRpcNotification.Method != "initialize") @@ -200,7 +200,7 @@ public sealed class TestSseServer : IAsyncDisposable // Write "accepted" as the response body using var writer = new StreamWriter(response.OutputStream); await writer.WriteAsync("accepted"); - await writer.FlushAsync(); + await writer.FlushAsync(cancellationToken); return; } @@ -220,7 +220,7 @@ public sealed class TestSseServer : IAsyncDisposable // Write "accepted" as the response body using var writer = new StreamWriter(response.OutputStream); await writer.WriteAsync("accepted"); - await writer.FlushAsync(ct); + await writer.FlushAsync(cancellationToken); // Then process the message and send the response via SSE if (jsonRpcRequest != null)