Fix HttpClient timeout for long-running tools without event stream store (#1268)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: halter73 <54385+halter73@users.noreply.github.com>
This commit is contained in:
parent
ec2983b29f
commit
fb2daf2f0d
1 changed files with 172 additions and 41 deletions
|
|
@ -231,6 +231,50 @@ public abstract class MapMcpTests(ITestOutputHelper testOutputHelper) : KestrelI
|
|||
"This suggests the GET request is not respecting ApplicationStopping token.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LongRunningToolCall_DoesNotTimeout_WhenNoEventStreamStore()
|
||||
{
|
||||
// Regression test for: Tool calls that last over HttpClient timeout without producing
|
||||
// intermediate notifications will timeout because HttpClient doesn't see the 200 response
|
||||
// until the first message is written. When primingItem is null (no ISseEventStreamStore),
|
||||
// we should flush the response stream so HttpClient sees the 200 immediately.
|
||||
|
||||
Builder.Services.AddMcpServer().WithHttpTransport(ConfigureStateless).WithTools<LongRunningTools>();
|
||||
|
||||
await using var app = Builder.Build();
|
||||
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)
|
||||
{
|
||||
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);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
private ClaimsPrincipal CreateUser(string name)
|
||||
=> new(new ClaimsIdentity(
|
||||
[new Claim("name", name), new Claim(ClaimTypes.NameIdentifier, name)],
|
||||
|
|
@ -288,4 +332,17 @@ public abstract class MapMcpTests(ITestOutputHelper testOutputHelper) : KestrelI
|
|||
return $"Sampling completed successfully. Client responded: {Assert.IsType<TextContentBlock>(Assert.Single(samplingResult.Content)).Text}";
|
||||
}
|
||||
}
|
||||
|
||||
[McpServerToolType]
|
||||
protected class LongRunningTools
|
||||
{
|
||||
[McpServerTool, Description("Simulates a long-running operation")]
|
||||
public static async Task<string> LongRunningOperation(
|
||||
[Description("Duration of the operation in milliseconds")] int durationMs,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await Task.Delay(durationMs, cancellationToken);
|
||||
return $"Operation completed after {durationMs}ms";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue