Enable netfx testing. (#588)
* Enable netfx testing. * Skip impacted tests and re-enable netfx testing. * Remove redundant comment. * Remove unnecessary project configuration. * Simplify test server copying.
This commit is contained in:
parent
c0440760ac
commit
aa75f6f37e
32 changed files with 1940 additions and 53 deletions
|
|
@ -60,7 +60,7 @@
|
||||||
<PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="$(System9Version)" />
|
<PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="$(System9Version)" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Options" Version="$(System9Version)" />
|
<PackageVersion Include="Microsoft.Extensions.Options" Version="$(System9Version)" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.TimeProvider.Testing" Version="9.5.0" />
|
<PackageVersion Include="Microsoft.Extensions.TimeProvider.Testing" Version="9.5.0" />
|
||||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.14.0" />
|
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||||
<PackageVersion Include="Moq" Version="4.20.72" />
|
<PackageVersion Include="Moq" Version="4.20.72" />
|
||||||
<PackageVersion Include="OpenTelemetry" Version="1.12.0" />
|
<PackageVersion Include="OpenTelemetry" Version="1.12.0" />
|
||||||
<PackageVersion Include="OpenTelemetry.Exporter.InMemory" Version="1.12.0" />
|
<PackageVersion Include="OpenTelemetry.Exporter.InMemory" Version="1.12.0" />
|
||||||
|
|
@ -75,8 +75,8 @@
|
||||||
<PackageVersion Include="Serilog.Sinks.File" Version="7.0.0" />
|
<PackageVersion Include="Serilog.Sinks.File" Version="7.0.0" />
|
||||||
<PackageVersion Include="Serilog" Version="4.3.0" />
|
<PackageVersion Include="Serilog" Version="4.3.0" />
|
||||||
<PackageVersion Include="System.Linq.AsyncEnumerable" Version="$(System10Version)" />
|
<PackageVersion Include="System.Linq.AsyncEnumerable" Version="$(System10Version)" />
|
||||||
<PackageVersion Include="xunit.v3" Version="2.0.2" />
|
<PackageVersion Include="xunit.v3" Version="2.0.3" />
|
||||||
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.0" />
|
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.1" />
|
||||||
<PackageVersion Include="System.Net.Http" Version="4.3.4" />
|
<PackageVersion Include="System.Net.Http" Version="4.3.4" />
|
||||||
<PackageVersion Include="JsonSchema.Net" Version="7.3.4" />
|
<PackageVersion Include="JsonSchema.Net" Version="7.3.4" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
|
||||||
1395
src/Common/CancellableStreamReader/CancellableStreamReader.cs
Normal file
1395
src/Common/CancellableStreamReader/CancellableStreamReader.cs
Normal file
File diff suppressed because it is too large
Load diff
15
src/Common/CancellableStreamReader/TextReaderExtensions.cs
Normal file
15
src/Common/CancellableStreamReader/TextReaderExtensions.cs
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
namespace System.IO;
|
||||||
|
|
||||||
|
internal static class TextReaderExtensions
|
||||||
|
{
|
||||||
|
public static ValueTask<string> ReadLineAsync(this TextReader reader, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (reader is CancellableStreamReader cancellableReader)
|
||||||
|
{
|
||||||
|
return cancellableReader.ReadLineAsync(cancellationToken)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
return new ValueTask<string>(reader.ReadLineAsync());
|
||||||
|
}
|
||||||
|
}
|
||||||
317
src/Common/CancellableStreamReader/ValueStringBuilder.cs
Normal file
317
src/Common/CancellableStreamReader/ValueStringBuilder.cs
Normal file
|
|
@ -0,0 +1,317 @@
|
||||||
|
// Licensed to the .NET Foundation under one or more agreements.
|
||||||
|
// The .NET Foundation licenses this file to you under the MIT license.
|
||||||
|
|
||||||
|
using System.Buffers;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace System.Text
|
||||||
|
{
|
||||||
|
internal ref partial struct ValueStringBuilder
|
||||||
|
{
|
||||||
|
private char[]? _arrayToReturnToPool;
|
||||||
|
private Span<char> _chars;
|
||||||
|
private int _pos;
|
||||||
|
|
||||||
|
public ValueStringBuilder(Span<char> initialBuffer)
|
||||||
|
{
|
||||||
|
_arrayToReturnToPool = null;
|
||||||
|
_chars = initialBuffer;
|
||||||
|
_pos = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ValueStringBuilder(int initialCapacity)
|
||||||
|
{
|
||||||
|
_arrayToReturnToPool = ArrayPool<char>.Shared.Rent(initialCapacity);
|
||||||
|
_chars = _arrayToReturnToPool;
|
||||||
|
_pos = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Length
|
||||||
|
{
|
||||||
|
get => _pos;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
Debug.Assert(value >= 0);
|
||||||
|
Debug.Assert(value <= _chars.Length);
|
||||||
|
_pos = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Capacity => _chars.Length;
|
||||||
|
|
||||||
|
public void EnsureCapacity(int capacity)
|
||||||
|
{
|
||||||
|
// This is not expected to be called this with negative capacity
|
||||||
|
Debug.Assert(capacity >= 0);
|
||||||
|
|
||||||
|
// If the caller has a bug and calls this with negative capacity, make sure to call Grow to throw an exception.
|
||||||
|
if ((uint)capacity > (uint)_chars.Length)
|
||||||
|
Grow(capacity - _pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get a pinnable reference to the builder.
|
||||||
|
/// Does not ensure there is a null char after <see cref="Length"/>
|
||||||
|
/// This overload is pattern matched in the C# 7.3+ compiler so you can omit
|
||||||
|
/// the explicit method call, and write eg "fixed (char* c = builder)"
|
||||||
|
/// </summary>
|
||||||
|
public ref char GetPinnableReference()
|
||||||
|
{
|
||||||
|
return ref MemoryMarshal.GetReference(_chars);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get a pinnable reference to the builder.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="terminate">Ensures that the builder has a null char after <see cref="Length"/></param>
|
||||||
|
public ref char GetPinnableReference(bool terminate)
|
||||||
|
{
|
||||||
|
if (terminate)
|
||||||
|
{
|
||||||
|
EnsureCapacity(Length + 1);
|
||||||
|
_chars[Length] = '\0';
|
||||||
|
}
|
||||||
|
return ref MemoryMarshal.GetReference(_chars);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ref char this[int index]
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
Debug.Assert(index < _pos);
|
||||||
|
return ref _chars[index];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
string s = _chars.Slice(0, _pos).ToString();
|
||||||
|
Dispose();
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Returns the underlying storage of the builder.</summary>
|
||||||
|
public Span<char> RawChars => _chars;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a span around the contents of the builder.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="terminate">Ensures that the builder has a null char after <see cref="Length"/></param>
|
||||||
|
public ReadOnlySpan<char> AsSpan(bool terminate)
|
||||||
|
{
|
||||||
|
if (terminate)
|
||||||
|
{
|
||||||
|
EnsureCapacity(Length + 1);
|
||||||
|
_chars[Length] = '\0';
|
||||||
|
}
|
||||||
|
return _chars.Slice(0, _pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ReadOnlySpan<char> AsSpan() => _chars.Slice(0, _pos);
|
||||||
|
public ReadOnlySpan<char> AsSpan(int start) => _chars.Slice(start, _pos - start);
|
||||||
|
public ReadOnlySpan<char> AsSpan(int start, int length) => _chars.Slice(start, length);
|
||||||
|
|
||||||
|
public bool TryCopyTo(Span<char> destination, out int charsWritten)
|
||||||
|
{
|
||||||
|
if (_chars.Slice(0, _pos).TryCopyTo(destination))
|
||||||
|
{
|
||||||
|
charsWritten = _pos;
|
||||||
|
Dispose();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
charsWritten = 0;
|
||||||
|
Dispose();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Insert(int index, char value, int count)
|
||||||
|
{
|
||||||
|
if (_pos > _chars.Length - count)
|
||||||
|
{
|
||||||
|
Grow(count);
|
||||||
|
}
|
||||||
|
|
||||||
|
int remaining = _pos - index;
|
||||||
|
_chars.Slice(index, remaining).CopyTo(_chars.Slice(index + count));
|
||||||
|
_chars.Slice(index, count).Fill(value);
|
||||||
|
_pos += count;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Insert(int index, string? s)
|
||||||
|
{
|
||||||
|
if (s == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int count = s.Length;
|
||||||
|
|
||||||
|
if (_pos > (_chars.Length - count))
|
||||||
|
{
|
||||||
|
Grow(count);
|
||||||
|
}
|
||||||
|
|
||||||
|
int remaining = _pos - index;
|
||||||
|
_chars.Slice(index, remaining).CopyTo(_chars.Slice(index + count));
|
||||||
|
s
|
||||||
|
#if !NET
|
||||||
|
.AsSpan()
|
||||||
|
#endif
|
||||||
|
.CopyTo(_chars.Slice(index));
|
||||||
|
_pos += count;
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public void Append(char c)
|
||||||
|
{
|
||||||
|
int pos = _pos;
|
||||||
|
Span<char> chars = _chars;
|
||||||
|
if ((uint)pos < (uint)chars.Length)
|
||||||
|
{
|
||||||
|
chars[pos] = c;
|
||||||
|
_pos = pos + 1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
GrowAndAppend(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public void Append(string? s)
|
||||||
|
{
|
||||||
|
if (s == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int pos = _pos;
|
||||||
|
if (s.Length == 1 && (uint)pos < (uint)_chars.Length) // very common case, e.g. appending strings from NumberFormatInfo like separators, percent symbols, etc.
|
||||||
|
{
|
||||||
|
_chars[pos] = s[0];
|
||||||
|
_pos = pos + 1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
AppendSlow(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AppendSlow(string s)
|
||||||
|
{
|
||||||
|
int pos = _pos;
|
||||||
|
if (pos > _chars.Length - s.Length)
|
||||||
|
{
|
||||||
|
Grow(s.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
s
|
||||||
|
#if !NET
|
||||||
|
.AsSpan()
|
||||||
|
#endif
|
||||||
|
.CopyTo(_chars.Slice(pos));
|
||||||
|
_pos += s.Length;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Append(char c, int count)
|
||||||
|
{
|
||||||
|
if (_pos > _chars.Length - count)
|
||||||
|
{
|
||||||
|
Grow(count);
|
||||||
|
}
|
||||||
|
|
||||||
|
Span<char> dst = _chars.Slice(_pos, count);
|
||||||
|
for (int i = 0; i < dst.Length; i++)
|
||||||
|
{
|
||||||
|
dst[i] = c;
|
||||||
|
}
|
||||||
|
_pos += count;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Append(scoped ReadOnlySpan<char> value)
|
||||||
|
{
|
||||||
|
int pos = _pos;
|
||||||
|
if (pos > _chars.Length - value.Length)
|
||||||
|
{
|
||||||
|
Grow(value.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
value.CopyTo(_chars.Slice(_pos));
|
||||||
|
_pos += value.Length;
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public Span<char> AppendSpan(int length)
|
||||||
|
{
|
||||||
|
int origPos = _pos;
|
||||||
|
if (origPos > _chars.Length - length)
|
||||||
|
{
|
||||||
|
Grow(length);
|
||||||
|
}
|
||||||
|
|
||||||
|
_pos = origPos + length;
|
||||||
|
return _chars.Slice(origPos, length);
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||||
|
private void GrowAndAppend(char c)
|
||||||
|
{
|
||||||
|
Grow(1);
|
||||||
|
Append(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resize the internal buffer either by doubling current buffer size or
|
||||||
|
/// by adding <paramref name="additionalCapacityBeyondPos"/> to
|
||||||
|
/// <see cref="_pos"/> whichever is greater.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="additionalCapacityBeyondPos">
|
||||||
|
/// Number of chars requested beyond current position.
|
||||||
|
/// </param>
|
||||||
|
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||||
|
private void Grow(int additionalCapacityBeyondPos)
|
||||||
|
{
|
||||||
|
Debug.Assert(additionalCapacityBeyondPos > 0);
|
||||||
|
Debug.Assert(_pos > _chars.Length - additionalCapacityBeyondPos, "Grow called incorrectly, no resize is needed.");
|
||||||
|
|
||||||
|
const uint ArrayMaxLength = 0x7FFFFFC7; // same as Array.MaxLength
|
||||||
|
|
||||||
|
// Increase to at least the required size (_pos + additionalCapacityBeyondPos), but try
|
||||||
|
// to double the size if possible, bounding the doubling to not go beyond the max array length.
|
||||||
|
int newCapacity = (int)Math.Max(
|
||||||
|
(uint)(_pos + additionalCapacityBeyondPos),
|
||||||
|
Math.Min((uint)_chars.Length * 2, ArrayMaxLength));
|
||||||
|
|
||||||
|
// Make sure to let Rent throw an exception if the caller has a bug and the desired capacity is negative.
|
||||||
|
// This could also go negative if the actual required length wraps around.
|
||||||
|
char[] poolArray = ArrayPool<char>.Shared.Rent(newCapacity);
|
||||||
|
|
||||||
|
_chars.Slice(0, _pos).CopyTo(poolArray);
|
||||||
|
|
||||||
|
char[]? toReturn = _arrayToReturnToPool;
|
||||||
|
_chars = _arrayToReturnToPool = poolArray;
|
||||||
|
if (toReturn != null)
|
||||||
|
{
|
||||||
|
ArrayPool<char>.Shared.Return(toReturn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
char[]? toReturn = _arrayToReturnToPool;
|
||||||
|
this = default; // for safety, to avoid using pooled array if this instance is erroneously appended to again
|
||||||
|
if (toReturn != null)
|
||||||
|
{
|
||||||
|
ArrayPool<char>.Shared.Return(toReturn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
using ModelContextProtocol;
|
using ModelContextProtocol;
|
||||||
using System.Buffers;
|
using System.Buffers;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
namespace System.IO;
|
namespace System.IO;
|
||||||
|
|
||||||
|
|
@ -33,4 +34,31 @@ internal static class StreamExtensions
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static ValueTask<int> ReadAsync(this Stream stream, Memory<byte> buffer, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
Throw.IfNull(stream);
|
||||||
|
if (MemoryMarshal.TryGetArray(buffer, out ArraySegment<byte> segment))
|
||||||
|
{
|
||||||
|
return new ValueTask<int>(stream.ReadAsync(segment.Array, segment.Offset, segment.Count, cancellationToken));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return ReadAsyncCore(stream, buffer, cancellationToken);
|
||||||
|
static async ValueTask<int> ReadAsyncCore(Stream stream, Memory<byte> buffer, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
byte[] array = ArrayPool<byte>.Shared.Rent(buffer.Length);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
int bytesRead = await stream.ReadAsync(array, 0, buffer.Length, cancellationToken).ConfigureAwait(false);
|
||||||
|
array.AsSpan(0, bytesRead).CopyTo(buffer.Span);
|
||||||
|
return bytesRead;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
ArrayPool<byte>.Shared.Return(array);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
namespace System.IO;
|
|
||||||
|
|
||||||
internal static class TextReaderExtensions
|
|
||||||
{
|
|
||||||
public static Task<string> ReadLineAsync(this TextReader reader, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
|
||||||
return reader.ReadLineAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -25,6 +25,15 @@ internal static class Throw
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void IfNegative(int arg, [CallerArgumentExpression(nameof(arg))] string? parameterName = null)
|
||||||
|
{
|
||||||
|
if (arg < 0)
|
||||||
|
{
|
||||||
|
Throw(parameterName);
|
||||||
|
static void Throw(string? parameterName) => throw new ArgumentOutOfRangeException(parameterName, "must not be negative.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[DoesNotReturn]
|
[DoesNotReturn]
|
||||||
private static void ThrowArgumentNullOrWhiteSpaceException(string? parameterName)
|
private static void ThrowArgumentNullOrWhiteSpaceException(string? parameterName)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ internal sealed class StdioClientSessionTransport : StreamClientSessionTransport
|
||||||
private int _cleanedUp = 0;
|
private int _cleanedUp = 0;
|
||||||
|
|
||||||
public StdioClientSessionTransport(StdioClientTransportOptions options, Process process, string endpointName, Queue<string> stderrRollingLog, ILoggerFactory? loggerFactory)
|
public StdioClientSessionTransport(StdioClientTransportOptions options, Process process, string endpointName, Queue<string> stderrRollingLog, ILoggerFactory? loggerFactory)
|
||||||
: base(process.StandardInput, process.StandardOutput, endpointName, loggerFactory)
|
: base(process.StandardInput.BaseStream, process.StandardOutput.BaseStream, encoding: null, endpointName, loggerFactory)
|
||||||
{
|
{
|
||||||
_process = process;
|
_process = process;
|
||||||
_options = options;
|
_options = options;
|
||||||
|
|
|
||||||
|
|
@ -69,8 +69,6 @@ public sealed partial class StdioClientTransport : IClientTransport
|
||||||
{
|
{
|
||||||
LogTransportConnecting(logger, endpointName);
|
LogTransportConnecting(logger, endpointName);
|
||||||
|
|
||||||
UTF8Encoding noBomUTF8 = new(encoderShouldEmitUTF8Identifier: false);
|
|
||||||
|
|
||||||
ProcessStartInfo startInfo = new()
|
ProcessStartInfo startInfo = new()
|
||||||
{
|
{
|
||||||
FileName = command,
|
FileName = command,
|
||||||
|
|
@ -80,10 +78,10 @@ public sealed partial class StdioClientTransport : IClientTransport
|
||||||
UseShellExecute = false,
|
UseShellExecute = false,
|
||||||
CreateNoWindow = true,
|
CreateNoWindow = true,
|
||||||
WorkingDirectory = _options.WorkingDirectory ?? Environment.CurrentDirectory,
|
WorkingDirectory = _options.WorkingDirectory ?? Environment.CurrentDirectory,
|
||||||
StandardOutputEncoding = noBomUTF8,
|
StandardOutputEncoding = StreamClientSessionTransport.NoBomUtf8Encoding,
|
||||||
StandardErrorEncoding = noBomUTF8,
|
StandardErrorEncoding = StreamClientSessionTransport.NoBomUtf8Encoding,
|
||||||
#if NET
|
#if NET
|
||||||
StandardInputEncoding = noBomUTF8,
|
StandardInputEncoding = StreamClientSessionTransport.NoBomUtf8Encoding,
|
||||||
#endif
|
#endif
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -164,7 +162,7 @@ public sealed partial class StdioClientTransport : IClientTransport
|
||||||
Encoding originalInputEncoding = Console.InputEncoding;
|
Encoding originalInputEncoding = Console.InputEncoding;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Console.InputEncoding = noBomUTF8;
|
Console.InputEncoding = StreamClientSessionTransport.NoBomUtf8Encoding;
|
||||||
processStarted = process.Start();
|
processStarted = process.Start();
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using ModelContextProtocol.Protocol;
|
using ModelContextProtocol.Protocol;
|
||||||
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace ModelContextProtocol.Client;
|
namespace ModelContextProtocol.Client;
|
||||||
|
|
@ -7,6 +8,8 @@ namespace ModelContextProtocol.Client;
|
||||||
/// <summary>Provides the client side of a stream-based session transport.</summary>
|
/// <summary>Provides the client side of a stream-based session transport.</summary>
|
||||||
internal class StreamClientSessionTransport : TransportBase
|
internal class StreamClientSessionTransport : TransportBase
|
||||||
{
|
{
|
||||||
|
internal static UTF8Encoding NoBomUtf8Encoding { get; } = new(encoderShouldEmitUTF8Identifier: false);
|
||||||
|
|
||||||
private readonly TextReader _serverOutput;
|
private readonly TextReader _serverOutput;
|
||||||
private readonly TextWriter _serverInput;
|
private readonly TextWriter _serverInput;
|
||||||
private readonly SemaphoreSlim _sendLock = new(1, 1);
|
private readonly SemaphoreSlim _sendLock = new(1, 1);
|
||||||
|
|
@ -54,6 +57,43 @@ internal class StreamClientSessionTransport : TransportBase
|
||||||
readTask.Start();
|
readTask.Start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="StreamClientSessionTransport"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="serverInput">
|
||||||
|
/// The server's input stream. Messages written to this stream will be sent to the server.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="serverOutput">
|
||||||
|
/// The server's output stream. Messages read from this stream will be received from the server.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="encoding">
|
||||||
|
/// The encoding used for reading and writing messages from the input and output streams. Defaults to UTF-8 without BOM if null.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="endpointName">
|
||||||
|
/// A name that identifies this transport endpoint in logs.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="loggerFactory">
|
||||||
|
/// Optional factory for creating loggers. If null, a NullLogger will be used.
|
||||||
|
/// </param>
|
||||||
|
/// <remarks>
|
||||||
|
/// This constructor starts a background task to read messages from the server output stream.
|
||||||
|
/// The transport will be marked as connected once initialized.
|
||||||
|
/// </remarks>
|
||||||
|
public StreamClientSessionTransport(Stream serverInput, Stream serverOutput, Encoding? encoding, string endpointName, ILoggerFactory? loggerFactory)
|
||||||
|
: this(
|
||||||
|
new StreamWriter(serverInput, encoding ?? NoBomUtf8Encoding),
|
||||||
|
#if NET
|
||||||
|
new StreamReader(serverOutput, encoding ?? NoBomUtf8Encoding),
|
||||||
|
#else
|
||||||
|
new CancellableStreamReader(serverOutput, encoding ?? NoBomUtf8Encoding),
|
||||||
|
#endif
|
||||||
|
endpointName,
|
||||||
|
loggerFactory)
|
||||||
|
{
|
||||||
|
Throw.IfNull(serverInput);
|
||||||
|
Throw.IfNull(serverOutput);
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public override async Task SendMessageAsync(JsonRpcMessage message, CancellationToken cancellationToken = default)
|
public override async Task SendMessageAsync(JsonRpcMessage message, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -47,8 +47,9 @@ public sealed class StreamClientTransport : IClientTransport
|
||||||
public Task<ITransport> ConnectAsync(CancellationToken cancellationToken = default)
|
public Task<ITransport> ConnectAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
return Task.FromResult<ITransport>(new StreamClientSessionTransport(
|
return Task.FromResult<ITransport>(new StreamClientSessionTransport(
|
||||||
new StreamWriter(_serverInput),
|
_serverInput,
|
||||||
new StreamReader(_serverOutput),
|
_serverOutput,
|
||||||
|
encoding: null,
|
||||||
"Client (stream)",
|
"Client (stream)",
|
||||||
_loggerFactory));
|
_loggerFactory));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
<PackageId>ModelContextProtocol.Core</PackageId>
|
<PackageId>ModelContextProtocol.Core</PackageId>
|
||||||
<Description>Core .NET SDK for the Model Context Protocol (MCP)</Description>
|
<Description>Core .NET SDK for the Model Context Protocol (MCP)</Description>
|
||||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||||
|
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup Condition="'$(TargetFramework)' != 'netstandard2.0'">
|
<PropertyGroup Condition="'$(TargetFramework)' != 'netstandard2.0'">
|
||||||
|
|
@ -20,6 +21,7 @@
|
||||||
<!-- Dependencies only needed by netstandard2.0 -->
|
<!-- Dependencies only needed by netstandard2.0 -->
|
||||||
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
|
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
|
||||||
<Compile Include="..\Common\Polyfills\**\*.cs" />
|
<Compile Include="..\Common\Polyfills\**\*.cs" />
|
||||||
|
<Compile Include="..\Common\CancellableStreamReader\**\*.cs" />
|
||||||
<PackageReference Include="Microsoft.Bcl.Memory" />
|
<PackageReference Include="Microsoft.Bcl.Memory" />
|
||||||
<PackageReference Include="System.Diagnostics.DiagnosticSource" />
|
<PackageReference Include="System.Diagnostics.DiagnosticSource" />
|
||||||
<PackageReference Include="System.Text.Json" />
|
<PackageReference Include="System.Text.Json" />
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ public class McpServerPrimitiveCollection<T> : ICollection<T>, IReadOnlyCollecti
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public McpServerPrimitiveCollection(IEqualityComparer<string>? keyComparer = null)
|
public McpServerPrimitiveCollection(IEqualityComparer<string>? keyComparer = null)
|
||||||
{
|
{
|
||||||
_primitives = new(keyComparer);
|
_primitives = new(keyComparer ?? EqualityComparer<string>.Default);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Occurs when the collection is changed.</summary>
|
/// <summary>Occurs when the collection is changed.</summary>
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,11 @@ public class StreamServerTransport : TransportBase
|
||||||
|
|
||||||
_logger = loggerFactory?.CreateLogger(GetType()) ?? NullLogger.Instance;
|
_logger = loggerFactory?.CreateLogger(GetType()) ?? NullLogger.Instance;
|
||||||
|
|
||||||
|
#if NET
|
||||||
_inputReader = new StreamReader(inputStream, Encoding.UTF8);
|
_inputReader = new StreamReader(inputStream, Encoding.UTF8);
|
||||||
|
#else
|
||||||
|
_inputReader = new CancellableStreamReader(inputStream, Encoding.UTF8);
|
||||||
|
#endif
|
||||||
_outputStream = outputStream;
|
_outputStream = outputStream;
|
||||||
|
|
||||||
SetConnected();
|
SetConnected();
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
namespace ModelContextProtocol.Tests.Utils;
|
using System.Net.Http;
|
||||||
|
|
||||||
|
namespace ModelContextProtocol.Tests.Utils;
|
||||||
|
|
||||||
public class MockHttpHandler : HttpMessageHandler
|
public class MockHttpHandler : HttpMessageHandler
|
||||||
{
|
{
|
||||||
|
|
|
||||||
15
tests/Common/Utils/ProcessExtensions.cs
Normal file
15
tests/Common/Utils/ProcessExtensions.cs
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
namespace System.Diagnostics;
|
||||||
|
|
||||||
|
public static class ProcessExtensions
|
||||||
|
{
|
||||||
|
public static async Task WaitForExitAsync(this Process process, TimeSpan timeout)
|
||||||
|
{
|
||||||
|
#if NET
|
||||||
|
using var shutdownCts = CancellationTokenSource.CreateLinkedTokenSource(TestContext.Current.CancellationToken);
|
||||||
|
shutdownCts.CancelAfter(timeout);
|
||||||
|
await process.WaitForExitAsync(shutdownCts.Token);
|
||||||
|
#else
|
||||||
|
process.WaitForExit(milliseconds: (int)timeout.TotalMilliseconds);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging" />
|
<PackageReference Include="Microsoft.Extensions.Logging" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" />
|
<PackageReference Include="Microsoft.Extensions.Logging.Console" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Options" />
|
<PackageReference Include="Microsoft.Extensions.Options" />
|
||||||
|
<PackageReference Include="Serilog" />
|
||||||
<PackageReference Include="Serilog.Extensions.Logging" />
|
<PackageReference Include="Serilog.Extensions.Logging" />
|
||||||
<PackageReference Include="Serilog.Sinks.File" />
|
<PackageReference Include="Serilog.Sinks.File" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
|
||||||
|
|
@ -25,14 +25,14 @@ public class ClientIntegrationTestFixture
|
||||||
|
|
||||||
TestServerTransportOptions = new()
|
TestServerTransportOptions = new()
|
||||||
{
|
{
|
||||||
Command = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "TestServer.exe" : "dotnet",
|
Command = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "TestServer.exe" : PlatformDetection.IsMonoRuntime ? "mono" : "dotnet",
|
||||||
Name = "TestServer",
|
Name = "TestServer",
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||||
{
|
{
|
||||||
// Change to Arguments to "mcp-server-everything" if you want to run the server locally after creating a symlink
|
// Change to Arguments to "mcp-server-everything" if you want to run the server locally after creating a symlink
|
||||||
TestServerTransportOptions.Arguments = ["TestServer.dll"];
|
TestServerTransportOptions.Arguments = [PlatformDetection.IsMonoRuntime ? "TestServer.exe" : "TestServer.dll"];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -606,7 +606,7 @@ public partial class McpServerBuilderExtensionsToolsTests : ClientServerTestBase
|
||||||
|
|
||||||
McpClientTool progressTool = tools.First(t => t.Name == "sends_progress_notifications");
|
McpClientTool progressTool = tools.First(t => t.Name == "sends_progress_notifications");
|
||||||
|
|
||||||
TaskCompletionSource tcs = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
TaskCompletionSource<bool> tcs = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
int remainingNotifications = 10;
|
int remainingNotifications = 10;
|
||||||
|
|
||||||
ConcurrentQueue<ProgressNotificationParams> notifications = new();
|
ConcurrentQueue<ProgressNotificationParams> notifications = new();
|
||||||
|
|
@ -618,7 +618,7 @@ public partial class McpServerBuilderExtensionsToolsTests : ClientServerTestBase
|
||||||
notifications.Enqueue(pn);
|
notifications.Enqueue(pn);
|
||||||
if (Interlocked.Decrement(ref remainingNotifications) == 0)
|
if (Interlocked.Decrement(ref remainingNotifications) == 0)
|
||||||
{
|
{
|
||||||
tcs.SetResult();
|
tcs.SetResult(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ public class EverythingSseServerFixture : IAsyncDisposable
|
||||||
|
|
||||||
using var stopProcess = Process.Start(stopInfo)
|
using var stopProcess = Process.Start(stopInfo)
|
||||||
?? throw new InvalidOperationException($"Could not stop process for {stopInfo.FileName} with '{stopInfo.Arguments}'.");
|
?? throw new InvalidOperationException($"Could not stop process for {stopInfo.FileName} with '{stopInfo.Arguments}'.");
|
||||||
await stopProcess.WaitForExitAsync();
|
await stopProcess.WaitForExitAsync(TimeSpan.FromSeconds(10));
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|
@ -60,6 +60,7 @@ public class EverythingSseServerFixture : IAsyncDisposable
|
||||||
|
|
||||||
private static bool CheckIsDockerAvailable()
|
private static bool CheckIsDockerAvailable()
|
||||||
{
|
{
|
||||||
|
#if NET
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
ProcessStartInfo processStartInfo = new()
|
ProcessStartInfo processStartInfo = new()
|
||||||
|
|
@ -78,5 +79,9 @@ public class EverythingSseServerFixture : IAsyncDisposable
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
#else
|
||||||
|
// Do not run docker tests using .NET framework.
|
||||||
|
return false;
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue