Use Kestrel for all in-memory HTTP tests (#225)

* Add route pattern parameter to MapMcp
* Add configureOptionsAsync
* Use Kestrel for all in-memory HTTP tests
* Use ApplicationStopping token for SSE responses
* Use RegisterNotificationHandler in MapMcp tests
* Work around SocketsHttpHandler bug where it doesn't call DispseAsync on the stream returned by the ConnectCallback
This commit is contained in:
Stephen Halter 2025-04-06 19:27:17 -07:00 • committed by GitHub
commit faf12b6a94
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 631 additions and 642 deletions

View file

@ -106,7 +106,10 @@ internal sealed class McpClient : McpEndpoint, IMcpClient
{
// Connect transport
_sessionTransport = await _clientTransport.ConnectAsync(cancellationToken).ConfigureAwait(false);
StartSession(_sessionTransport);
InitializeSession(_sessionTransport);
// We don't want the ConnectAsync token to cancel the session after we've successfully connected.
// The base class handles cleaning up the session in DisposeAsync without our help.
StartSession(_sessionTransport, fullSessionCancellationToken: CancellationToken.None);
// Perform initialization sequence
using var initializationCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);

View file

@ -151,16 +151,14 @@ internal sealed class SseClientSessionTransport : TransportBase
{
try
{
if (!_connectionCts.IsCancellationRequested)
{
await _connectionCts.CancelAsync().ConfigureAwait(false);
_connectionCts.Dispose();
}
await _connectionCts.CancelAsync().ConfigureAwait(false);
if (_receiveTask != null)
{
await _receiveTask.ConfigureAwait(false);
}
_connectionCts.Dispose();
}
finally
{

View file

@ -18,6 +18,8 @@ internal sealed class McpServer : McpEndpoint, IMcpServer
Version = DefaultAssemblyName.Version?.ToString() ?? "1.0.0",
};
private readonly ITransport _sessionTransport;
private readonly EventHandler? _toolsChangedDelegate;
private readonly EventHandler? _promptsChangedDelegate;
@ -41,6 +43,7 @@ internal sealed class McpServer : McpEndpoint, IMcpServer
options ??= new();
_sessionTransport = transport;
ServerOptions = options;
Services = serviceProvider;
_endpointName = $"Server ({options.ServerInfo?.Name ?? DefaultImplementation.Name} {options.ServerInfo?.Version ?? DefaultImplementation.Version})";
@ -81,8 +84,8 @@ internal sealed class McpServer : McpEndpoint, IMcpServer
prompts.Changed += _promptsChangedDelegate;
}
// And start the session.
StartSession(transport);
// And initialize the session.
InitializeSession(transport);
}
public ServerCapabilities? ServerCapabilities { get; set; }
@ -112,9 +115,8 @@ internal sealed class McpServer : McpEndpoint, IMcpServer
try
{
using var _ = cancellationToken.Register(static s => ((McpServer)s!).CancelSession(), this);
// The McpServer ctor always calls StartSession, so MessageProcessingTask is always set.
await MessageProcessingTask!.ConfigureAwait(false);
StartSession(_sessionTransport, fullSessionCancellationToken: cancellationToken);
await MessageProcessingTask.ConfigureAwait(false);
}
finally
{

View file

@ -5,6 +5,7 @@ using ModelContextProtocol.Protocol.Messages;
using ModelContextProtocol.Protocol.Transport;
using ModelContextProtocol.Server;
using ModelContextProtocol.Utils;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
@ -62,12 +63,16 @@ internal abstract class McpEndpoint : IAsyncDisposable
/// </summary>
protected Task? MessageProcessingTask { get; private set; }
[MemberNotNull(nameof(MessageProcessingTask))]
protected void StartSession(ITransport sessionTransport)
protected void InitializeSession(ITransport sessionTransport)
{
_sessionCts = new CancellationTokenSource();
_session = new McpSession(this is IMcpServer, sessionTransport, EndpointName, RequestHandlers, NotificationHandlers, _logger);
MessageProcessingTask = _session.ProcessMessagesAsync(_sessionCts.Token);
}
[MemberNotNull(nameof(MessageProcessingTask))]
protected void StartSession(ITransport sessionTransport, CancellationToken fullSessionCancellationToken)
{
_sessionCts = CancellationTokenSource.CreateLinkedTokenSource(fullSessionCancellationToken);
MessageProcessingTask = GetSessionOrThrow().ProcessMessagesAsync(_sessionCts.Token);
}
protected void CancelSession() => _sessionCts?.Cancel();
@ -122,5 +127,5 @@ internal abstract class McpEndpoint : IAsyncDisposable
}
protected McpSession GetSessionOrThrow()
=> _session ?? throw new InvalidOperationException($"This should be unreachable from public API! Call {nameof(StartSession)} before sending messages.");
=> _session ?? throw new InvalidOperationException($"This should be unreachable from public API! Call {nameof(InitializeSession)} before sending messages.");
}