using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using ModelContextProtocol.AspNetCore.Tests.Utils; using ModelContextProtocol.Protocol; using ModelContextProtocol.Server; namespace ModelContextProtocol.AspNetCore.Tests; public class HttpMcpServerBuilderExtensionsTests(ITestOutputHelper testOutputHelper) : KestrelInMemoryTest(testOutputHelper) { [Fact] public void WithDistributedCacheEventStreamStore_RegistersStoreInDI() { Builder.Services.AddDistributedMemoryCache(); Builder.Services .AddMcpServer() .WithHttpTransport() .WithDistributedCacheEventStreamStore(); using var app = Builder.Build(); var store = app.Services.GetService(); Assert.IsType(store); } [Fact] public void WithDistributedCacheEventStreamStore_ConfigureCallbackIsInvoked() { DistributedCacheEventStreamStoreOptions? capturedOptions = null; Builder.Services.AddDistributedMemoryCache(); Builder.Services .AddMcpServer() .WithHttpTransport() .WithDistributedCacheEventStreamStore(options => capturedOptions = options); using var app = Builder.Build(); // Force options resolution to trigger the configure callback. _ = app.Services.GetRequiredService>().Value; Assert.NotNull(capturedOptions); } [Fact] public void WithDistributedCacheEventStreamStore_WorksWithoutDICache_WhenCacheSetViaCallback() { var explicitCache = new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())); Builder.Services .AddMcpServer() .WithHttpTransport() .WithDistributedCacheEventStreamStore(options => options.Cache = explicitCache); using var app = Builder.Build(); var store = app.Services.GetService(); Assert.IsType(store); } [Fact] public void WithDistributedCacheEventStreamStore_ThrowsOptionsValidationException_WhenNoCacheConfigured() { Builder.Services .AddMcpServer() .WithHttpTransport() .WithDistributedCacheEventStreamStore(); using var app = Builder.Build(); var ex = Assert.Throws( () => app.Services.GetRequiredService()); Assert.StartsWith($"The '{nameof(DistributedCacheEventStreamStoreOptions)}.{nameof(DistributedCacheEventStreamStoreOptions.Cache)}'", ex.Message); } [Fact] public void EventStreamStore_IsPopulatedFromDI_ViaPostConfigure() { Builder.Services.AddDistributedMemoryCache(); Builder.Services .AddMcpServer() .WithHttpTransport() .WithDistributedCacheEventStreamStore(); using var app = Builder.Build(); var options = app.Services.GetRequiredService>().Value; Assert.IsType(options.EventStreamStore); } [Fact] public void EventStreamStore_ExplicitOption_TakesPrecedenceOverDI() { var explicitStore = new TestSseEventStreamStore(); Builder.Services.AddDistributedMemoryCache(); Builder.Services .AddMcpServer() .WithHttpTransport(options => options.EventStreamStore = explicitStore) .WithDistributedCacheEventStreamStore(); using var app = Builder.Build(); var options = app.Services.GetRequiredService>().Value; Assert.Same(explicitStore, options.EventStreamStore); } [Fact] public void EventStreamStore_RemainsNull_WhenNothingIsRegistered() { Builder.Services .AddMcpServer() .WithHttpTransport(); using var app = Builder.Build(); var options = app.Services.GetRequiredService>().Value; Assert.Null(options.EventStreamStore); } [Fact] public void EventStreamStore_CanBeOverriddenToNull_AfterDIRegistration() { Builder.Services.AddDistributedMemoryCache(); Builder.Services .AddMcpServer() .WithHttpTransport() .WithDistributedCacheEventStreamStore(); Builder.Services.Configure(options => options.EventStreamStore = null); using var app = Builder.Build(); var options = app.Services.GetRequiredService>().Value; Assert.Null(options.EventStreamStore); } [Fact] public void SessionMigrationHandler_IsPopulatedFromDI_ViaPostConfigure() { var handler = new StubSessionMigrationHandler(); Builder.Services.AddSingleton(handler); Builder.Services .AddMcpServer() .WithHttpTransport(); using var app = Builder.Build(); var options = app.Services.GetRequiredService>().Value; Assert.Same(handler, options.SessionMigrationHandler); } [Fact] public void SessionMigrationHandler_ExplicitOption_TakesPrecedenceOverDI() { var diHandler = new StubSessionMigrationHandler(); var explicitHandler = new StubSessionMigrationHandler(); Builder.Services.AddSingleton(diHandler); Builder.Services .AddMcpServer() .WithHttpTransport(options => options.SessionMigrationHandler = explicitHandler); using var app = Builder.Build(); var options = app.Services.GetRequiredService>().Value; Assert.Same(explicitHandler, options.SessionMigrationHandler); } [Fact] public void SessionMigrationHandler_RemainsNull_WhenNothingIsRegistered() { Builder.Services .AddMcpServer() .WithHttpTransport(); using var app = Builder.Build(); var options = app.Services.GetRequiredService>().Value; Assert.Null(options.SessionMigrationHandler); } private sealed class StubSessionMigrationHandler : ISessionMigrationHandler { public ValueTask AllowSessionMigrationAsync( HttpContext context, string sessionId, CancellationToken cancellationToken = default) => new((InitializeRequestParams?)null); public ValueTask OnSessionInitializedAsync( HttpContext context, string sessionId, InitializeRequestParams initializeParams, CancellationToken cancellationToken = default) => default; } }