csharp-sdk/tests/ModelContextProtocol.AspNetCore.Tests/Utils/KestrelInMemoryConnection.cs
Stephen Halter 9c124b1770
Add StreamableHttpHandler and WithHttpTransport (#291)
* Simplify ModelContextProtocol.AspNetCore README

* Add StreamableHttpHandler

* Use "docker info" in CheckIsDockerAvailable

* Call WithHttpTransport in samples, tests and README

* Cleanup test namespaces

* Add CanConnect_WithMcpClient_AfterCustomizingRoute test

* Simplify relative URI handling

* Handle request made directly to the MapMcp route pattern

* Add Messages_FromNewUser_AreRejected test

* Fix README

* Shorten UserIdClaim ValueTuple names

* Remove MaxReconnectAttempts and ReconnectDelay from SseClientTransportOptions

- Add proper AdditionalHeaders support
2025-04-14 10:15:33 -04:00

113 lines
4.5 KiB
C#

using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Http.Features;
using System.IO.Pipelines;
namespace ModelContextProtocol.AspNetCore.Tests.Utils;
public sealed class KestrelInMemoryConnection : ConnectionContext
{
private readonly Pipe _clientToServerPipe = new();
private readonly Pipe _serverToClientPipe = new();
private readonly CancellationTokenSource _connectionClosedCts = new();
private readonly FeatureCollection _features = new();
public KestrelInMemoryConnection()
{
ConnectionClosed = _connectionClosedCts.Token;
Transport = new DuplexPipe
{
Input = _clientToServerPipe.Reader,
Output = _serverToClientPipe.Writer,
};
Application = new DuplexPipe
{
Input = _serverToClientPipe.Reader,
Output = _clientToServerPipe.Writer,
};
ClientStream = new DuplexStream(Application, _connectionClosedCts);
}
public IDuplexPipe Application { get; }
public Stream ClientStream { get; }
public override IDuplexPipe Transport { get; set; }
public override string ConnectionId { get; set; } = Guid.NewGuid().ToString("N");
public override IFeatureCollection Features => _features;
public override IDictionary<object, object?> Items { get; set; } = new Dictionary<object, object?>();
public override async ValueTask DisposeAsync()
{
// This is called by Kestrel. The client should dispose the DuplexStream which
// completes the other half of these pipes.
await _serverToClientPipe.Writer.CompleteAsync();
await _serverToClientPipe.Reader.CompleteAsync();
// Don't bother disposing the _connectionClosedCts, since this is just for testing,
// and it's annoying to synchronize with DuplexStream.
await base.DisposeAsync();
}
private class DuplexPipe : IDuplexPipe
{
public required PipeReader Input { get; init; }
public required PipeWriter Output { get; init; }
}
private class DuplexStream(IDuplexPipe duplexPipe, CancellationTokenSource connectionClosedCts) : Stream
{
private readonly Stream _readStream = duplexPipe.Input.AsStream();
private readonly Stream _writeStream = duplexPipe.Output.AsStream();
public override bool CanRead => true;
public override bool CanWrite => true;
public override bool CanSeek => false;
public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
=> _readStream.ReadAsync(buffer, offset, count, cancellationToken);
public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
=> _readStream.ReadAsync(buffer, cancellationToken);
public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
=> _writeStream.WriteAsync(buffer, offset, count, cancellationToken);
public override ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default)
=> _writeStream.WriteAsync(buffer, cancellationToken);
public override Task FlushAsync(CancellationToken cancellationToken)
=> _writeStream.FlushAsync(cancellationToken);
protected override void Dispose(bool disposing)
{
// Signal to the server the the client has closed the connection, and dispose the client-half of the Pipes.
connectionClosedCts.Cancel();
duplexPipe.Input.Complete();
duplexPipe.Output.Complete();
}
// Unsupported stuff
public override long Length => throw new NotSupportedException();
public override long Position
{
get => throw new NotSupportedException();
set => throw new NotSupportedException();
}
public override long Seek(long offset, SeekOrigin origin)
{
throw new NotSupportedException();
}
public override void SetLength(long value)
{
throw new NotSupportedException();
}
// Don't bother supporting sync or APM methods. SocketsHttpHandler shouldn't use them.
public override int Read(byte[] buffer, int offset, int count) => throw new NotSupportedException();
public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();
public override void Flush() => throw new NotSupportedException();
}
}