Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 39 additions & 24 deletions tests/ModelContextProtocol.AspNetCore.Tests/MapMcpTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -245,34 +245,49 @@ public async Task LongRunningToolCall_DoesNotTimeout_WhenNoEventStreamStore()
app.MapMcp();
await app.StartAsync(TestContext.Current.CancellationToken);

// Create a custom HttpClient with a very short timeout (1 second)
// The tool will take 2 seconds to complete
using var shortTimeoutClient = new HttpClient(SocketsHttpHandler)
// Retry a couple of times to reduce occasional flakiness on low-resource machines.
// If the server regresses to flushing only after tool completion, each attempt should still fail
// because HttpClient timeout (1 second) is below the tool duration (2 seconds).
for (var attempt = 0; attempt < 3; attempt++)
{
BaseAddress = new Uri("http://localhost:5000/"),
Timeout = TimeSpan.FromSeconds(1)
};

var path = UseStreamableHttp ? "/" : "/sse";
var transportMode = UseStreamableHttp ? HttpTransportMode.StreamableHttp : HttpTransportMode.Sse;

await using var transport = new HttpClientTransport(new()
{
Endpoint = new($"http://localhost:5000{path}"),
TransportMode = transportMode,
}, shortTimeoutClient, LoggerFactory);
try
{
// Create a custom HttpClient with a very short timeout (1 second)
// The tool will take 2 seconds to complete
using var shortTimeoutClient = new HttpClient(SocketsHttpHandler, disposeHandler: false)
{
BaseAddress = new Uri("http://localhost:5000/"),
Timeout = TimeSpan.FromSeconds(1)
};

await using var mcpClient = await McpClient.CreateAsync(transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken);
var path = UseStreamableHttp ? "/" : "/sse";
var transportMode = UseStreamableHttp ? HttpTransportMode.StreamableHttp : HttpTransportMode.Sse;

// Call a tool that takes 2 seconds - this should succeed despite the 1 second HttpClient timeout
// because the response stream is flushed immediately after receiving the request
var response = await mcpClient.CallToolAsync(
"long_running_operation",
new Dictionary<string, object?>() { ["durationMs"] = 2000 },
cancellationToken: TestContext.Current.CancellationToken);
await using var transport = new HttpClientTransport(new()
{
Endpoint = new($"http://localhost:5000{path}"),
TransportMode = transportMode,
}, shortTimeoutClient, LoggerFactory);

await using var mcpClient = await McpClient.CreateAsync(transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken);

// Call a tool that takes 2 seconds - this should succeed despite the 1 second HttpClient timeout
// because the response stream is flushed immediately after receiving the request
var response = await mcpClient.CallToolAsync(
"long_running_operation",
new Dictionary<string, object?>() { ["durationMs"] = 2000 },
cancellationToken: TestContext.Current.CancellationToken);

var content = Assert.Single(response.Content.OfType<TextContentBlock>());
Assert.Equal("Operation completed after 2000ms", content.Text);
return;
}
catch (OperationCanceledException) when (attempt < 2)
{
// Retry intermittent timeout-related failures on slow CI machines.
}
}

var content = Assert.Single(response.Content.OfType<TextContentBlock>());
Assert.Equal("Operation completed after 2000ms", content.Text);
}

private ClaimsPrincipal CreateUser(string name)
Expand Down