diff --git a/README.MD b/README.MD index 38f8460..db3f88e 100644 --- a/README.MD +++ b/README.MD @@ -61,7 +61,7 @@ var config = new McpServerConfig { Id = "everything", Name = "Everything", - TransportType = "stdio", + TransportType = TransportTypes.StdIo, TransportOptions = new Dictionary { ["command"] = "npx", diff --git a/samples/TestServerWithHosting/Program.cs b/samples/TestServerWithHosting/Program.cs index d0e3f81..4f0770c 100644 --- a/samples/TestServerWithHosting/Program.cs +++ b/samples/TestServerWithHosting/Program.cs @@ -19,7 +19,7 @@ try builder.Services.AddSerilog(); builder.Services.AddMcpServer() .WithStdioServerTransport() - .WithCallToolHandler(async (r, ct) => new McpDotNet.Protocol.Types.CallToolResponse()); + .WithCallToolHandler((r, ct) => Task.FromResult(new McpDotNet.Protocol.Types.CallToolResponse())); var app = builder.Build(); diff --git a/samples/anthropic/tools/ToolsConsole/McpToolExtensions.cs b/samples/anthropic/tools/ToolsConsole/McpToolExtensions.cs index c9dbd20..45c0cf9 100644 --- a/samples/anthropic/tools/ToolsConsole/McpToolExtensions.cs +++ b/samples/anthropic/tools/ToolsConsole/McpToolExtensions.cs @@ -6,7 +6,7 @@ public static class McpToolExtensions { public static IList ToAnthropicTools(this IEnumerable tools) { - List result = new(); + List result = []; foreach (var tool in tools) { var function = tool.InputSchema == null diff --git a/samples/anthropic/tools/ToolsConsole/Program.cs b/samples/anthropic/tools/ToolsConsole/Program.cs index fa18fb0..a889988 100644 --- a/samples/anthropic/tools/ToolsConsole/Program.cs +++ b/samples/anthropic/tools/ToolsConsole/Program.cs @@ -1,9 +1,10 @@ -using McpDotNet.Client; -using McpDotNet.Configuration; -using Microsoft.Extensions.Logging.Abstractions; -using Anthropic.SDK; -using Anthropic.SDK.Messaging; +using Anthropic.SDK; using Anthropic.SDK.Constants; +using Anthropic.SDK.Messaging; +using McpDotNet.Client; +using McpDotNet.Configuration; +using McpDotNet.Protocol.Transport; +using Microsoft.Extensions.Logging.Abstractions; internal class Program { @@ -19,7 +20,7 @@ internal class Program { Id = "everything", Name = "Everything", - TransportType = "stdio", + TransportType = TransportTypes.StdIo, TransportOptions = new Dictionary { ["command"] = "npx", @@ -85,7 +86,7 @@ internal class Program foreach (var toolCall in res.ToolCalls) { - var response = await client.CallToolAsync(toolCall.Name, toolCall.Arguments?.ToMCPArguments()); + var response = await client.CallToolAsync(toolCall.Name, toolCall.Arguments?.ToMCPArguments() ?? []); messages.Add(new Message(toolCall, response.Content[0].Text)); } diff --git a/samples/microsoft.extensions.ai/tools/ToolsConsole/McpAIFunction.cs b/samples/microsoft.extensions.ai/tools/ToolsConsole/McpAIFunction.cs index 4e3e812..6719701 100644 --- a/samples/microsoft.extensions.ai/tools/ToolsConsole/McpAIFunction.cs +++ b/samples/microsoft.extensions.ai/tools/ToolsConsole/McpAIFunction.cs @@ -30,7 +30,7 @@ public class McpAIFunction : AIFunction protected async override Task InvokeCoreAsync(IEnumerable> arguments, CancellationToken cancellationToken) { // Convert arguments to dictionary format expected by mcpdotnet - Dictionary argDict = new(); + Dictionary argDict = []; foreach (var arg in arguments) { if (arg.Value is not null) @@ -42,7 +42,7 @@ public class McpAIFunction : AIFunction // Call the tool through mcpdotnet var result = await _client.CallToolAsync( _tool.Name, - argDict.Count == 0 ? new() : argDict, + argDict.Count == 0 ? [] : argDict, cancellationToken: cancellationToken ); @@ -60,9 +60,9 @@ public class McpAIFunction : AIFunction var properties = jsonSchema?.Properties; if (properties == null) { - return new(); + return []; } - HashSet requiredProperties = new(jsonSchema!.Required ?? []); + HashSet requiredProperties = [.. jsonSchema!.Required ?? []]; return properties.Select(kvp => new AIFunctionParameterMetadata(kvp.Key) { diff --git a/samples/microsoft.extensions.ai/tools/ToolsConsole/Program.cs b/samples/microsoft.extensions.ai/tools/ToolsConsole/Program.cs index 3c0d20c..d41ca75 100644 --- a/samples/microsoft.extensions.ai/tools/ToolsConsole/Program.cs +++ b/samples/microsoft.extensions.ai/tools/ToolsConsole/Program.cs @@ -1,9 +1,9 @@ using McpDotNet.Client; using McpDotNet.Configuration; +using McpDotNet.Protocol.Transport; using Microsoft.Extensions.AI; using Microsoft.Extensions.Logging.Abstractions; using OpenAI; -using OpenAI.Chat; using SimpleToolsConsole; internal class Program @@ -20,7 +20,7 @@ internal class Program { Id = "everything", Name = "Everything", - TransportType = "stdio", + TransportType = TransportTypes.StdIo, TransportOptions = new Dictionary { ["command"] = "npx", @@ -66,9 +66,11 @@ internal class Program .Build(); // Create message list - IList messages = new List(); - // Add a system message - messages.Add(new(ChatRole.System, "You are a helpful assistant, helping us test MCP server functionality.")); + IList messages = + [ + // Add a system message + new(ChatRole.System, "You are a helpful assistant, helping us test MCP server functionality."), + ]; // If MCP server provides instructions, add them as an additional system message (you could also add it as a content part) if (!string.IsNullOrEmpty(client.ServerInstructions)) { diff --git a/src/McpDotNet.Extensions.AI/AIFunctionUtilities.cs b/src/McpDotNet.Extensions.AI/AIFunctionUtilities.cs index da00892..6825cb2 100644 --- a/src/McpDotNet.Extensions.AI/AIFunctionUtilities.cs +++ b/src/McpDotNet.Extensions.AI/AIFunctionUtilities.cs @@ -16,7 +16,7 @@ internal static class AIFunctionUtilities ["type"] = "object", ["title"] = tool.Name, ["description"] = tool.Description ?? string.Empty, - ["properties"] = tool.InputSchema?.Properties ?? new(), + ["properties"] = tool.InputSchema?.Properties ?? [], ["required"] = tool.InputSchema?.Required ?? [] }; diff --git a/src/McpDotNet.Extensions.AI/McpAIFunction.cs b/src/McpDotNet.Extensions.AI/McpAIFunction.cs index 6c8f7b1..35bb445 100644 --- a/src/McpDotNet.Extensions.AI/McpAIFunction.cs +++ b/src/McpDotNet.Extensions.AI/McpAIFunction.cs @@ -32,7 +32,7 @@ public class McpAIFunction : AIFunction protected async override Task InvokeCoreAsync(IEnumerable> arguments, CancellationToken cancellationToken) { // Convert arguments to dictionary format expected by mcpdotnet - Dictionary argDict = new(); + Dictionary argDict = []; foreach (var arg in arguments) { if (arg.Value is not null) @@ -44,7 +44,7 @@ public class McpAIFunction : AIFunction // Call the tool through mcpdotnet var result = await _client.CallToolAsync( _tool.Name, - argDict.Count == 0 ? new() : argDict, + argDict.Count == 0 ? [] : argDict, cancellationToken: cancellationToken ); diff --git a/src/McpDotNet.Extensions.AI/McpSessionScope.cs b/src/McpDotNet.Extensions.AI/McpSessionScope.cs index 872c44f..b37fcaa 100644 --- a/src/McpDotNet.Extensions.AI/McpSessionScope.cs +++ b/src/McpDotNet.Extensions.AI/McpSessionScope.cs @@ -14,7 +14,7 @@ namespace McpDotNet.Extensions.AI; /// public class McpSessionScope : IAsyncDisposable { - private readonly List _clients = new(); + private readonly List _clients = []; private bool _disposed; /// diff --git a/src/McpDotNet.Extensions.AI/README.md b/src/McpDotNet.Extensions.AI/README.md index 53d335b..69f00ad 100644 --- a/src/McpDotNet.Extensions.AI/README.md +++ b/src/McpDotNet.Extensions.AI/README.md @@ -37,7 +37,7 @@ var config = new McpServerConfig { Id = "everything", Name = "Everything", - TransportType = "stdio", + TransportType = TransportTypes.StdIo, TransportOptions = new Dictionary { ["command"] = "npx", diff --git a/src/mcpdotnet/Client/IMcpClient.cs b/src/mcpdotnet/Client/IMcpClient.cs index 9d1f137..e6e11ce 100644 --- a/src/mcpdotnet/Client/IMcpClient.cs +++ b/src/mcpdotnet/Client/IMcpClient.cs @@ -1,7 +1,7 @@ namespace McpDotNet.Client; -using McpDotNet.Protocol.Types; using McpDotNet.Protocol.Messages; +using McpDotNet.Protocol.Types; /// /// Represents an instance of the MCP client connecting to a specific server. @@ -35,12 +35,12 @@ public interface IMcpClient : IAsyncDisposable /// /// Gets or sets the handler for server sampling requests. /// - Func>? SamplingHandler { get; set; } + Func>? SamplingHandler { get; set; } /// /// Gets or sets the handler for providing root URIs to servers. /// - Func>? RootsHandler { get; set; } + Func>? RootsHandler { get; set; } /// /// Establishes a connection to the server. diff --git a/src/mcpdotnet/Client/McpClient.cs b/src/mcpdotnet/Client/McpClient.cs index 0ffd16d..c02451a 100644 --- a/src/mcpdotnet/Client/McpClient.cs +++ b/src/mcpdotnet/Client/McpClient.cs @@ -1,12 +1,12 @@  -using McpDotNet.Protocol.Types; -using McpDotNet.Protocol.Transport; -using McpDotNet.Protocol.Messages; using System.Text.Json; -using Microsoft.Extensions.Logging; -using McpDotNet.Logging; using McpDotNet.Configuration; +using McpDotNet.Logging; +using McpDotNet.Protocol.Messages; +using McpDotNet.Protocol.Transport; +using McpDotNet.Protocol.Types; using McpDotNet.Shared; +using Microsoft.Extensions.Logging; namespace McpDotNet.Client; @@ -46,7 +46,8 @@ internal class McpClient : McpJsonRpcEndpoint, IMcpClient if (options.Capabilities?.Sampling != null) { SetRequestHandler("sampling/createMessage", - async (request) => { + async (request) => + { if (SamplingHandler == null) { // Setting the capability, but not a handler means we have nothing to return to the server @@ -168,7 +169,7 @@ internal class McpClient : McpJsonRpcEndpoint, IMcpClient } /// - public async Task PingAsync(CancellationToken cancellationToken) + public async Task PingAsync(CancellationToken cancellationToken = default) { _logger.PingingServer(EndpointName); await SendRequestAsync( @@ -268,7 +269,7 @@ internal class McpClient : McpJsonRpcEndpoint, IMcpClient } if (argumentValue is null) { - _logger.InvalidCompletionArgumentValue(EndpointName, argumentValue); + _logger.InvalidCompletionArgumentValue(EndpointName, "null", argumentName); throw new McpClientException("Argument value cannot be null"); } @@ -330,13 +331,13 @@ internal class McpClient : McpJsonRpcEndpoint, IMcpClient new JsonRpcRequest { Method = "tools/call", - Params = CreateParametersDictionary(toolName, arguments ?? new()) + Params = CreateParametersDictionary(toolName, arguments ?? []) }, cancellationToken ).ConfigureAwait(false); } - private static Dictionary CreateParametersDictionary(string nameParameter, Dictionary arguments) + private static Dictionary CreateParametersDictionary(string nameParameter, Dictionary? arguments) { var parameters = new Dictionary { @@ -352,10 +353,10 @@ internal class McpClient : McpJsonRpcEndpoint, IMcpClient } /// - public Func>? SamplingHandler { get; set; } + public Func>? SamplingHandler { get; set; } /// - public Func>? RootsHandler { get; set; } + public Func>? RootsHandler { get; set; } /// public override string EndpointName diff --git a/src/mcpdotnet/Client/McpClientFactory.cs b/src/mcpdotnet/Client/McpClientFactory.cs index 5aedc11..34fb2c2 100644 --- a/src/mcpdotnet/Client/McpClientFactory.cs +++ b/src/mcpdotnet/Client/McpClientFactory.cs @@ -14,7 +14,7 @@ public class McpClientFactory { private readonly Dictionary _serverConfigs; private readonly McpClientOptions _clientOptions; - private readonly Dictionary _clients = new(); + private readonly Dictionary _clients = []; private readonly Func _transportFactoryMethod; private readonly Func _clientFactoryMethod; private readonly ILoggerFactory _loggerFactory; @@ -47,12 +47,9 @@ public class McpClientFactory // Initialize commands for stdio transport, this is to run commands in a shell even if specified directly, as otherwise // the stdio protocol will not work correctly. _logger.InitializingStdioCommands(); - foreach (var config in _serverConfigs.Values) + foreach (var config in _serverConfigs.Values.Where(c => c.TransportType.Equals(TransportTypes.StdIo, StringComparison.OrdinalIgnoreCase))) { - if (config.TransportType.ToLowerInvariant() == "stdio") - { - InitializeCommand(config); - } + InitializeCommand(config); } } @@ -97,11 +94,11 @@ public class McpClientFactory { string endpointName = $"Client ({config.Id}: {config.Name})"; - var options = string.Join(", ", config.TransportOptions?.Select(kv => $"{kv.Key}={kv.Value}") ?? Enumerable.Empty()); + var options = string.Join(", ", config.TransportOptions?.Select(kv => $"{kv.Key}={kv.Value}") ?? []); _logger.CreatingTransport(endpointName, config.TransportType, options); return config.TransportType.ToLowerInvariant() switch { - "stdio" => new StdioClientTransport(new StdioClientTransportOptions + TransportTypes.StdIo => new StdioClientTransport(new StdioClientTransportOptions { Command = GetCommand(config), Arguments = config.TransportOptions?.GetValueOrDefault("arguments")?.Split(' '), @@ -110,7 +107,7 @@ public class McpClientFactory .Where(kv => kv.Key.StartsWith("env:")) .ToDictionary(kv => kv.Key.Substring(4), kv => kv.Value) }, config, _loggerFactory), - "sse" or "http" => new SseClientTransport( + TransportTypes.Sse or "http" => new SseClientTransport( new SseClientTransportOptions { ConnectionTimeout = TimeSpan.FromSeconds(ParseOrDefault(config.TransportOptions, "connectionTimeout", 30)), @@ -124,7 +121,7 @@ public class McpClientFactory }; } - private static int ParseOrDefault(IDictionary? options, string key, int defaultValue) + private static int ParseOrDefault(Dictionary? options, string key, int defaultValue) { if (options?.TryGetValue(key, out var value) ?? false) { @@ -135,15 +132,14 @@ public class McpClientFactory return defaultValue; } - private string? GetCommand(McpServerConfig config) + private static string GetCommand(McpServerConfig config) { - if (config.TransportOptions == null || - string.IsNullOrEmpty(config.TransportOptions.GetValueOrDefault("command"))) - { - return config.Location!; - } + var command = config.TransportOptions?.GetValueOrDefault("command"); - return OperatingSystem.IsWindows() ? "cmd.exe" : config.TransportOptions?.GetValueOrDefault("command"); + if (string.IsNullOrEmpty(command)) + return config.Location!; + + return OperatingSystem.IsWindows() ? "cmd.exe" : command; } /// @@ -162,7 +158,7 @@ public class McpClientFactory } // On Windows, we need to wrap non-shell commands with cmd.exe /c - if (command.ToLowerInvariant().Contains("cmd.exe")) + if (command.Contains("cmd.exe", StringComparison.OrdinalIgnoreCase)) { _logger.SkippingShellWrapper(endpointName); return; @@ -171,8 +167,8 @@ public class McpClientFactory // If the command is not empty and does not contain cmd.exe, we need to inject /c {command} (usually npx or uvicorn) // This is because the stdio transport will not work correctly if the command is not run in a shell _logger.PromotingCommandToShellArgumentForStdio(endpointName, command, config.TransportOptions!.GetValueOrDefault("arguments") ?? ""); - config.TransportOptions!["arguments"] = config.TransportOptions.ContainsKey("arguments") - ? $"/c {command} {config.TransportOptions["arguments"]}" + config.TransportOptions!["arguments"] = config.TransportOptions.TryGetValue("arguments", out var args) + ? $"/c {command} {args}" : $"/c {command}"; } } \ No newline at end of file diff --git a/src/mcpdotnet/Configuration/McpServerBuilderExtensions.cs b/src/mcpdotnet/Configuration/McpServerBuilderExtensions.cs index acfb9ea..b97171a 100644 --- a/src/mcpdotnet/Configuration/McpServerBuilderExtensions.cs +++ b/src/mcpdotnet/Configuration/McpServerBuilderExtensions.cs @@ -28,7 +28,7 @@ public static class McpServerBuilderExtensions /// The builder instance. /// The handler. /// - public static IMcpServerBuilder WithListToolsHandler(this IMcpServerBuilder builder, Func> handler) + public static IMcpServerBuilder WithListToolsHandler(this IMcpServerBuilder builder, Func> handler) { builder.Services.Configure(s => s.ListToolsHandler = handler); return builder; @@ -40,7 +40,7 @@ public static class McpServerBuilderExtensions /// The builder instance. /// The handler. /// - public static IMcpServerBuilder WithCallToolHandler(this IMcpServerBuilder builder, Func> handler) + public static IMcpServerBuilder WithCallToolHandler(this IMcpServerBuilder builder, Func> handler) { builder.Services.Configure(s => s.CallToolHandler = handler); return builder; @@ -52,7 +52,7 @@ public static class McpServerBuilderExtensions /// The builder instance. /// The handler. /// - public static IMcpServerBuilder WithListPromptsHandler(this IMcpServerBuilder builder, Func> handler) + public static IMcpServerBuilder WithListPromptsHandler(this IMcpServerBuilder builder, Func> handler) { builder.Services.Configure(s => s.ListPromptsHandler = handler); return builder; @@ -64,7 +64,7 @@ public static class McpServerBuilderExtensions /// The builder instance. /// The handler. /// - public static IMcpServerBuilder WithGetPromptHandler(this IMcpServerBuilder builder, Func> handler) + public static IMcpServerBuilder WithGetPromptHandler(this IMcpServerBuilder builder, Func> handler) { builder.Services.Configure(s => s.GetPromptHandler = handler); return builder; @@ -76,7 +76,7 @@ public static class McpServerBuilderExtensions /// The builder instance. /// The handler. /// - public static IMcpServerBuilder WithListResourcesHandler(this IMcpServerBuilder builder, Func> handler) + public static IMcpServerBuilder WithListResourcesHandler(this IMcpServerBuilder builder, Func> handler) { builder.Services.Configure(s => s.ListResourcesHandler = handler); return builder; @@ -88,7 +88,7 @@ public static class McpServerBuilderExtensions /// The builder instance. /// The handler. /// - public static IMcpServerBuilder WithReadResourceHandler(this IMcpServerBuilder builder, Func> handler) + public static IMcpServerBuilder WithReadResourceHandler(this IMcpServerBuilder builder, Func> handler) { builder.Services.Configure(s => s.ReadResourceHandler = handler); return builder; @@ -100,7 +100,7 @@ public static class McpServerBuilderExtensions /// The builder instance. /// The handler. /// - public static IMcpServerBuilder WithGetCompletionHandler(this IMcpServerBuilder builder, Func> handler) + public static IMcpServerBuilder WithGetCompletionHandler(this IMcpServerBuilder builder, Func> handler) { builder.Services.Configure(s => s.GetCompletionHandler = handler); return builder; diff --git a/src/mcpdotnet/Logging/Log.cs b/src/mcpdotnet/Logging/Log.cs index ed5e286..771e633 100644 --- a/src/mcpdotnet/Logging/Log.cs +++ b/src/mcpdotnet/Logging/Log.cs @@ -364,12 +364,13 @@ internal static partial class Log [LoggerMessage( EventId = 7006, Level = LogLevel.Trace, - Message = "Invalid completion argument value {argumentValue} for {endpointName}" + Message = "Invalid completion argument value {argumentValue} for {argumentName} for {endpointName}" )] public static partial void InvalidCompletionArgumentValue( this ILogger logger, string endpointName, - string argumentValue); + string argumentValue, + string argumentName); [LoggerMessage( EventId = 7007, diff --git a/src/mcpdotnet/Protocol/Transport/SseClientTransport.cs b/src/mcpdotnet/Protocol/Transport/SseClientTransport.cs index 6c96188..a508d26 100644 --- a/src/mcpdotnet/Protocol/Transport/SseClientTransport.cs +++ b/src/mcpdotnet/Protocol/Transport/SseClientTransport.cs @@ -1,12 +1,11 @@ -using System.Text.Json; +using System.Net.Http.Headers; using System.Text; -using McpDotNet.Protocol.Messages; +using System.Text.Json; using McpDotNet.Configuration; -using Microsoft.Extensions.Logging; using McpDotNet.Logging; -using System.Net.Http.Headers; -using System.Text.Json.Serialization; +using McpDotNet.Protocol.Messages; using McpDotNet.Utils.Json; +using Microsoft.Extensions.Logging; namespace McpDotNet.Protocol.Transport; @@ -24,7 +23,7 @@ public sealed class SseClientTransport : TransportBase, IClientTransport private readonly ILogger _logger; private readonly McpServerConfig _serverConfig; private readonly JsonSerializerOptions _jsonOptions; - private TaskCompletionSource _connectionEstablished; + private readonly TaskCompletionSource _connectionEstablished; private string EndpointName => $"Client (SSE) for ({_serverConfig.Id}: {_serverConfig.Name})"; @@ -100,7 +99,7 @@ public sealed class SseClientTransport : TransportBase, IClientTransport ); string messageId = "(no id)"; - RequestId? requestId = null; + if (message is IJsonRpcMessageWithId messageWithId) { messageId = messageWithId.Id.ToString(); @@ -114,24 +113,22 @@ public sealed class SseClientTransport : TransportBase, IClientTransport response.EnsureSuccessStatusCode(); - var responseContent = await response.Content.ReadAsStringAsync(); + var responseContent = await response.Content.ReadAsStringAsync(cancellationToken); // Check if the message was an initialize request if (message is JsonRpcRequest request && request.Method == "initialize") { // If the response is not a JSON-RPC response, it is an SSE message - if (responseContent.ToLowerInvariant() == "accepted") + if (responseContent.Equals("accepted", StringComparison.OrdinalIgnoreCase)) { _logger.SSETransportPostAccepted(EndpointName, messageId); // The response will arrive as an SSE message } else { - JsonRpcResponse initializeResponse = JsonSerializer.Deserialize(responseContent, _jsonOptions); - if (initializeResponse == null) - { + JsonRpcResponse initializeResponse = JsonSerializer.Deserialize(responseContent, _jsonOptions) ?? throw new McpTransportException("Failed to initialize client"); - } + _logger.TransportReceivedMessageParsed(EndpointName, messageId); await WriteMessageAsync(initializeResponse, cancellationToken); _logger.TransportMessageWritten(EndpointName, messageId); @@ -140,7 +137,7 @@ public sealed class SseClientTransport : TransportBase, IClientTransport } // Otherwise, check if the response was accepted (the response will come as an SSE message) - if (responseContent.ToLowerInvariant() == "accepted") + if (responseContent.Equals("accepted", StringComparison.OrdinalIgnoreCase)) { _logger.SSETransportPostAccepted(EndpointName, messageId); } @@ -154,7 +151,7 @@ public sealed class SseClientTransport : TransportBase, IClientTransport /// public async Task CloseAsync() { - _connectionCts.Cancel(); + await _connectionCts.CancelAsync(); if (_receiveTask != null) await _receiveTask; @@ -290,7 +287,7 @@ public sealed class SseClientTransport : TransportBase, IClientTransport } // Check if data is absolute URI - if (data.ToLowerInvariant().StartsWith("http://") || data.ToLowerInvariant().StartsWith("https://")) + if (data.StartsWith("http://", StringComparison.OrdinalIgnoreCase) || data.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) { // Since the endpoint is an absolute URI, we can use it directly _messageEndpoint = new Uri(data); @@ -317,10 +314,4 @@ public sealed class SseClientTransport : TransportBase, IClientTransport throw new McpTransportException("Failed to parse endpoint event", ex); } } - - private record EndpointEventData - { - [JsonPropertyName("uri")] - public string? Uri { get; init; } - } } \ No newline at end of file diff --git a/src/mcpdotnet/Protocol/Transport/StdioClientTransport.cs b/src/mcpdotnet/Protocol/Transport/StdioClientTransport.cs index d88b197..c015575 100644 --- a/src/mcpdotnet/Protocol/Transport/StdioClientTransport.cs +++ b/src/mcpdotnet/Protocol/Transport/StdioClientTransport.cs @@ -1,10 +1,10 @@ using System.Diagnostics; using System.Text.Json; using McpDotNet.Configuration; +using McpDotNet.Logging; using McpDotNet.Protocol.Messages; using McpDotNet.Utils.Json; using Microsoft.Extensions.Logging; -using McpDotNet.Logging; namespace McpDotNet.Protocol.Transport; @@ -139,7 +139,7 @@ public sealed class StdioClientTransport : TransportBase, IClientTransport // Write the message followed by a newline await _process!.StandardInput.WriteLineAsync(json.AsMemory(), cancellationToken); - await _process.StandardInput.FlushAsync(); + await _process.StandardInput.FlushAsync(cancellationToken); _logger.TransportSentMessage(EndpointName, id); } @@ -228,7 +228,7 @@ public sealed class StdioClientTransport : TransportBase, IClientTransport { _logger.TransportCleaningUp(EndpointName); if (_process != null && _processStarted && !_process.HasExited) - { + { try { // Try to close stdin to signal the process to exit @@ -253,7 +253,8 @@ public sealed class StdioClientTransport : TransportBase, IClientTransport _process = null; } - _shutdownCts?.Cancel(); + if (_shutdownCts != null) + await _shutdownCts.CancelAsync(); _shutdownCts?.Dispose(); _shutdownCts = null; diff --git a/src/mcpdotnet/Protocol/Transport/StdioClientTransportOptions.cs b/src/mcpdotnet/Protocol/Transport/StdioClientTransportOptions.cs index 0213ed7..926e635 100644 --- a/src/mcpdotnet/Protocol/Transport/StdioClientTransportOptions.cs +++ b/src/mcpdotnet/Protocol/Transport/StdioClientTransportOptions.cs @@ -14,7 +14,7 @@ public record StdioClientTransportOptions /// /// Arguments to pass to the server process. /// - public string[]? Arguments { get; set; } = Array.Empty(); + public string[]? Arguments { get; set; } = []; /// /// The working directory for the server process. diff --git a/src/mcpdotnet/Protocol/Transport/TransportTypes.cs b/src/mcpdotnet/Protocol/Transport/TransportTypes.cs new file mode 100644 index 0000000..518848f --- /dev/null +++ b/src/mcpdotnet/Protocol/Transport/TransportTypes.cs @@ -0,0 +1,17 @@ +namespace McpDotNet.Protocol.Transport; + +/// +/// List all transport types +/// +public static class TransportTypes +{ + /// + /// The name of the Standard IO transport. + /// + public const string StdIo = "stdio"; + + /// + /// The name of the ServerSideEvents transport. + /// + public const string Sse = "sse"; +} diff --git a/src/mcpdotnet/Protocol/Types/Argument.cs b/src/mcpdotnet/Protocol/Types/Argument.cs index e5eba40..18ac2f2 100644 --- a/src/mcpdotnet/Protocol/Types/Argument.cs +++ b/src/mcpdotnet/Protocol/Types/Argument.cs @@ -8,9 +8,15 @@ namespace McpDotNet.Protocol.Types; /// public class Argument { + /// + /// The name of the argument. + /// [JsonPropertyName("name")] public string Name { get; set; } = string.Empty; + /// + /// The value of the argument to use for completion matching. + /// [JsonPropertyName("value")] public string Value { get; set; } = string.Empty; } \ No newline at end of file diff --git a/src/mcpdotnet/Protocol/Types/CallToolResponse.cs b/src/mcpdotnet/Protocol/Types/CallToolResponse.cs index f2ff6fe..63c52be 100644 --- a/src/mcpdotnet/Protocol/Types/CallToolResponse.cs +++ b/src/mcpdotnet/Protocol/Types/CallToolResponse.cs @@ -19,7 +19,7 @@ public class CallToolResponse /// The server's response to a tools/call request from the client. /// [System.Text.Json.Serialization.JsonPropertyName("content")] - public List Content { get; set; } = new(); + public List Content { get; set; } = []; /// /// Whether the tool call was unsuccessful. If true, the call was unsuccessful. diff --git a/src/mcpdotnet/Protocol/Types/GetPromptResult.cs b/src/mcpdotnet/Protocol/Types/GetPromptResult.cs index db1a51c..73f9ec5 100644 --- a/src/mcpdotnet/Protocol/Types/GetPromptResult.cs +++ b/src/mcpdotnet/Protocol/Types/GetPromptResult.cs @@ -16,5 +16,5 @@ public class GetPromptResult /// The prompt or prompt template that the server offers. /// [System.Text.Json.Serialization.JsonPropertyName("messages")] - public List Messages { get; set; } = new(); + public List Messages { get; set; } = []; } diff --git a/src/mcpdotnet/Protocol/Types/ListPromptsResult.cs b/src/mcpdotnet/Protocol/Types/ListPromptsResult.cs index d37689b..35f9e8f 100644 --- a/src/mcpdotnet/Protocol/Types/ListPromptsResult.cs +++ b/src/mcpdotnet/Protocol/Types/ListPromptsResult.cs @@ -12,5 +12,5 @@ public class ListPromptsResult : PaginatedResult /// A list of prompts or prompt templates that the server offers. /// [System.Text.Json.Serialization.JsonPropertyName("prompts")] - public List Prompts { get; set; } = new(); + public List Prompts { get; set; } = []; } \ No newline at end of file diff --git a/src/mcpdotnet/Protocol/Types/ListResourcesResult.cs b/src/mcpdotnet/Protocol/Types/ListResourcesResult.cs index a67f6f6..f532c3e 100644 --- a/src/mcpdotnet/Protocol/Types/ListResourcesResult.cs +++ b/src/mcpdotnet/Protocol/Types/ListResourcesResult.cs @@ -12,5 +12,5 @@ public class ListResourcesResult : PaginatedResult /// A list of resources that the server offers. /// [System.Text.Json.Serialization.JsonPropertyName("resources")] - public List Resources { get; set; } = new(); + public List Resources { get; set; } = []; } diff --git a/src/mcpdotnet/Protocol/Types/ListToolsResult.cs b/src/mcpdotnet/Protocol/Types/ListToolsResult.cs index 8a70c61..8979f22 100644 --- a/src/mcpdotnet/Protocol/Types/ListToolsResult.cs +++ b/src/mcpdotnet/Protocol/Types/ListToolsResult.cs @@ -12,5 +12,5 @@ public class ListToolsResult : PaginatedResult /// The server's response to a tools/list request from the client. /// [System.Text.Json.Serialization.JsonPropertyName("tools")] - public List Tools { get; set; } = new(); + public List Tools { get; set; } = []; } diff --git a/src/mcpdotnet/Protocol/Types/ModelPreferences.cs b/src/mcpdotnet/Protocol/Types/ModelPreferences.cs index 97c2f21..e93e80c 100644 --- a/src/mcpdotnet/Protocol/Types/ModelPreferences.cs +++ b/src/mcpdotnet/Protocol/Types/ModelPreferences.cs @@ -58,7 +58,7 @@ public class ModelPreferences public bool Validate(out string errorMessage) { bool valid = true; - List errors = new(); + List errors = []; if (CostPriority < 0 || CostPriority > 1) { errors.Add("CostPriority must be between 0 and 1"); diff --git a/src/mcpdotnet/Protocol/Types/ReadResourceResult.cs b/src/mcpdotnet/Protocol/Types/ReadResourceResult.cs index 69f47fd..9e4e49b 100644 --- a/src/mcpdotnet/Protocol/Types/ReadResourceResult.cs +++ b/src/mcpdotnet/Protocol/Types/ReadResourceResult.cs @@ -10,5 +10,5 @@ public class ReadResourceResult /// A list of ResourceContents that this resource contains. /// [System.Text.Json.Serialization.JsonPropertyName("contents")] - public List Contents { get; set; } = new(); + public List Contents { get; set; } = []; } diff --git a/src/mcpdotnet/Protocol/Types/Reference.cs b/src/mcpdotnet/Protocol/Types/Reference.cs index f4fb40a..46669cb 100644 --- a/src/mcpdotnet/Protocol/Types/Reference.cs +++ b/src/mcpdotnet/Protocol/Types/Reference.cs @@ -14,9 +14,15 @@ public class Reference [JsonPropertyName("type")] public string Type { get; set; } = string.Empty; + /// + /// The URI or URI template of the resource. + /// [JsonPropertyName("uri")] public string? Uri { get; set; } + /// + /// The name of the prompt or prompt template. + /// [JsonPropertyName("name")] public string? Name { get; set; } @@ -65,6 +71,9 @@ public class Reference /// public class CompleteResult { + /// + /// The completion object containing the completion values. + /// [JsonPropertyName("completion")] public Completion Completion { get; set; } = new Completion(); } diff --git a/src/mcpdotnet/Server/IMcpServer.cs b/src/mcpdotnet/Server/IMcpServer.cs index 2839619..6c533f6 100644 --- a/src/mcpdotnet/Server/IMcpServer.cs +++ b/src/mcpdotnet/Server/IMcpServer.cs @@ -1,5 +1,5 @@ -using McpDotNet.Protocol.Types; -using McpDotNet.Protocol.Messages; +using McpDotNet.Protocol.Messages; +using McpDotNet.Protocol.Types; namespace McpDotNet.Server; @@ -31,37 +31,37 @@ public interface IMcpServer : IAsyncDisposable /// /// Gets or sets the handler for list tools requests. /// - Func>? ListToolsHandler { get; set; } + Func>? ListToolsHandler { get; set; } /// /// Gets or sets the handler for call tool requests. /// - Func>? CallToolHandler { get; set; } + Func>? CallToolHandler { get; set; } /// /// Gets or sets the handler for list prompts requests. /// - Func>? ListPromptsHandler { get; set; } + Func>? ListPromptsHandler { get; set; } /// /// Gets or sets the handler for get prompt requests. /// - Func>? GetPromptHandler { get; set; } + Func>? GetPromptHandler { get; set; } /// /// Gets or sets the handler for list resources requests. /// - Func>? ListResourcesHandler { get; set; } + Func>? ListResourcesHandler { get; set; } /// /// Gets or sets the handler for read resources requests. /// - Func>? ReadResourceHandler { get; set; } + Func>? ReadResourceHandler { get; set; } /// /// Gets or sets the handler for get resources requests. /// - Func>? GetCompletionHandler { get; set; } + Func>? GetCompletionHandler { get; set; } /// /// Gets or sets the handler for subscribe to resources messages. diff --git a/src/mcpdotnet/Server/McpServer.cs b/src/mcpdotnet/Server/McpServer.cs index c253255..8aef11f 100644 --- a/src/mcpdotnet/Server/McpServer.cs +++ b/src/mcpdotnet/Server/McpServer.cs @@ -139,8 +139,8 @@ internal class McpServer : McpJsonRpcEndpoint, IMcpServer SetRequestHandler("initialize", (request) => { - ClientCapabilities = request.Capabilities ?? new(); - ClientInfo = request.ClientInfo; + ClientCapabilities = request?.Capabilities ?? new(); + ClientInfo = request?.ClientInfo; return Task.FromResult(new InitializeResult() { ProtocolVersion = options.ProtocolVersion, @@ -171,25 +171,25 @@ internal class McpServer : McpJsonRpcEndpoint, IMcpServer public string? ServerInstructions { get; set; } /// - public Func>? ListToolsHandler { get; set; } + public Func>? ListToolsHandler { get; set; } /// - public Func>? CallToolHandler { get; set; } + public Func>? CallToolHandler { get; set; } /// - public Func>? ListPromptsHandler { get; set; } + public Func>? ListPromptsHandler { get; set; } /// - public Func>? GetPromptHandler { get; set; } + public Func>? GetPromptHandler { get; set; } /// - public Func>? ListResourcesHandler { get; set; } + public Func>? ListResourcesHandler { get; set; } /// - public Func>? ReadResourceHandler { get; set; } + public Func>? ReadResourceHandler { get; set; } /// - public Func>? GetCompletionHandler { get; set; } + public Func>? GetCompletionHandler { get; set; } /// public Func? SubscribeToResourcesHandler { get; set; } diff --git a/src/mcpdotnet/Server/McpServerDelegates.cs b/src/mcpdotnet/Server/McpServerDelegates.cs index fc5b691..f5f0de7 100644 --- a/src/mcpdotnet/Server/McpServerDelegates.cs +++ b/src/mcpdotnet/Server/McpServerDelegates.cs @@ -10,37 +10,37 @@ public class McpServerDelegates /// /// Gets or sets the handler for list tools requests. /// - public Func>? ListToolsHandler { get; set; } + public Func>? ListToolsHandler { get; set; } /// /// Gets or sets the handler for call tool requests. /// - public Func>? CallToolHandler { get; set; } + public Func>? CallToolHandler { get; set; } /// /// Gets or sets the handler for list prompts requests. /// - public Func>? ListPromptsHandler { get; set; } + public Func>? ListPromptsHandler { get; set; } /// /// Gets or sets the handler for get prompt requests. /// - public Func>? GetPromptHandler { get; set; } + public Func>? GetPromptHandler { get; set; } /// /// Gets or sets the handler for list resources requests. /// - public Func>? ListResourcesHandler { get; set; } + public Func>? ListResourcesHandler { get; set; } /// /// Gets or sets the handler for read resources requests. /// - public Func>? ReadResourceHandler { get; set; } + public Func>? ReadResourceHandler { get; set; } /// /// Gets or sets the handler for get resources requests. /// - public Func>? GetCompletionHandler { get; set; } + public Func>? GetCompletionHandler { get; set; } /// /// Gets or sets the handler for subscribe to resources messages. diff --git a/src/mcpdotnet/Shared/McpJsonRpcEndpoint.cs b/src/mcpdotnet/Shared/McpJsonRpcEndpoint.cs index f500a41..0f1b236 100644 --- a/src/mcpdotnet/Shared/McpJsonRpcEndpoint.cs +++ b/src/mcpdotnet/Shared/McpJsonRpcEndpoint.cs @@ -1,11 +1,11 @@ using System.Collections.Concurrent; using System.Text.Json; -using Microsoft.Extensions.Logging; using McpDotNet.Client; +using McpDotNet.Logging; using McpDotNet.Protocol.Messages; using McpDotNet.Protocol.Transport; using McpDotNet.Utils.Json; -using McpDotNet.Logging; +using Microsoft.Extensions.Logging; namespace McpDotNet.Shared; @@ -21,10 +21,8 @@ internal abstract class McpJsonRpcEndpoint private readonly ITransport _transport; private readonly ConcurrentDictionary> _pendingRequests; private readonly ConcurrentDictionary>> _notificationHandlers; - private readonly Dictionary>> _requestHandlers = new(); + private readonly Dictionary>> _requestHandlers = []; private int _nextRequestId; - private Task? _messageProcessingTask; - private CancellationTokenSource? _cts; private readonly JsonSerializerOptions _jsonOptions; private readonly ILogger _logger; @@ -33,7 +31,7 @@ internal abstract class McpJsonRpcEndpoint /// /// An MCP transport implementation. /// The logger factory. - public McpJsonRpcEndpoint(ITransport transport, ILoggerFactory loggerFactory) + protected McpJsonRpcEndpoint(ITransport transport, ILoggerFactory loggerFactory) { _transport = transport; _pendingRequests = new(); @@ -330,7 +328,7 @@ internal abstract class McpJsonRpcEndpoint /// The async handler function to process notifications. public void OnNotification(string method, Func handler) { - var handlers = _notificationHandlers.GetOrAdd(method, _ => new()); + var handlers = _notificationHandlers.GetOrAdd(method, _ => []); lock (handlers) { handlers.Add(handler); @@ -351,12 +349,9 @@ internal abstract class McpJsonRpcEndpoint /// Type of response payload (not full RPC response /// Method identifier to register for /// Handler to be called when a request with specified method identifier is received - protected void SetRequestHandler(string method, Func> handler) + protected void SetRequestHandler(string method, Func> handler) where TResponse : class { - if (handler is null) - { - throw new ArgumentNullException(nameof(handler)); - } + ArgumentNullException.ThrowIfNull(handler); _requestHandlers[method] = async (request) => { @@ -371,20 +366,12 @@ internal abstract class McpJsonRpcEndpoint /// /// Task that processes incoming messages from the transport. /// - protected Task? MessageProcessingTask - { - get => _messageProcessingTask; - set => _messageProcessingTask = value; - } + protected Task? MessageProcessingTask { get; set; } /// /// CancellationTokenSource used to cancel the message processing task. /// - protected CancellationTokenSource? CancellationTokenSource - { - get => _cts; - set => _cts = value; - } + protected CancellationTokenSource? CancellationTokenSource { get; set; } /// /// Cleans up the endpoint and releases resources. @@ -394,13 +381,14 @@ internal abstract class McpJsonRpcEndpoint { _logger.CleaningUpEndpoint(EndpointName); - _cts?.Cancel(); + if (CancellationTokenSource != null) + await CancellationTokenSource.CancelAsync(); - if (_messageProcessingTask != null) + if (MessageProcessingTask != null) { try { - await _messageProcessingTask.ConfigureAwait(false); + await MessageProcessingTask.ConfigureAwait(false); } catch (OperationCanceledException) { @@ -416,7 +404,7 @@ internal abstract class McpJsonRpcEndpoint _pendingRequests.Clear(); await _transport.DisposeAsync().ConfigureAwait(false); - _cts?.Dispose(); + CancellationTokenSource?.Dispose(); IsInitialized = false; diff --git a/tests/McpDotNet.Extensions.AI.Tests/IntegrationTests.cs b/tests/McpDotNet.Extensions.AI.Tests/IntegrationTests.cs index ec57ba0..020fb0d 100644 --- a/tests/McpDotNet.Extensions.AI.Tests/IntegrationTests.cs +++ b/tests/McpDotNet.Extensions.AI.Tests/IntegrationTests.cs @@ -17,7 +17,7 @@ public class IntegrationTests { Id = "everything", Name = "Everything", - TransportType = "stdio", + TransportType = McpDotNet.Protocol.Transport.TransportTypes.StdIo, TransportOptions = new Dictionary { ["command"] = "npx", @@ -59,9 +59,11 @@ public class IntegrationTests .Build(); // Create message list - IList messages = new List(); - // Add a system message - messages.Add(new(ChatRole.System, "You are a helpful assistant, helping us test MCP server functionality.")); + IList messages = + [ + // Add a system message + new(ChatRole.System, "You are a helpful assistant, helping us test MCP server functionality."), + ]; // If MCP server provides instructions, add them as an additional system message (you could also add it as a content part) if (!string.IsNullOrEmpty(client.ServerInstructions)) { diff --git a/tests/McpDotNet.Extensions.AI.Tests/McpDotNet.Extensions.AI.Tests.csproj b/tests/McpDotNet.Extensions.AI.Tests/McpDotNet.Extensions.AI.Tests.csproj index 643c3be..40484c0 100644 --- a/tests/McpDotNet.Extensions.AI.Tests/McpDotNet.Extensions.AI.Tests.csproj +++ b/tests/McpDotNet.Extensions.AI.Tests/McpDotNet.Extensions.AI.Tests.csproj @@ -27,6 +27,7 @@ + diff --git a/tests/mcpdotnet.TestServer/Program.cs b/tests/mcpdotnet.TestServer/Program.cs index 3c942ea..eb0c3ed 100644 --- a/tests/mcpdotnet.TestServer/Program.cs +++ b/tests/mcpdotnet.TestServer/Program.cs @@ -108,7 +108,7 @@ internal class Program server.CallToolHandler = async (request, cancellationToken) => { - if (request.Name == "echo") + if (request?.Name == "echo") { if (request.Arguments is null || !request.Arguments.TryGetValue("message", out var message)) { @@ -119,7 +119,7 @@ internal class Program Content = [new Content() { Text = "Echo: " + message.ToString(), Type = "text" }] }; } - else if (request.Name == "sampleLLM") + else if (request?.Name == "sampleLLM") { if (request.Arguments is null || !request.Arguments.TryGetValue("prompt", out var prompt) || @@ -137,14 +137,14 @@ internal class Program } else { - throw new McpServerException($"Unknown tool: {request.Name}"); + throw new McpServerException($"Unknown tool: {request?.Name}"); } }; #endregion #region Resources - List resources = new(); - List resourceContents = new(); + List resources = []; + List resourceContents = []; for (int i = 0; i < 100; ++i) { string uri = $"test://static/resource/{i + 1}"; @@ -216,15 +216,14 @@ internal class Program server.ReadResourceHandler = (request, cancellationToken) => { - if (request.Uri is null) + if (request?.Uri is null) { throw new McpServerException("Missing required argument 'uri'"); } - ResourceContents? contents = resourceContents.FirstOrDefault(r => r.Uri == request.Uri); - if (contents is null) - { - throw new McpServerException("Resource not found"); - } + + ResourceContents contents = resourceContents.FirstOrDefault(r => r.Uri == request.Uri) + ?? throw new McpServerException("Resource not found"); + return Task.FromResult(new ReadResourceResult() { Contents = [contents] @@ -247,8 +246,8 @@ internal class Program { Name = "complex_prompt", Description = "A prompt with arguments", - Arguments = new() - { + Arguments = + [ new PromptArgument() { Name = "temperature", @@ -261,7 +260,7 @@ internal class Program Description = "Output style", Required = false } - } + ] } ] }); @@ -269,8 +268,8 @@ internal class Program server.GetPromptHandler = (request, cancellationToken) => { - List messages = new(); - if (request.Name == "simple_prompt") + List messages = []; + if (request?.Name == "simple_prompt") { messages.Add(new PromptMessage() { @@ -282,7 +281,7 @@ internal class Program } }); } - else if (request.Name == "complex_prompt") + else if (request?.Name == "complex_prompt") { string temperature = request.Arguments?["temperature"]?.ToString() ?? "unknown"; string style = request.Arguments?["style"]?.ToString() ?? "unknown"; @@ -317,7 +316,7 @@ internal class Program } else { - throw new McpServerException($"Unknown prompt: {request.Name}"); + throw new McpServerException($"Unknown prompt: {request?.Name}"); } return Task.FromResult(new GetPromptResult() diff --git a/tests/mcpdotnet.Tests/ClientIntegrationTestFixture.cs b/tests/mcpdotnet.Tests/ClientIntegrationTestFixture.cs index af0db42..8d79ec0 100644 --- a/tests/mcpdotnet.Tests/ClientIntegrationTestFixture.cs +++ b/tests/mcpdotnet.Tests/ClientIntegrationTestFixture.cs @@ -1,5 +1,6 @@ using McpDotNet.Client; using McpDotNet.Configuration; +using McpDotNet.Protocol.Transport; using Microsoft.Extensions.Logging; namespace McpDotNet.Tests; @@ -26,7 +27,7 @@ public class ClientIntegrationTestFixture : IDisposable { Id = "everything", Name = "Everything", - TransportType = "stdio", + TransportType = TransportTypes.StdIo, TransportOptions = new Dictionary { ["command"] = "npx", diff --git a/tests/mcpdotnet.Tests/ClientIntegrationTests.cs b/tests/mcpdotnet.Tests/ClientIntegrationTests.cs index ff922c3..5d53957 100644 --- a/tests/mcpdotnet.Tests/ClientIntegrationTests.cs +++ b/tests/mcpdotnet.Tests/ClientIntegrationTests.cs @@ -1,6 +1,7 @@ using McpDotNet.Client; using McpDotNet.Configuration; using McpDotNet.Protocol.Messages; +using McpDotNet.Protocol.Transport; using McpDotNet.Protocol.Types; namespace McpDotNet.Tests; @@ -151,7 +152,7 @@ public class ClientIntegrationTests : IClassFixture allResources = new(); + List allResources = []; string? cursor = null; do { @@ -267,7 +268,7 @@ public class ClientIntegrationTests : IClassFixture { ["command"] = "npx", @@ -335,7 +336,7 @@ public class ClientIntegrationTests : IClassFixture { ["command"] = "npx", @@ -394,7 +395,7 @@ public class ClientIntegrationTests : IClassFixture { ["command"] = "npx", @@ -429,7 +430,7 @@ public class ClientIntegrationTests : IClassFixture { ["command"] = "npx", @@ -450,7 +451,7 @@ public class ClientIntegrationTests : IClassFixture(), + [], CancellationToken.None ); diff --git a/tests/mcpdotnet.Tests/EverythingSseServerFixture.cs b/tests/mcpdotnet.Tests/EverythingSseServerFixture.cs index dcffe04..ac05c8d 100644 --- a/tests/mcpdotnet.Tests/EverythingSseServerFixture.cs +++ b/tests/mcpdotnet.Tests/EverythingSseServerFixture.cs @@ -4,7 +4,7 @@ namespace McpDotNet.Tests; public class EverythingSseServerFixture : IAsyncDisposable { - private Process _process; + private Process? _process; public async Task StartAsync() { @@ -18,7 +18,8 @@ public class EverythingSseServerFixture : IAsyncDisposable UseShellExecute = false, }; - _process = Process.Start(processStartInfo); + _process = Process.Start(processStartInfo) + ?? throw new InvalidOperationException($"Could not start process for {processStartInfo.FileName} with '{processStartInfo.Arguments}'."); // Wait for the server to start await Task.Delay(10000); @@ -36,7 +37,8 @@ public class EverythingSseServerFixture : IAsyncDisposable UseShellExecute = false }; - using var psProcess = Process.Start(psInfo); + using var psProcess = Process.Start(psInfo) + ?? throw new InvalidOperationException($"Could not start process for {psInfo.FileName} with '{psInfo.Arguments}'."); string containerId = await psProcess.StandardOutput.ReadToEndAsync(); containerId = containerId.Trim(); @@ -50,7 +52,8 @@ public class EverythingSseServerFixture : IAsyncDisposable UseShellExecute = false }; - using var stopProcess = Process.Start(stopInfo); + using var stopProcess = Process.Start(stopInfo) + ?? throw new InvalidOperationException($"Could not start process for {stopInfo.FileName} with '{stopInfo.Arguments}'."); await stopProcess.WaitForExitAsync(); } } diff --git a/tests/mcpdotnet.Tests/McpClientFactoryTests.cs b/tests/mcpdotnet.Tests/McpClientFactoryTests.cs index 27cd9df..75532e9 100644 --- a/tests/mcpdotnet.Tests/McpClientFactoryTests.cs +++ b/tests/mcpdotnet.Tests/McpClientFactoryTests.cs @@ -1,10 +1,10 @@ +using System.Threading.Channels; using McpDotNet.Client; using McpDotNet.Configuration; using McpDotNet.Protocol.Messages; using McpDotNet.Protocol.Transport; using Microsoft.Extensions.Logging.Abstractions; using Moq; -using System.Threading.Channels; namespace McpDotNet.Tests.Client; @@ -23,7 +23,7 @@ public class McpClientFactoryTests { Id = "test-server", Name = "Test Server", - TransportType = "stdio", + TransportType = TransportTypes.StdIo, Location = "/path/to/server", TransportOptions = new Dictionary { @@ -70,7 +70,7 @@ public class McpClientFactoryTests { Id = "test-server", Name = "Test Server", - TransportType = "stdio", + TransportType = TransportTypes.StdIo, Location = "/path/to/server" }; @@ -87,11 +87,11 @@ public class McpClientFactoryTests .Returns(Task.CompletedTask); mockClient.Setup(c => c.IsInitialized).Returns(true); - var factory = new McpClientFactory([config], + var factory = new McpClientFactory([config], _defaultOptions, NullLoggerFactory.Instance, - transportFactoryMethod: _ => mockTransport.Object, - clientFactoryMethod: (_,_,_) => mockClient.Object); + transportFactoryMethod: _ => mockTransport.Object, + clientFactoryMethod: (_, _, _) => mockClient.Object); // Act var client1 = await factory.GetClientAsync("test-server"); @@ -105,7 +105,7 @@ public class McpClientFactoryTests public async Task GetClientAsync_WithInvalidServerId_ThrowsArgumentException() { // Arrange - var factory = new McpClientFactory(Array.Empty(), + var factory = new McpClientFactory(Array.Empty(), _defaultOptions, NullLoggerFactory.Instance); @@ -144,7 +144,7 @@ public class McpClientFactoryTests { Id = "test-server", Name = "Test Server", - TransportType = "stdio", + TransportType = TransportTypes.StdIo, Location = "/path/to/server" }; @@ -164,7 +164,7 @@ public class McpClientFactoryTests var factory = new McpClientFactory([config], _defaultOptions, NullLoggerFactory.Instance, - transportFactoryMethod: _ => mockTransport.Object, + transportFactoryMethod: _ => mockTransport.Object, clientFactoryMethod: (_, _, _) => mockClient.Object); // Act @@ -180,8 +180,8 @@ public class McpClientFactoryTests // Arrange McpServerConfig[] configs = [ - new McpServerConfig { Id = "duplicate", Name = "duplicate", TransportType = "stdio", Location = "/path1" }, - new McpServerConfig { Id = "duplicate", Name = "duplicate", TransportType = "stdio", Location = "/path2" } + new McpServerConfig { Id = "duplicate", Name = "duplicate", TransportType = TransportTypes.StdIo, Location = "/path1" }, + new McpServerConfig { Id = "duplicate", Name = "duplicate", TransportType = TransportTypes.StdIo, Location = "/path2" } ]; // Act & Assert @@ -197,10 +197,10 @@ public class McpClientFactoryTests { Id = "test-server", Name = "Test Server", - TransportType = "sse", + TransportType = TransportTypes.Sse, Location = "http://localhost:8080" }; - + // Create a mock transport var mockTransport = new Mock(); mockTransport.Setup(t => t.ConnectAsync(It.IsAny())) @@ -238,7 +238,7 @@ public class McpClientFactoryTests { Id = "test-server", Name = "Test Server", - TransportType = "sse", + TransportType = TransportTypes.Sse, Location = "http://localhost:8080", TransportOptions = new Dictionary { @@ -260,10 +260,11 @@ public class McpClientFactoryTests // Assert Assert.NotNull(transport); - Assert.True(transport.Options.ConnectionTimeout == TimeSpan.FromSeconds(10)); - Assert.True(transport.Options.MaxReconnectAttempts == 2); - Assert.True(transport.Options.ReconnectDelay == TimeSpan.FromSeconds(5)); - Assert.True(transport.Options.AdditionalHeaders["test"] == "the_header_value"); + Assert.Equal(TimeSpan.FromSeconds(10), transport.Options.ConnectionTimeout); + Assert.Equal(2, transport.Options.MaxReconnectAttempts); + Assert.Equal(TimeSpan.FromSeconds(5), transport.Options.ReconnectDelay); + Assert.NotNull(transport.Options.AdditionalHeaders); + Assert.Equal("the_header_value", transport.Options.AdditionalHeaders["test"]); } [Fact] @@ -274,7 +275,7 @@ public class McpClientFactoryTests { Id = "test-server", Name = "Test Server", - TransportType = "sse", + TransportType = TransportTypes.Sse, Location = "http://localhost:8080" }; @@ -305,7 +306,7 @@ public class McpClientFactoryTests { Id = "test-server", Name = "Test Server", - TransportType = "sse", + TransportType = TransportTypes.Sse, Location = "http://localhost:8080", TransportOptions = new Dictionary { @@ -327,10 +328,11 @@ public class McpClientFactoryTests // Assert Assert.NotNull(transport); - Assert.True(transport.Options.ConnectionTimeout == TimeSpan.FromSeconds(10)); - Assert.True(transport.Options.MaxReconnectAttempts == defaultOptions.MaxReconnectAttempts); - Assert.True(transport.Options.ReconnectDelay == defaultOptions.ReconnectDelay); - Assert.True(transport.Options.AdditionalHeaders["test"] == "the_header_value"); + Assert.Equal(TimeSpan.FromSeconds(10), transport.Options.ConnectionTimeout); + Assert.Equal(defaultOptions.MaxReconnectAttempts, transport.Options.MaxReconnectAttempts); + Assert.Equal(defaultOptions.ReconnectDelay, transport.Options.ReconnectDelay); + Assert.NotNull(transport.Options.AdditionalHeaders); + Assert.Equal("the_header_value", transport.Options.AdditionalHeaders["test"]); } [Theory] @@ -344,7 +346,7 @@ public class McpClientFactoryTests { Id = "test-server", Name = "Test Server", - TransportType = "sse", + TransportType = TransportTypes.Sse, Location = "http://localhost:8080", TransportOptions = new Dictionary { diff --git a/tests/mcpdotnet.Tests/ServerIntegrationTestFixture.cs b/tests/mcpdotnet.Tests/ServerIntegrationTestFixture.cs index 0c395c1..55fa6e2 100644 --- a/tests/mcpdotnet.Tests/ServerIntegrationTestFixture.cs +++ b/tests/mcpdotnet.Tests/ServerIntegrationTestFixture.cs @@ -1,7 +1,7 @@ using McpDotNet.Client; using McpDotNet.Configuration; +using McpDotNet.Protocol.Transport; using Microsoft.Extensions.Logging; -using System.Threading.Tasks; namespace McpDotNet.Tests; @@ -28,7 +28,7 @@ public class ServerIntegrationTestFixture : IDisposable { Id = "test_server", Name = "TestServer", - TransportType = "stdio", + TransportType = TransportTypes.StdIo, TransportOptions = new Dictionary { ["command"] = "TestServer.exe", diff --git a/tests/mcpdotnet.Tests/ServerIntegrationTests.cs b/tests/mcpdotnet.Tests/ServerIntegrationTests.cs index bb7db7c..00b9038 100644 --- a/tests/mcpdotnet.Tests/ServerIntegrationTests.cs +++ b/tests/mcpdotnet.Tests/ServerIntegrationTests.cs @@ -86,7 +86,7 @@ public class ServerIntegrationTests : IClassFixture allResources = new(); + List allResources = []; string? cursor = null; do { diff --git a/tests/mcpdotnet.Tests/SseIntegrationTests.cs b/tests/mcpdotnet.Tests/SseIntegrationTests.cs index 4afa75a..8655bc8 100644 --- a/tests/mcpdotnet.Tests/SseIntegrationTests.cs +++ b/tests/mcpdotnet.Tests/SseIntegrationTests.cs @@ -32,8 +32,8 @@ public class SseIntegrationTests { Id = "test_server", Name = "In-memory Test Server", - TransportType = "sse", - TransportOptions = new Dictionary(), + TransportType = TransportTypes.Sse, + TransportOptions = [], Location = "http://localhost:5000/sse" }; @@ -75,8 +75,8 @@ public class SseIntegrationTests { Id = "everything", Name = "Everything", - TransportType = "sse", - TransportOptions = new Dictionary(), + TransportType = TransportTypes.Sse, + TransportOptions = [], Location = "http://localhost:3001/sse" }; @@ -123,8 +123,8 @@ public class SseIntegrationTests { Id = "everything", Name = "Everything", - TransportType = "sse", - TransportOptions = new Dictionary(), + TransportType = TransportTypes.Sse, + TransportOptions = [], Location = "http://localhost:3001/sse" }; @@ -193,8 +193,8 @@ public class SseIntegrationTests { Id = "test_server", Name = "In-memory Test Server", - TransportType = "sse", - TransportOptions = new Dictionary(), + TransportType = TransportTypes.Sse, + TransportOptions = [], Location = "http://localhost:5000/sse" }; @@ -238,8 +238,8 @@ public class SseIntegrationTests { Id = "test_server", Name = "In-memory Test Server", - TransportType = "sse", - TransportOptions = new Dictionary(), + TransportType = TransportTypes.Sse, + TransportOptions = [], Location = "http://localhost:5000/sse" }; @@ -255,11 +255,13 @@ public class SseIntegrationTests // Wait for SSE connection to be established await server.WaitForConnectionAsync(TimeSpan.FromSeconds(10)); - var receivedNotification = new TaskCompletionSource(); - client.OnNotification("test/notification", async (args) => + var receivedNotification = new TaskCompletionSource(); + client.OnNotification("test/notification", (args) => { - var msg = ((JsonElement)args.Params).GetProperty("message").GetString(); + var msg = ((JsonElement?)args.Params)?.GetProperty("message").GetString(); receivedNotification.SetResult(msg); + + return Task.CompletedTask; }); // Act @@ -291,8 +293,8 @@ public class SseIntegrationTests { Id = "test_server", Name = "In-memory Test Server", - TransportType = "sse", - TransportOptions = new Dictionary(), + TransportType = TransportTypes.Sse, + TransportOptions = [], Location = "http://localhost:5000/sse" }; diff --git a/tests/mcpdotnet.Tests/SseTransportTests.cs b/tests/mcpdotnet.Tests/SseTransportTests.cs index d534461..df759c9 100644 --- a/tests/mcpdotnet.Tests/SseTransportTests.cs +++ b/tests/mcpdotnet.Tests/SseTransportTests.cs @@ -15,7 +15,7 @@ public class SseTransportTests { Id = "test-server", Name = "Test Server", - TransportType = "sse", + TransportType = TransportTypes.Sse, Location = "http://localhost:8080" }; @@ -35,21 +35,22 @@ public class SseTransportTests // Assert Assert.NotNull(transport); - Assert.True(transport.Options.ConnectionTimeout == TimeSpan.FromSeconds(10)); - Assert.True(transport.Options.MaxReconnectAttempts == 2); - Assert.True(transport.Options.ReconnectDelay == TimeSpan.FromSeconds(5)); - Assert.True(transport.Options.AdditionalHeaders["test"] == "header"); + Assert.Equal(TimeSpan.FromSeconds(10), transport.Options.ConnectionTimeout); + Assert.Equal(2, transport.Options.MaxReconnectAttempts); + Assert.Equal(TimeSpan.FromSeconds(5), transport.Options.ReconnectDelay); + Assert.NotNull(transport.Options.AdditionalHeaders); + Assert.Equal("header", transport.Options.AdditionalHeaders["test"]); } [Fact] - public async void SseTransportSendMessageAsync_WithMessageEndpointNotSet_ThrowsException() + public async Task SseTransportSendMessageAsync_WithMessageEndpointNotSet_ThrowsException() { // Arrange var config = new McpServerConfig { Id = "test-server", Name = "Test Server", - TransportType = "sse", + TransportType = TransportTypes.Sse, Location = "http://localhost:8080" }; @@ -69,6 +70,6 @@ public class SseTransportTests // Assert Assert.True(string.IsNullOrEmpty(transport.MessageEndpoint?.ToString())); - await Assert.ThrowsAsync(() => transport.SendMessageAsync(new JsonRpcRequest() { Method = "test"}, CancellationToken.None)); + await Assert.ThrowsAsync(() => transport.SendMessageAsync(new JsonRpcRequest() { Method = "test" }, CancellationToken.None)); } } \ No newline at end of file diff --git a/tests/mcpdotnet.Tests/Utils/TestSseServer.cs b/tests/mcpdotnet.Tests/Utils/TestSseServer.cs index 1bee18e..d49625c 100644 --- a/tests/mcpdotnet.Tests/Utils/TestSseServer.cs +++ b/tests/mcpdotnet.Tests/Utils/TestSseServer.cs @@ -1,9 +1,9 @@ -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Logging; -using System.Collections.Concurrent; +using System.Collections.Concurrent; using System.Net; using System.Text.Json; using McpDotNet.Protocol.Messages; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; namespace McpDotNet.Tests.Utils; @@ -21,7 +21,7 @@ public sealed class TestSseServer : IAsyncDisposable private readonly string _messagePath; // Keep track of all open SSE connections (StreamWriters). - private readonly ConcurrentBag _sseClients = new(); + private readonly ConcurrentBag _sseClients = []; public TestSseServer(int port = 5000, ILogger? logger = null) { @@ -90,13 +90,13 @@ public sealed class TestSseServer : IAsyncDisposable // Handle SSE endpoint if (request.HttpMethod.Equals("GET", StringComparison.OrdinalIgnoreCase) - && request.Url.AbsolutePath.Equals(_endpointPath, StringComparison.OrdinalIgnoreCase)) + && request.Url?.AbsolutePath.Equals(_endpointPath, StringComparison.OrdinalIgnoreCase) == true) { await HandleSseConnectionAsync(context, ct); } // Handle POST /message else if (request.HttpMethod.Equals("POST", StringComparison.OrdinalIgnoreCase) - && request.Url.AbsolutePath.Equals(_messagePath, StringComparison.OrdinalIgnoreCase)) + && request.Url?.AbsolutePath.Equals(_messagePath, StringComparison.OrdinalIgnoreCase) == true) { await HandlePostMessageAsync(context, ct); } @@ -135,7 +135,7 @@ public sealed class TestSseServer : IAsyncDisposable await writer.WriteLineAsync($"data: {_messagePath}"); } await writer.WriteLineAsync(); // blank line to end an SSE message - await writer.FlushAsync(); + await writer.FlushAsync(ct); _logger.LogInformation("New SSE client connected."); _connectionEstablished.TrySetResult(); // Signal connection is ready @@ -149,7 +149,7 @@ public sealed class TestSseServer : IAsyncDisposable _logger.LogDebug("SSE connection alive check"); // Optionally do a periodic no-op to keep connection alive: await writer.WriteLineAsync(": keep-alive"); - await writer.FlushAsync(); + await writer.FlushAsync(ct); await Task.Delay(TimeSpan.FromSeconds(5), ct); } } @@ -187,7 +187,7 @@ public sealed class TestSseServer : IAsyncDisposable try { using var reader = new StreamReader(request.InputStream); - string content = await reader.ReadToEndAsync(); + string content = await reader.ReadToEndAsync(ct); var jsonRpcNotification = JsonSerializer.Deserialize(content); if (jsonRpcNotification != null && jsonRpcNotification.Method != "initialize") @@ -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(); + await writer.FlushAsync(ct); // Then process the message and send the response via SSE if (jsonRpcRequest != null) @@ -245,14 +245,14 @@ public sealed class TestSseServer : IAsyncDisposable } } - private async Task ProcessRequestAsync(JsonRpcRequest jsonRpcRequest) + private static Task ProcessRequestAsync(JsonRpcRequest jsonRpcRequest) { // This is a test server so we just echo back the request - return new JsonRpcResponse + return Task.FromResult(new JsonRpcResponse { Id = jsonRpcRequest.Id, - Result = jsonRpcRequest.Params - }; + Result = jsonRpcRequest.Params! + }); } private async Task SendSseMessageAsync(JsonRpcResponse jsonRpcResponse) @@ -261,7 +261,7 @@ public sealed class TestSseServer : IAsyncDisposable await BroadcastMessageAsync(JsonSerializer.Serialize(jsonRpcResponse)); } - private async Task SendJsonRpcErrorAsync(HttpListenerResponse response, RequestId? id, int code, string message, string? data = null) + private static async Task SendJsonRpcErrorAsync(HttpListenerResponse response, RequestId? id, int code, string message, string? data = null) { var errorResponse = new JsonRpcError { @@ -281,21 +281,10 @@ public sealed class TestSseServer : IAsyncDisposable await writer.WriteAsync(JsonSerializer.Serialize(errorResponse)); } - private async Task HandleInitializationRequest(HttpListenerResponse response, JsonRpcRequest jsonRpcRequest) + private static async Task HandleInitializationRequest(HttpListenerResponse response, JsonRpcRequest jsonRpcRequest) { // We don't need to validate the client's initialization request for the test // Just send back a valid server initialization response - var jsonResponse = new - { - protocolVersion = "2024-11-05", - capabilities = new - { - experimental = (object?)null, - roots = (object?)null, - sampling = (object?)null - } - }; - var jsonRpcResponse = new JsonRpcResponse() { Id = jsonRpcRequest.Id, @@ -353,7 +342,7 @@ public sealed class TestSseServer : IAsyncDisposable public async ValueTask DisposeAsync() { - _cts.Cancel(); + await _cts.CancelAsync(); if (_serverTask != null) {