Reduce dependencies to 8.x versions of packages (#195)
* Reduce dependencies to 8.x versions of packages When building for netstandard2.0 or net8.0, the ModelContextProtocol library now only depends on 8.x versions of nuget packages, rather than using the newer 9.x versions. The exception to this are packages that don't have 8.x versions. This also introduces a net9.0 tfm. When building for net9.0, it'll use 9.x versions of packages. The PR temporarily removes the Microsoft.Extensions.AI.nupkg reference (the Microsoft.Extensions.AI.Abstractions reference is still very much there). The latest Microsoft.Extensions.AI package on nuget has a System.Text.Json 9.x reference, but the next build to be published drops that down to 8.x as well. Once that build is published, the dependency will be added back, and temporary polyfills (in particular TemporaryAIFunctionFactory) will be deleted. * Fix StdioClientSessionTransport shutdown StreamClientSessionTransport's ReadMessageAsync method does await CleanupAsync in its finally. CleanupAsync awaits _readTask. _readTask _is_ ReadMessageAsync. So it's waiting for itself to complete, which will then always timeout. We may want to refactor how this works more in the future, but for now, make sure it doesn't wait for itself. * Fix dangling requests when server unexpectedly shuts down If the client makes a request to the server and while waiting for the response the server shuts down, the client hangs. Make sure that when the client's message loops ends due to the server's output it's reading from ends, any pending requests are notified. * Add net9.0 testing * Avoid docker port conflicts from test suites running concurrently
This commit is contained in:
parent
1c145a5d52
commit
c452dc8e26
5 changed files with 247 additions and 91 deletions
|
|
@ -542,21 +542,11 @@ public class ClientIntegrationTests : LoggedTest, IClassFixture<ClientIntegratio
|
|||
[MemberData(nameof(GetClients))]
|
||||
public async Task SetLoggingLevel_ReceivesLoggingMessages(string clientId)
|
||||
{
|
||||
// arrange
|
||||
JsonSerializerOptions jsonSerializerOptions = new(JsonSerializerDefaults.Web)
|
||||
{
|
||||
TypeInfoResolver = new DefaultJsonTypeInfoResolver(),
|
||||
Converters = { new JsonStringEnumConverter() },
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
NumberHandling = JsonNumberHandling.AllowReadingFromString,
|
||||
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
|
||||
};
|
||||
|
||||
TaskCompletionSource<bool> receivedNotification = new();
|
||||
await using var client = await _fixture.CreateClientAsync(clientId);
|
||||
client.AddNotificationHandler(NotificationMethods.LoggingMessageNotification, (notification) =>
|
||||
{
|
||||
var loggingMessageNotificationParameters = JsonSerializer.Deserialize<LoggingMessageNotificationParams>(notification.Params, jsonSerializerOptions);
|
||||
var loggingMessageNotificationParameters = JsonSerializer.Deserialize<LoggingMessageNotificationParams>(notification.Params);
|
||||
if (loggingMessageNotificationParameters is not null)
|
||||
{
|
||||
receivedNotification.TrySetResult(true);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<TargetFrameworks>net9.0;net8.0</TargetFrameworks>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>Latest</LangVersion>
|
||||
|
|
@ -20,6 +20,7 @@
|
|||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.Extensions.AI" />
|
||||
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" />
|
||||
|
|
|
|||
|
|
@ -8,11 +8,25 @@ namespace ModelContextProtocol.Tests;
|
|||
|
||||
public class SseIntegrationTests(ITestOutputHelper outputHelper) : LoggedTest(outputHelper)
|
||||
{
|
||||
/// <summary>Port number to be grabbed by the next test.</summary>
|
||||
private static int s_nextPort = 3000;
|
||||
|
||||
// If the tests run concurrently against different versions of the runtime, tests can conflict with
|
||||
// each other in the ports set up for interacting with containers. Ensure that such suites running
|
||||
// against different TFMs use different port numbers.
|
||||
private static readonly int s_portOffset = 1000 * (Environment.Version.Major switch
|
||||
{
|
||||
int v when v >= 8 => Environment.Version.Major - 7,
|
||||
_ => 0,
|
||||
});
|
||||
|
||||
private static int CreatePortNumber() => Interlocked.Increment(ref s_nextPort) + s_portOffset;
|
||||
|
||||
[Fact]
|
||||
public async Task ConnectAndReceiveMessage_InMemoryServer()
|
||||
{
|
||||
// Arrange
|
||||
await using InMemoryTestSseServer server = new(logger: LoggerFactory.CreateLogger<InMemoryTestSseServer>());
|
||||
await using InMemoryTestSseServer server = new(CreatePortNumber(), LoggerFactory.CreateLogger<InMemoryTestSseServer>());
|
||||
await server.StartAsync();
|
||||
|
||||
var defaultOptions = new McpClientOptions
|
||||
|
|
@ -26,7 +40,7 @@ public class SseIntegrationTests(ITestOutputHelper outputHelper) : LoggedTest(ou
|
|||
Name = "In-memory Test Server",
|
||||
TransportType = TransportTypes.Sse,
|
||||
TransportOptions = [],
|
||||
Location = "http://localhost:5000/sse"
|
||||
Location = $"http://localhost:{server.Port}/sse"
|
||||
};
|
||||
|
||||
// Act
|
||||
|
|
@ -52,7 +66,7 @@ public class SseIntegrationTests(ITestOutputHelper outputHelper) : LoggedTest(ou
|
|||
{
|
||||
Assert.SkipWhen(!EverythingSseServerFixture.IsDockerAvailable, "docker is not available");
|
||||
|
||||
int port = 3001;
|
||||
int port = CreatePortNumber();
|
||||
|
||||
await using var fixture = new EverythingSseServerFixture(port);
|
||||
await fixture.StartAsync();
|
||||
|
|
@ -89,7 +103,7 @@ public class SseIntegrationTests(ITestOutputHelper outputHelper) : LoggedTest(ou
|
|||
{
|
||||
Assert.SkipWhen(!EverythingSseServerFixture.IsDockerAvailable, "docker is not available");
|
||||
|
||||
int port = 3002;
|
||||
int port = CreatePortNumber();
|
||||
|
||||
await using var fixture = new EverythingSseServerFixture(port);
|
||||
await fixture.StartAsync();
|
||||
|
|
@ -157,11 +171,10 @@ public class SseIntegrationTests(ITestOutputHelper outputHelper) : LoggedTest(ou
|
|||
public async Task ConnectAndReceiveMessage_InMemoryServer_WithFullEndpointEventUri()
|
||||
{
|
||||
// Arrange
|
||||
await using InMemoryTestSseServer server = new(logger: LoggerFactory.CreateLogger<InMemoryTestSseServer>());
|
||||
await using InMemoryTestSseServer server = new(CreatePortNumber(), LoggerFactory.CreateLogger<InMemoryTestSseServer>());
|
||||
server.UseFullUrlForEndpointEvent = true;
|
||||
await server.StartAsync();
|
||||
|
||||
|
||||
var defaultOptions = new McpClientOptions
|
||||
{
|
||||
ClientInfo = new() { Name = "IntegrationTestClient", Version = "1.0.0" }
|
||||
|
|
@ -173,7 +186,7 @@ public class SseIntegrationTests(ITestOutputHelper outputHelper) : LoggedTest(ou
|
|||
Name = "In-memory Test Server",
|
||||
TransportType = TransportTypes.Sse,
|
||||
TransportOptions = [],
|
||||
Location = "http://localhost:5000/sse"
|
||||
Location = $"http://localhost:{server.Port}/sse"
|
||||
};
|
||||
|
||||
// Act
|
||||
|
|
@ -197,7 +210,7 @@ public class SseIntegrationTests(ITestOutputHelper outputHelper) : LoggedTest(ou
|
|||
public async Task ConnectAndReceiveNotification_InMemoryServer()
|
||||
{
|
||||
// Arrange
|
||||
await using InMemoryTestSseServer server = new(logger: LoggerFactory.CreateLogger<InMemoryTestSseServer>());
|
||||
await using InMemoryTestSseServer server = new(CreatePortNumber(), LoggerFactory.CreateLogger<InMemoryTestSseServer>());
|
||||
await server.StartAsync();
|
||||
|
||||
|
||||
|
|
@ -212,7 +225,7 @@ public class SseIntegrationTests(ITestOutputHelper outputHelper) : LoggedTest(ou
|
|||
Name = "In-memory Test Server",
|
||||
TransportType = TransportTypes.Sse,
|
||||
TransportOptions = [],
|
||||
Location = "http://localhost:5000/sse"
|
||||
Location = $"http://localhost:{server.Port}/sse"
|
||||
};
|
||||
|
||||
// Act
|
||||
|
|
|
|||
|
|
@ -17,16 +17,19 @@ public class SseServerIntegrationTestFixture : IAsyncDisposable
|
|||
|
||||
public SseServerIntegrationTestFixture()
|
||||
{
|
||||
// Ensure that test suites running against different TFMs and possibly concurrently use different port numbers.
|
||||
int port = 3001 + Environment.Version.Major;
|
||||
|
||||
DefaultConfig = new McpServerConfig
|
||||
{
|
||||
Id = "test_server",
|
||||
Name = "TestServer",
|
||||
TransportType = TransportTypes.Sse,
|
||||
TransportOptions = [],
|
||||
Location = "http://localhost:3001/sse"
|
||||
Location = $"http://localhost:{port}/sse"
|
||||
};
|
||||
|
||||
_serverTask = Program.MainAsync([], new XunitLoggerProvider(_delegatingTestOutputHelper), _stopCts.Token);
|
||||
_serverTask = Program.MainAsync([port.ToString()], new XunitLoggerProvider(_delegatingTestOutputHelper), _stopCts.Token);
|
||||
}
|
||||
|
||||
public static McpClientOptions CreateDefaultClientOptions() => new()
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@ public sealed class InMemoryTestSseServer : IAsyncDisposable
|
|||
|
||||
public InMemoryTestSseServer(int port = 5000, ILogger<InMemoryTestSseServer>? logger = null)
|
||||
{
|
||||
Port = port;
|
||||
|
||||
_listener = new HttpListener();
|
||||
_listener.Prefixes.Add($"http://localhost:{port}/");
|
||||
_cts = new CancellationTokenSource();
|
||||
|
|
@ -35,6 +37,8 @@ public sealed class InMemoryTestSseServer : IAsyncDisposable
|
|||
_messagePath = "/message";
|
||||
}
|
||||
|
||||
public int Port { get; }
|
||||
|
||||
/// <summary>
|
||||
/// This is to be able to use the full URL for the endpoint event.
|
||||
/// </summary>
|
||||
|
|
|
|||
Loading…
Reference in a new issue