Send DELETE request when closing a Streamable HTTP session on the client (#501)

This commit is contained in:
Stephen Halter 2025-06-19 08:40:11 -07:00 • committed by GitHub
commit a4b4edaa2a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -14,8 +14,10 @@ namespace ModelContextProtocol.AspNetCore.Tests;
public class StreamableHttpClientConformanceTests(ITestOutputHelper outputHelper) : KestrelInMemoryTest(outputHelper), IAsyncDisposable
{
private WebApplication? _app;
private readonly List<string> _deleteRequestSessionIds = [];
private async Task StartAsync()
// Don't add the delete endpoint by default to ensure the client still works with basic sessionless servers.
private async Task StartAsync(bool enableDelete = false)
{
Builder.Services.Configure<JsonOptions>(options =>
{
@ -28,7 +30,7 @@ public class StreamableHttpClientConformanceTests(ITestOutputHelper outputHelper
Services = _app.Services,
});
_app.MapPost("/mcp", (JsonRpcMessage message) =>
_app.MapPost("/mcp", (JsonRpcMessage message, HttpContext context) =>
{
if (message is not JsonRpcRequest request)
{
@ -36,6 +38,12 @@ public class StreamableHttpClientConformanceTests(ITestOutputHelper outputHelper
return Results.Accepted();
}
if (enableDelete)
{
// Add a session ID to the response to enable session tracking
context.Response.Headers.Append("mcp-session-id", "test-session-123");
}
if (request.Method == "initialize")
{
return Results.Json(new JsonRpcResponse
@ -87,6 +95,15 @@ public class StreamableHttpClientConformanceTests(ITestOutputHelper outputHelper
throw new Exception("Unexpected message!");
});
if (enableDelete)
{
_app.MapDelete("/mcp", context =>
{
_deleteRequestSessionIds.Add(context.Request.Headers["mcp-session-id"].ToString());
return Task.CompletedTask;
});
}
await _app.StartAsync(TestContext.Current.CancellationToken);
}
@ -136,6 +153,27 @@ public class StreamableHttpClientConformanceTests(ITestOutputHelper outputHelper
await Task.WhenAll(echoTasks);
}
[Fact]
public async Task SendsDeleteRequestOnDispose()
{
await StartAsync(enableDelete: true);
await using var transport = new SseClientTransport(new()
{
Endpoint = new("http://localhost/mcp"),
TransportMode = HttpTransportMode.StreamableHttp,
}, HttpClient, LoggerFactory);
await using var client = await McpClientFactory.CreateAsync(transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken);
// Dispose should trigger DELETE request
await client.DisposeAsync();
// Verify DELETE request was sent with correct session ID
var sessionId = Assert.Single(_deleteRequestSessionIds);
Assert.Equal("test-session-123", sessionId);
}
private static async Task CallEchoAndValidateAsync(McpClientTool echoTool)
{
var response = await echoTool.CallAsync(new Dictionary<string, object?>() { ["message"] = "Hello world!" }, cancellationToken: TestContext.Current.CancellationToken);