diff --git a/src/Common/Polyfills/System/Collections/Generic/CollectionExtensions.cs b/src/Common/Polyfills/System/Collections/Generic/CollectionExtensions.cs new file mode 100644 index 0000000..4848134 --- /dev/null +++ b/src/Common/Polyfills/System/Collections/Generic/CollectionExtensions.cs @@ -0,0 +1,19 @@ +namespace System.Collections.Generic; + +internal static class CollectionExtensions +{ + public static TValue? GetValueOrDefault(this IReadOnlyDictionary dictionary, TKey key) + { + return dictionary.GetValueOrDefault(key, default!); + } + + public static TValue GetValueOrDefault(this IReadOnlyDictionary dictionary, TKey key, TValue defaultValue) + { + if (dictionary is null) + { + throw new ArgumentNullException(nameof(dictionary)); + } + + return dictionary.TryGetValue(key, out TValue? value) ? value : defaultValue; + } +} \ No newline at end of file diff --git a/src/Common/Polyfills/System/Diagnostics/CodeAnalysis/NullableAttributes.cs b/src/Common/Polyfills/System/Diagnostics/CodeAnalysis/NullableAttributes.cs new file mode 100644 index 0000000..ef577a9 --- /dev/null +++ b/src/Common/Polyfills/System/Diagnostics/CodeAnalysis/NullableAttributes.cs @@ -0,0 +1,139 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Diagnostics.CodeAnalysis +{ + /// Specifies that null is allowed as an input even if the corresponding type disallows it. + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)] + internal sealed class AllowNullAttribute : Attribute; + + /// Specifies that null is disallowed as an input even if the corresponding type allows it. + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)] + internal sealed class DisallowNullAttribute : Attribute; + + /// Specifies that an output may be null even if the corresponding type disallows it. + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)] + internal sealed class MaybeNullAttribute : Attribute; + + /// Specifies that an output will not be null even if the corresponding type allows it. Specifies that an input argument was not null when the call returns. + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)] + internal sealed class NotNullAttribute : Attribute; + + /// Specifies that when a method returns , the parameter may be null even if the corresponding type disallows it. + [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] + internal sealed class MaybeNullWhenAttribute : Attribute + { + /// Initializes the attribute with the specified return value condition. + /// + /// The return value condition. If the method returns this value, the associated parameter may be null. + /// + public MaybeNullWhenAttribute(bool returnValue) => ReturnValue = returnValue; + + /// Gets the return value condition. + public bool ReturnValue { get; } + } + + /// Specifies that when a method returns , the parameter will not be null even if the corresponding type allows it. + [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] + internal sealed class NotNullWhenAttribute : Attribute + { + /// Initializes the attribute with the specified return value condition. + /// + /// The return value condition. If the method returns this value, the associated parameter will not be null. + /// + public NotNullWhenAttribute(bool returnValue) => ReturnValue = returnValue; + + /// Gets the return value condition. + public bool ReturnValue { get; } + } + + /// Specifies that the output will be non-null if the named parameter is non-null. + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, AllowMultiple = true, Inherited = false)] + internal sealed class NotNullIfNotNullAttribute : Attribute + { + /// Initializes the attribute with the associated parameter name. + /// + /// The associated parameter name. The output will be non-null if the argument to the parameter specified is non-null. + /// + public NotNullIfNotNullAttribute(string parameterName) => ParameterName = parameterName; + + /// Gets the associated parameter name. + public string ParameterName { get; } + } + + /// Applied to a method that will never return under any circumstance. + [AttributeUsage(AttributeTargets.Method, Inherited = false)] + internal sealed class DoesNotReturnAttribute : Attribute; + + /// Specifies that the method will not return if the associated Boolean parameter is passed the specified value. + [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] + internal sealed class DoesNotReturnIfAttribute : Attribute + { + /// Initializes the attribute with the specified parameter value. + /// + /// The condition parameter value. Code after the method will be considered unreachable by diagnostics if the argument to + /// the associated parameter matches this value. + /// + public DoesNotReturnIfAttribute(bool parameterValue) => ParameterValue = parameterValue; + + /// Gets the condition parameter value. + public bool ParameterValue { get; } + } + + /// Specifies that the method or property will ensure that the listed field and property members have not-null values. + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)] + internal sealed class MemberNotNullAttribute : Attribute + { + /// Initializes the attribute with a field or property member. + /// + /// The field or property member that is promised to be not-null. + /// + public MemberNotNullAttribute(string member) => Members = [member]; + + /// Initializes the attribute with the list of field and property members. + /// + /// The list of field and property members that are promised to be not-null. + /// + public MemberNotNullAttribute(params string[] members) => Members = members; + + /// Gets field or property member names. + public string[] Members { get; } + } + + /// Specifies that the method or property will ensure that the listed field and property members have not-null values when returning with the specified return value condition. + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)] + internal sealed class MemberNotNullWhenAttribute : Attribute + { + /// Initializes the attribute with the specified return value condition and a field or property member. + /// + /// The return value condition. If the method returns this value, the associated field or property member will not be null. + /// + /// + /// The field or property member that is promised to be not-null. + /// + public MemberNotNullWhenAttribute(bool returnValue, string member) + { + ReturnValue = returnValue; + Members = [member]; + } + + /// Initializes the attribute with the specified return value condition and list of field and property members. + /// + /// The return value condition. If the method returns this value, the associated field and property members will not be null. + /// + /// + /// The list of field and property members that are promised to be not-null. + /// + public MemberNotNullWhenAttribute(bool returnValue, params string[] members) + { + ReturnValue = returnValue; + Members = members; + } + + /// Gets the return value condition. + public bool ReturnValue { get; } + + /// Gets field or property member names. + public string[] Members { get; } + } +} diff --git a/src/Common/Polyfills/System/Diagnostics/CodeAnalysis/SetsRequiredMembersAttribute.cs b/src/Common/Polyfills/System/Diagnostics/CodeAnalysis/SetsRequiredMembersAttribute.cs new file mode 100644 index 0000000..83d6793 --- /dev/null +++ b/src/Common/Polyfills/System/Diagnostics/CodeAnalysis/SetsRequiredMembersAttribute.cs @@ -0,0 +1,5 @@ +namespace System.Diagnostics.CodeAnalysis +{ + [AttributeUsage(AttributeTargets.Constructor, AllowMultiple = false, Inherited = false)] + internal sealed class SetsRequiredMembersAttribute : Attribute; +} diff --git a/src/Common/Polyfills/System/Diagnostics/ProcessExtensions.cs b/src/Common/Polyfills/System/Diagnostics/ProcessExtensions.cs new file mode 100644 index 0000000..93e88c6 --- /dev/null +++ b/src/Common/Polyfills/System/Diagnostics/ProcessExtensions.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Diagnostics; + +internal static class ProcessExtensions +{ + public static void Kill(this Process process, bool entireProcessTree) + { + _ = entireProcessTree; + process.Kill(); + } +} \ No newline at end of file diff --git a/src/Common/Polyfills/System/IO/TextReaderExtensions.cs b/src/Common/Polyfills/System/IO/TextReaderExtensions.cs new file mode 100644 index 0000000..b5a61df --- /dev/null +++ b/src/Common/Polyfills/System/IO/TextReaderExtensions.cs @@ -0,0 +1,10 @@ +namespace System.IO; + +internal static class TextReaderExtensions +{ + public static Task ReadLineAsync(this TextReader reader, CancellationToken cancellationToken) + { + _ = cancellationToken; + return reader.ReadLineAsync(); + } +} \ No newline at end of file diff --git a/src/Common/Polyfills/System/IO/TextWriterExtensions.cs b/src/Common/Polyfills/System/IO/TextWriterExtensions.cs new file mode 100644 index 0000000..b111948 --- /dev/null +++ b/src/Common/Polyfills/System/IO/TextWriterExtensions.cs @@ -0,0 +1,42 @@ +using System.Runtime.InteropServices; + +namespace System.IO; + +internal static class TextWriterExtensions +{ + public static async Task WriteLineAsync(this TextWriter writer, ReadOnlyMemory value, CancellationToken cancellationToken) + { + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } + + if (value.IsEmpty) + { + return; + } + + cancellationToken.ThrowIfCancellationRequested(); + + if (MemoryMarshal.TryGetString(value, out string str, out int start, out int length) && + start == 0 && length == str.Length) + { + await writer.WriteLineAsync(str).ConfigureAwait(false); + } + else if (MemoryMarshal.TryGetArray(value, out ArraySegment array) && + array.Array is not null && array.Offset == 0 && array.Count == array.Array.Length) + { + await writer.WriteLineAsync(array.Array).ConfigureAwait(false); + } + else + { + await writer.WriteLineAsync(value.ToArray()).ConfigureAwait(false); + } + } + + public static async Task FlushAsync(this TextWriter writer, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + await writer.FlushAsync(); + } +} \ No newline at end of file diff --git a/src/Common/Polyfills/System/Net/Http/HttpClientExtensions.cs b/src/Common/Polyfills/System/Net/Http/HttpClientExtensions.cs new file mode 100644 index 0000000..5b00858 --- /dev/null +++ b/src/Common/Polyfills/System/Net/Http/HttpClientExtensions.cs @@ -0,0 +1,26 @@ +namespace System.Net.Http; + +internal static class HttpClientExtensions +{ + public static async Task ReadAsStreamAsync(this HttpContent content, CancellationToken cancellationToken) + { + if (content is null) + { + throw new ArgumentNullException(nameof(content)); + } + + cancellationToken.ThrowIfCancellationRequested(); + return await content.ReadAsStreamAsync(); + } + + public static async Task ReadAsStringAsync(this HttpContent content, CancellationToken cancellationToken) + { + if (content is null) + { + throw new ArgumentNullException(nameof(content)); + } + + cancellationToken.ThrowIfCancellationRequested(); + return await content.ReadAsStringAsync(); + } +} \ No newline at end of file diff --git a/src/Common/Polyfills/System/Runtime/CompilerServices/CompilerFeatureRequiredAttribute.cs b/src/Common/Polyfills/System/Runtime/CompilerServices/CompilerFeatureRequiredAttribute.cs new file mode 100644 index 0000000..12f3e5d --- /dev/null +++ b/src/Common/Polyfills/System/Runtime/CompilerServices/CompilerFeatureRequiredAttribute.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Runtime.CompilerServices +{ + /// + /// Indicates that compiler support for a particular feature is required for the location where this attribute is applied. + /// + [AttributeUsage(AttributeTargets.All, AllowMultiple = true, Inherited = false)] + internal sealed class CompilerFeatureRequiredAttribute : Attribute + { + public CompilerFeatureRequiredAttribute(string featureName) + { + FeatureName = featureName; + } + + /// + /// The name of the compiler feature. + /// + public string FeatureName { get; } + + /// + /// If true, the compiler can choose to allow access to the location where this attribute is applied if it does not understand . + /// + public bool IsOptional { get; init; } + + /// + /// The used for the required members C# feature. + /// + public const string RequiredMembers = nameof(RequiredMembers); + } +} diff --git a/src/Common/Polyfills/System/Runtime/CompilerServices/IsExternalInit.cs b/src/Common/Polyfills/System/Runtime/CompilerServices/IsExternalInit.cs new file mode 100644 index 0000000..7044309 --- /dev/null +++ b/src/Common/Polyfills/System/Runtime/CompilerServices/IsExternalInit.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.ComponentModel; + +namespace System.Runtime.CompilerServices +{ + /// + /// Reserved to be used by the compiler for tracking metadata. + /// This class should not be used by developers in source code. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + internal static class IsExternalInit; +} diff --git a/src/Common/Polyfills/System/Runtime/CompilerServices/RequiredMemberAttribute.cs b/src/Common/Polyfills/System/Runtime/CompilerServices/RequiredMemberAttribute.cs new file mode 100644 index 0000000..6930dc4 --- /dev/null +++ b/src/Common/Polyfills/System/Runtime/CompilerServices/RequiredMemberAttribute.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.ComponentModel; + +namespace System.Runtime.CompilerServices +{ + /// Specifies that a type has required members or that a member is required. + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = false)] + [EditorBrowsable(EditorBrowsableState.Never)] + internal sealed class RequiredMemberAttribute : Attribute; +} diff --git a/src/Common/Polyfills/System/Threading/CancellationTokenSourceExtensions.cs b/src/Common/Polyfills/System/Threading/CancellationTokenSourceExtensions.cs new file mode 100644 index 0000000..be96fd1 --- /dev/null +++ b/src/Common/Polyfills/System/Threading/CancellationTokenSourceExtensions.cs @@ -0,0 +1,17 @@ +using System.Text; + +namespace System.Threading.Tasks; + +internal static class CancellationTokenSourceExtensions +{ + public static Task CancelAsync(this CancellationTokenSource cancellationTokenSource) + { + if (cancellationTokenSource is null) + { + throw new ArgumentNullException(nameof(cancellationTokenSource)); + } + + cancellationTokenSource.Cancel(); + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/src/Common/Polyfills/System/Threading/Tasks/TaskExtensions.cs b/src/Common/Polyfills/System/Threading/Tasks/TaskExtensions.cs new file mode 100644 index 0000000..6f8e2bf --- /dev/null +++ b/src/Common/Polyfills/System/Threading/Tasks/TaskExtensions.cs @@ -0,0 +1,48 @@ +namespace System.Threading.Tasks; + +internal static class TaskExtensions +{ + public static Task WaitAsync(this Task task, CancellationToken cancellationToken) + { + return WaitAsync(task, Timeout.InfiniteTimeSpan, cancellationToken); + } + + public static async Task WaitAsync(this Task task, CancellationToken cancellationToken) + { + await WaitAsync(task, Timeout.InfiniteTimeSpan, cancellationToken); + return task.Result; + } + + public static async Task WaitAsync(this Task task, TimeSpan timeout, CancellationToken cancellationToken) + { + if (task is null) + { + throw new ArgumentNullException(nameof(task)); + } + + if (timeout < TimeSpan.Zero && timeout != Timeout.InfiniteTimeSpan) + { + throw new ArgumentOutOfRangeException(nameof(timeout)); + } + + if (!task.IsCompleted) + { + cancellationToken.ThrowIfCancellationRequested(); + + using CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + cts.CancelAfter(timeout); + + var cancellationTask = new TaskCompletionSource(); + using var _ = cts.Token.Register(tcs => ((TaskCompletionSource)tcs!).TrySetResult(true), cancellationTask); + await Task.WhenAny(task, cancellationTask.Task).ConfigureAwait(false); + + if (!task.IsCompleted) + { + cancellationToken.ThrowIfCancellationRequested(); + throw new TimeoutException(); + } + } + + await task.ConfigureAwait(false); + } +} \ No newline at end of file diff --git a/src/McpDotNet.Extensions.AI/McpDotNet.Extensions.AI.csproj b/src/McpDotNet.Extensions.AI/McpDotNet.Extensions.AI.csproj index 49671fd..b7134ee 100644 --- a/src/McpDotNet.Extensions.AI/McpDotNet.Extensions.AI.csproj +++ b/src/McpDotNet.Extensions.AI/McpDotNet.Extensions.AI.csproj @@ -1,9 +1,10 @@  - net8.0 + net8.0;netstandard2.0 enable enable + Latest @@ -35,9 +36,14 @@ + + - + + + + diff --git a/src/McpDotNet.Extensions.AI/McpSessionScope.cs b/src/McpDotNet.Extensions.AI/McpSessionScope.cs index b37fcaa..cf5bd5e 100644 --- a/src/McpDotNet.Extensions.AI/McpSessionScope.cs +++ b/src/McpDotNet.Extensions.AI/McpSessionScope.cs @@ -45,7 +45,7 @@ public class McpSessionScope : IAsyncDisposable scope.Tools = tools.Tools.Select(t => new McpAIFunction(t, client)).ToList(); if (!string.IsNullOrEmpty(client.ServerInstructions)) { - scope.ServerInstructions = [client.ServerInstructions]; + scope.ServerInstructions = [client.ServerInstructions!]; } return scope; } diff --git a/src/mcpdotnet/Client/McpClientFactory.cs b/src/mcpdotnet/Client/McpClientFactory.cs index 34fb2c2..a1d5472 100644 --- a/src/mcpdotnet/Client/McpClientFactory.cs +++ b/src/mcpdotnet/Client/McpClientFactory.cs @@ -2,6 +2,7 @@ using McpDotNet.Logging; using McpDotNet.Protocol.Transport; using Microsoft.Extensions.Logging; +using System.Runtime.InteropServices; namespace McpDotNet.Client; /// @@ -139,7 +140,7 @@ public class McpClientFactory if (string.IsNullOrEmpty(command)) return config.Location!; - return OperatingSystem.IsWindows() ? "cmd.exe" : command; + return RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "cmd.exe" : command!; } /// @@ -152,13 +153,13 @@ public class McpClientFactory // If the command is empty or already contains cmd.exe, we don't need to do anything var command = config.TransportOptions?.GetValueOrDefault("command"); - if (string.IsNullOrEmpty(command) || !OperatingSystem.IsWindows()) + if (string.IsNullOrEmpty(command) || !RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { return; } // On Windows, we need to wrap non-shell commands with cmd.exe /c - if (command.Contains("cmd.exe", StringComparison.OrdinalIgnoreCase)) + if (command!.IndexOf("cmd.exe", StringComparison.OrdinalIgnoreCase) >= 0) { _logger.SkippingShellWrapper(endpointName); return; diff --git a/src/mcpdotnet/Configuration/McpServerBuilderExtensions.Tools.cs b/src/mcpdotnet/Configuration/McpServerBuilderExtensions.Tools.cs index c2feb80..43b3368 100644 --- a/src/mcpdotnet/Configuration/McpServerBuilderExtensions.Tools.cs +++ b/src/mcpdotnet/Configuration/McpServerBuilderExtensions.Tools.cs @@ -37,8 +37,7 @@ 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) { - ArgumentNullException.ThrowIfNull(toolTypes); - if (toolTypes.Length == 0) + if (toolTypes is null || toolTypes.Length == 0) throw new ArgumentException("At least one tool type must be provided.", nameof(toolTypes)); var tools = new List(); diff --git a/src/mcpdotnet/Protocol/Transport/PasteArguments.cs b/src/mcpdotnet/Protocol/Transport/PasteArguments.cs new file mode 100644 index 0000000..be4750c --- /dev/null +++ b/src/mcpdotnet/Protocol/Transport/PasteArguments.cs @@ -0,0 +1,102 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// Copied from: +// https://github.com/dotnet/runtime/blob/d2650b6ae7023a2d9d2c74c56116f1f18472ab04/src/libraries/System.Private.CoreLib/src/System/PasteArguments.cs +// and changed from using ValueStringBuilder to StringBuilder. + +using System.Text; + +namespace McpDotNet.Protocol.Transport +{ + internal static partial class PasteArguments + { + internal static void AppendArgument(StringBuilder stringBuilder, string argument) + { + if (stringBuilder.Length != 0) + { + stringBuilder.Append(' '); + } + + // Parsing rules for non-argv[0] arguments: + // - Backslash is a normal character except followed by a quote. + // - 2N backslashes followed by a quote ==> N literal backslashes followed by unescaped quote + // - 2N+1 backslashes followed by a quote ==> N literal backslashes followed by a literal quote + // - Parsing stops at first whitespace outside of quoted region. + // - (post 2008 rule): A closing quote followed by another quote ==> literal quote, and parsing remains in quoting mode. + if (argument.Length != 0 && ContainsNoWhitespaceOrQuotes(argument)) + { + // Simple case - no quoting or changes needed. + stringBuilder.Append(argument); + } + else + { + stringBuilder.Append(Quote); + int idx = 0; + while (idx < argument.Length) + { + char c = argument[idx++]; + if (c == Backslash) + { + int numBackSlash = 1; + while (idx < argument.Length && argument[idx] == Backslash) + { + idx++; + numBackSlash++; + } + + if (idx == argument.Length) + { + // We'll emit an end quote after this so must double the number of backslashes. + stringBuilder.Append(Backslash, numBackSlash * 2); + } + else if (argument[idx] == Quote) + { + // Backslashes will be followed by a quote. Must double the number of backslashes. + stringBuilder.Append(Backslash, numBackSlash * 2 + 1); + stringBuilder.Append(Quote); + idx++; + } + else + { + // Backslash will not be followed by a quote, so emit as normal characters. + stringBuilder.Append(Backslash, numBackSlash); + } + + continue; + } + + if (c == Quote) + { + // Escape the quote so it appears as a literal. This also guarantees that we won't end up generating a closing quote followed + // by another quote (which parses differently pre-2008 vs. post-2008.) + stringBuilder.Append(Backslash); + stringBuilder.Append(Quote); + continue; + } + + stringBuilder.Append(c); + } + + stringBuilder.Append(Quote); + } + } + + private static bool ContainsNoWhitespaceOrQuotes(string s) + { + for (int i = 0; i < s.Length; i++) + { + char c = s[i]; + if (char.IsWhiteSpace(c) || c == Quote) + { + return false; + } + } + + return true; + } + + private const char Quote = '\"'; + private const char Backslash = '\\'; + } +} \ No newline at end of file diff --git a/src/mcpdotnet/Protocol/Transport/SseClientTransport.cs b/src/mcpdotnet/Protocol/Transport/SseClientTransport.cs index 4340fde..5541e64 100644 --- a/src/mcpdotnet/Protocol/Transport/SseClientTransport.cs +++ b/src/mcpdotnet/Protocol/Transport/SseClientTransport.cs @@ -64,13 +64,7 @@ public sealed class SseClientTransport : TransportBase, IClientTransport _logger.TransportReadingMessages(EndpointName); - await Task.WhenAny( - _connectionEstablished.Task, - Task.Delay(_options.ConnectionTimeout, cancellationToken) - ); - - if (!IsConnected) - throw new TimeoutException("Failed to receive endpoint event"); + await _connectionEstablished.Task.WaitAsync(_options.ConnectionTimeout, cancellationToken); } catch (McpTransportException) { diff --git a/src/mcpdotnet/Protocol/Transport/StdioClientTransport.cs b/src/mcpdotnet/Protocol/Transport/StdioClientTransport.cs index 7a594ee..c05f190 100644 --- a/src/mcpdotnet/Protocol/Transport/StdioClientTransport.cs +++ b/src/mcpdotnet/Protocol/Transport/StdioClientTransport.cs @@ -1,10 +1,12 @@ -using System.Diagnostics; -using System.Text.Json; -using McpDotNet.Configuration; +using McpDotNet.Configuration; using McpDotNet.Logging; using McpDotNet.Protocol.Messages; using McpDotNet.Utils.Json; using Microsoft.Extensions.Logging; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Text; +using System.Text.Json; namespace McpDotNet.Protocol.Transport; @@ -61,28 +63,31 @@ public sealed class StdioClientTransport : TransportBase, IClientTransport RedirectStandardInput = true, RedirectStandardOutput = true, RedirectStandardError = true, - CreateNoWindow = OperatingSystem.IsWindows(), + CreateNoWindow = RuntimeInformation.IsOSPlatform(OSPlatform.Windows), WorkingDirectory = _options.WorkingDirectory ?? Environment.CurrentDirectory, }; - if (_options.Arguments != null) + if (_options.Arguments is { Length: > 0 }) { + StringBuilder argsBuilder = new(); foreach (var arg in _options.Arguments) { - startInfo.ArgumentList.Add(arg); + PasteArguments.AppendArgument(argsBuilder, arg); } + + startInfo.Arguments = argsBuilder.ToString(); } if (_options.EnvironmentVariables != null) { - foreach (var (key, value) in _options.EnvironmentVariables) + foreach (var entry in _options.EnvironmentVariables) { - startInfo.Environment[key] = value; + startInfo.Environment[entry.Key] = entry.Value; } } _logger.CreateProcessForTransport(EndpointName, _options.Command, - string.Join(", ", startInfo.ArgumentList), string.Join(", ", startInfo.Environment.Select(kvp => kvp.Key + "=" + kvp.Value)), + startInfo.Arguments, string.Join(", ", startInfo.Environment.Select(kvp => kvp.Key + "=" + kvp.Value)), startInfo.WorkingDirectory, _options.ShutdownTimeout.ToString()); _process = new Process { StartInfo = startInfo }; @@ -102,9 +107,8 @@ public sealed class StdioClientTransport : TransportBase, IClientTransport _processStarted = true; _process.BeginErrorReadLine(); - // Start reading messages in the background - _readTask = Task.Run(async () => await ReadMessagesAsync(_shutdownCts.Token)); + _readTask = Task.Run(async () => await ReadMessagesAsync(_shutdownCts.Token), CancellationToken.None); _logger.TransportReadingMessages(EndpointName); SetConnected(true); diff --git a/src/mcpdotnet/Protocol/Transport/StdioServerTransport.cs b/src/mcpdotnet/Protocol/Transport/StdioServerTransport.cs index f3744a9..9be51e7 100644 --- a/src/mcpdotnet/Protocol/Transport/StdioServerTransport.cs +++ b/src/mcpdotnet/Protocol/Transport/StdioServerTransport.cs @@ -48,7 +48,7 @@ public sealed class StdioServerTransport : TransportBase, IServerTransport { _shutdownCts = new CancellationTokenSource(); - _readTask = Task.Run(async () => await ReadMessagesAsync(_shutdownCts.Token)); + _readTask = Task.Run(async () => await ReadMessagesAsync(_shutdownCts.Token), CancellationToken.None); SetConnected(true); @@ -79,7 +79,7 @@ public sealed class StdioServerTransport : TransportBase, IServerTransport using (Console.OpenStandardOutput()) { await Console.Out.WriteLineAsync(json.AsMemory(), cancellationToken); - await Console.Out.FlushAsync(); + await Console.Out.FlushAsync(cancellationToken); } _logger.TransportSentMessage(EndpointName, id); diff --git a/src/mcpdotnet/Shared/McpJsonRpcEndpoint.cs b/src/mcpdotnet/Shared/McpJsonRpcEndpoint.cs index 0e5cfa5..c55d87d 100644 --- a/src/mcpdotnet/Shared/McpJsonRpcEndpoint.cs +++ b/src/mcpdotnet/Shared/McpJsonRpcEndpoint.cs @@ -71,7 +71,7 @@ internal abstract class McpJsonRpcEndpoint // Fire and forget the message handling task to avoid blocking the transport // If awaiting the task, the transport will not be able to read more messages, // which could lead to a deadlock if the handler sends a message back - FireAndForget(Task.Run(() => HandleMessageAsync(message, cancellationToken)), message); + FireAndForget(Task.Run(() => HandleMessageAsync(message, cancellationToken), CancellationToken.None), message); } } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) @@ -351,7 +351,10 @@ internal abstract class McpJsonRpcEndpoint /// Handler to be called when a request with specified method identifier is received protected void SetRequestHandler(string method, Func> handler) { - ArgumentNullException.ThrowIfNull(handler); + if (handler is null) + { + throw new ArgumentNullException(nameof(handler)); + } _requestHandlers[method] = async (request) => { @@ -397,9 +400,9 @@ internal abstract class McpJsonRpcEndpoint } // Complete all pending requests with cancellation - foreach (var (_, tcs) in _pendingRequests) + foreach (var entry in _pendingRequests) { - tcs.TrySetCanceled(); + entry.Value.TrySetCanceled(); } _pendingRequests.Clear(); diff --git a/src/mcpdotnet/mcpdotnet.csproj b/src/mcpdotnet/mcpdotnet.csproj index 26e8189..797d83e 100644 --- a/src/mcpdotnet/mcpdotnet.csproj +++ b/src/mcpdotnet/mcpdotnet.csproj @@ -1,9 +1,10 @@  - net8.0 + net8.0;netstandard2.0 enable - enable + enable + Latest @@ -35,9 +36,16 @@ - - - + + + + + + + + + + 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..f027a99 100644 --- a/tests/McpDotNet.Extensions.AI.Tests/McpDotNet.Extensions.AI.Tests.csproj +++ b/tests/McpDotNet.Extensions.AI.Tests/McpDotNet.Extensions.AI.Tests.csproj @@ -4,6 +4,7 @@ net8.0 enable enable + Latest false true diff --git a/tests/mcpdotnet.Tests/mcpdotnet.Tests.csproj b/tests/mcpdotnet.Tests/mcpdotnet.Tests.csproj index 49b67c4..00cb111 100644 --- a/tests/mcpdotnet.Tests/mcpdotnet.Tests.csproj +++ b/tests/mcpdotnet.Tests/mcpdotnet.Tests.csproj @@ -4,6 +4,7 @@ net8.0 enable enable + Latest false true