using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Primitives; using ModelContextProtocol.Client; using System.Collections.Concurrent; namespace ModelContextProtocol.AspNetCore.Tests; public class MapMcpStreamableHttpTests(ITestOutputHelper outputHelper) : MapMcpTests(outputHelper) { protected override bool UseStreamableHttp => true; protected override bool Stateless => false; [Theory] [InlineData("/a", "/a")] [InlineData("/a", "/a/")] [InlineData("/a/", "/a/")] [InlineData("/a/", "/a")] [InlineData("/a/b", "/a/b")] public async Task CanConnect_WithMcpClient_AfterCustomizingRoute(string routePattern, string requestPath) { Builder.Services.AddMcpServer(options => { options.ServerInfo = new() { Name = "TestCustomRouteServer", Version = "1.0.0", }; }).WithHttpTransport(ConfigureStateless); await using var app = Builder.Build(); app.MapMcp(routePattern); await app.StartAsync(TestContext.Current.CancellationToken); await using var mcpClient = await ConnectAsync(requestPath); 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("http://localhost:5000/"), 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("http://localhost:5000/"), 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("http://localhost:5000/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(transportOptions: new() { Endpoint = new("http://localhost:5000/sse"), TransportMode = HttpTransportMode.Sse }); Assert.Equal("SseTestServer", mcpClient.ServerInfo.Name); } [Fact] public async Task StreamableHttpClient_SendsMcpProtocolVersionHeader_AfterInitialization() { var protocolVersionHeaderValues = new ConcurrentQueue(); Builder.Services.AddMcpServer().WithHttpTransport(ConfigureStateless).WithTools(); await using var app = Builder.Build(); app.Use(next => { return async context => { if (!StringValues.IsNullOrEmpty(context.Request.Headers["mcp-protocol-version"])) { protocolVersionHeaderValues.Enqueue(context.Request.Headers["mcp-protocol-version"]); } await next(context); }; }); app.MapMcp(); await app.StartAsync(TestContext.Current.CancellationToken); await using var mcpClient = await ConnectAsync(clientOptions: new() { ProtocolVersion = "2025-03-26", }); Assert.Equal("2025-03-26", mcpClient.NegotiatedProtocolVersion); await mcpClient.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken); await mcpClient.DisposeAsync(); // The GET request might not have started in time, and the DELETE request won't be sent in // Stateless mode due to the lack of an Mcp-Session-Id, but the header should be included in the // initialized notification and the tools/list call at a minimum. Assert.True(protocolVersionHeaderValues.Count > 1); Assert.All(protocolVersionHeaderValues, v => Assert.Equal("2025-03-26", v)); } }