From 2960ada2b48f135a6265a4d1c613df01908dc9dc Mon Sep 17 00:00:00 2001 From: Mukunda Rao Katta Date: Sun, 26 Apr 2026 14:26:49 -0700 Subject: [PATCH 1/2] perf(server): skip IdleTrackingBackgroundService timer in stateless mode --- .../IdleTrackingBackgroundService.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/ModelContextProtocol.AspNetCore/IdleTrackingBackgroundService.cs b/src/ModelContextProtocol.AspNetCore/IdleTrackingBackgroundService.cs index d68f83e5d..645253d6f 100644 --- a/src/ModelContextProtocol.AspNetCore/IdleTrackingBackgroundService.cs +++ b/src/ModelContextProtocol.AspNetCore/IdleTrackingBackgroundService.cs @@ -31,6 +31,17 @@ public IdleTrackingBackgroundService( _logger = logger; } + public override Task StartAsync(CancellationToken cancellationToken) + { + // In stateless mode there are no sessions to track, so skip starting the periodic timer entirely. + if (_options.Value.Stateless) + { + return Task.CompletedTask; + } + + return base.StartAsync(cancellationToken); + } + protected override async Task ExecuteAsync(CancellationToken stoppingToken) { try From 6fb82b8ceaefa39a6b012b863ee183503e720ec0 Mon Sep 17 00:00:00 2001 From: Mukunda Rao Katta Date: Sun, 26 Apr 2026 14:27:00 -0700 Subject: [PATCH 2/2] test: assert IdleTrackingBackgroundService timer is gated by Stateless option --- .../HttpMcpServerBuilderExtensionsTests.cs | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/HttpMcpServerBuilderExtensionsTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/HttpMcpServerBuilderExtensionsTests.cs index cc6ff0b13..ef385ed70 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/HttpMcpServerBuilderExtensionsTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/HttpMcpServerBuilderExtensionsTests.cs @@ -2,6 +2,7 @@ using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Options; using ModelContextProtocol.AspNetCore.Tests.Utils; using ModelContextProtocol.Protocol; @@ -184,6 +185,55 @@ public void SessionMigrationHandler_RemainsNull_WhenNothingIsRegistered() Assert.Null(options.SessionMigrationHandler); } + [Fact] + public async Task IdleTrackingBackgroundService_DoesNotStartTimer_WhenStateless() + { + Builder.Services + .AddMcpServer() + .WithHttpTransport(options => options.Stateless = true); + + using var app = Builder.Build(); + + var idleTrackingService = GetIdleTrackingService(app.Services); + Assert.NotNull(idleTrackingService); + + await idleTrackingService.StartAsync(TestContext.Current.CancellationToken); + + // BackgroundService.ExecuteTask is only set when ExecuteAsync has been kicked off via base.StartAsync. + // In stateless mode we early-return, so ExecuteTask should remain null. + Assert.Null(idleTrackingService.ExecuteTask); + + await idleTrackingService.StopAsync(TestContext.Current.CancellationToken); + } + + [Fact] + public async Task IdleTrackingBackgroundService_StartsTimer_WhenStateful() + { + Builder.Services + .AddMcpServer() + .WithHttpTransport(); + + using var app = Builder.Build(); + + var idleTrackingService = GetIdleTrackingService(app.Services); + Assert.NotNull(idleTrackingService); + + await idleTrackingService.StartAsync(TestContext.Current.CancellationToken); + + // In the default (stateful) mode the timer loop must start, so ExecuteTask should be set. + Assert.NotNull(idleTrackingService.ExecuteTask); + + await idleTrackingService.StopAsync(TestContext.Current.CancellationToken); + } + + private static BackgroundService? GetIdleTrackingService(IServiceProvider services) + { + // IdleTrackingBackgroundService is internal, so look it up by type name from the registered IHostedService instances. + return services.GetServices() + .OfType() + .FirstOrDefault(s => s.GetType().Name == "IdleTrackingBackgroundService"); + } + private sealed class StubSessionMigrationHandler : ISessionMigrationHandler { public ValueTask AllowSessionMigrationAsync(