Automatically fall back from Streamable HTTP to SSE on the client by default (#456)

This commit is contained in:
Stephen Halter 2025-05-28 11:06:58 -07:00 • committed by GitHub
commit 317f468807
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 489 additions and 49 deletions

View file

@ -56,7 +56,7 @@ public abstract class HttpServerIntegrationTests : LoggedTest, IClassFixture<Sse
[Fact]
public async Task ListTools_Sse_TestServer()
{
{
// arrange
// act

View file

@ -1,5 +1,6 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using ModelContextProtocol.Client;
namespace ModelContextProtocol.AspNetCore.Tests;
@ -34,4 +35,112 @@ public class MapMcpStreamableHttpTests(ITestOutputHelper outputHelper) : MapMcpT
Assert.Equal("TestCustomRouteServer", mcpClient.ServerInfo.Name);
}
[Fact]
public async Task StreamableHttpMode_Works_WithRootEndpoint()
{
Builder.Services.AddMcpServer(options =>
{
options.ServerInfo = new()
{
Name = "StreamableHttpTestServer",
Version = "1.0.0",
};
}).WithHttpTransport(ConfigureStateless);
await using var app = Builder.Build();
app.MapMcp();
await app.StartAsync(TestContext.Current.CancellationToken);
await using var mcpClient = await ConnectAsync("/", new()
{
Endpoint = new Uri("http://localhost/"),
TransportMode = HttpTransportMode.AutoDetect
});
Assert.Equal("StreamableHttpTestServer", mcpClient.ServerInfo.Name);
}
[Fact]
public async Task AutoDetectMode_Works_WithRootEndpoint()
{
Builder.Services.AddMcpServer(options =>
{
options.ServerInfo = new()
{
Name = "AutoDetectTestServer",
Version = "1.0.0",
};
}).WithHttpTransport(ConfigureStateless);
await using var app = Builder.Build();
app.MapMcp();
await app.StartAsync(TestContext.Current.CancellationToken);
await using var mcpClient = await ConnectAsync("/", new()
{
Endpoint = new Uri("http://localhost/"),
TransportMode = HttpTransportMode.AutoDetect
});
Assert.Equal("AutoDetectTestServer", mcpClient.ServerInfo.Name);
}
[Fact]
public async Task AutoDetectMode_Works_WithSseEndpoint()
{
Assert.SkipWhen(Stateless, "SSE endpoint is disabled in stateless mode.");
Builder.Services.AddMcpServer(options =>
{
options.ServerInfo = new()
{
Name = "AutoDetectSseTestServer",
Version = "1.0.0",
};
}).WithHttpTransport(ConfigureStateless);
await using var app = Builder.Build();
app.MapMcp();
await app.StartAsync(TestContext.Current.CancellationToken);
await using var mcpClient = await ConnectAsync("/sse", new()
{
Endpoint = new Uri("http://localhost/sse"),
TransportMode = HttpTransportMode.AutoDetect
});
Assert.Equal("AutoDetectSseTestServer", mcpClient.ServerInfo.Name);
}
[Fact]
public async Task SseMode_Works_WithSseEndpoint()
{
Assert.SkipWhen(Stateless, "SSE endpoint is disabled in stateless mode.");
Builder.Services.AddMcpServer(options =>
{
options.ServerInfo = new()
{
Name = "SseTestServer",
Version = "1.0.0",
};
}).WithHttpTransport(ConfigureStateless);
await using var app = Builder.Build();
app.MapMcp();
await app.StartAsync(TestContext.Current.CancellationToken);
await using var mcpClient = await ConnectAsync(options: new()
{
Endpoint = new Uri("http://localhost/sse"),
TransportMode = HttpTransportMode.Sse
});
Assert.Equal("SseTestServer", mcpClient.ServerInfo.Name);
}
}

View file

@ -20,16 +20,17 @@ public abstract class MapMcpTests(ITestOutputHelper testOutputHelper) : KestrelI
options.Stateless = Stateless;
}
protected async Task<IMcpClient> ConnectAsync(string? path = null)
protected async Task<IMcpClient> ConnectAsync(string? path = null, SseClientTransportOptions? options = null)
{
// Default behavior when no options are provided
path ??= UseStreamableHttp ? "/" : "/sse";
var sseClientTransportOptions = new SseClientTransportOptions()
await using var transport = new SseClientTransport(options ?? new SseClientTransportOptions()
{
Endpoint = new Uri($"http://localhost{path}"),
UseStreamableHttp = UseStreamableHttp,
};
await using var transport = new SseClientTransport(sseClientTransportOptions, HttpClient, LoggerFactory);
TransportMode = UseStreamableHttp ? HttpTransportMode.StreamableHttp : HttpTransportMode.Sse,
}, HttpClient, LoggerFactory);
return await McpClientFactory.CreateAsync(transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken);
}

View file

@ -9,6 +9,6 @@ public class StatelessServerIntegrationTests(SseServerIntegrationTestFixture fix
{
Endpoint = new Uri("http://localhost/stateless"),
Name = "In-memory Streamable HTTP Client",
UseStreamableHttp = true,
TransportMode = HttpTransportMode.StreamableHttp,
};
}

View file

@ -18,7 +18,7 @@ public class StatelessServerTests(ITestOutputHelper outputHelper) : KestrelInMem
{
Endpoint = new Uri("http://localhost/"),
Name = "In-memory Streamable HTTP Client",
UseStreamableHttp = true,
TransportMode = HttpTransportMode.StreamableHttp,
};
private async Task StartAsync()

View file

@ -98,7 +98,7 @@ public class StreamableHttpClientConformanceTests(ITestOutputHelper outputHelper
await using var transport = new SseClientTransport(new()
{
Endpoint = new("http://localhost/mcp"),
UseStreamableHttp = true,
TransportMode = HttpTransportMode.StreamableHttp,
}, HttpClient, LoggerFactory);
await using var client = await McpClientFactory.CreateAsync(transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken);
@ -118,7 +118,7 @@ public class StreamableHttpClientConformanceTests(ITestOutputHelper outputHelper
await using var transport = new SseClientTransport(new()
{
Endpoint = new("http://localhost/mcp"),
UseStreamableHttp = true,
TransportMode = HttpTransportMode.StreamableHttp,
}, HttpClient, LoggerFactory);
await using var client = await McpClientFactory.CreateAsync(transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken);

View file

@ -15,7 +15,7 @@ public class StreamableHttpServerIntegrationTests(SseServerIntegrationTestFixtur
{
Endpoint = new Uri("http://localhost/"),
Name = "In-memory Streamable HTTP Client",
UseStreamableHttp = true,
TransportMode = HttpTransportMode.StreamableHttp,
};
[Fact]