Skip to content

Commit 669c36b

Browse files
Unify service configuration for ISseEventStreamStore, IMcpTaskStore, and ISessionMigrationHandler (#1362)
1 parent 3877560 commit 669c36b

15 files changed

Lines changed: 462 additions & 80 deletions
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
using Microsoft.Extensions.Caching.Distributed;
2+
using Microsoft.Extensions.Options;
3+
using ModelContextProtocol.Server;
4+
5+
namespace ModelContextProtocol.AspNetCore;
6+
7+
/// <summary>
8+
/// Configures <see cref="DistributedCacheEventStreamStoreOptions"/> by resolving
9+
/// the <see cref="IDistributedCache"/> from DI when not explicitly set.
10+
/// </summary>
11+
internal sealed class DistributedCacheEventStreamStoreOptionsSetup(IDistributedCache? cache = null) : IConfigureOptions<DistributedCacheEventStreamStoreOptions>
12+
{
13+
public void Configure(DistributedCacheEventStreamStoreOptions options)
14+
{
15+
options.Cache ??= cache;
16+
}
17+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
using Microsoft.Extensions.Caching.Distributed;
2+
using Microsoft.Extensions.DependencyInjection;
3+
using Microsoft.Extensions.Options;
4+
using ModelContextProtocol.Server;
5+
6+
namespace ModelContextProtocol.AspNetCore;
7+
8+
/// <summary>
9+
/// Validates that <see cref="DistributedCacheEventStreamStoreOptions.Cache"/> is set.
10+
/// </summary>
11+
internal sealed class DistributedCacheEventStreamStoreOptionsValidator : IValidateOptions<DistributedCacheEventStreamStoreOptions>
12+
{
13+
public ValidateOptionsResult Validate(string? name, DistributedCacheEventStreamStoreOptions options)
14+
{
15+
if (options.Cache is null)
16+
{
17+
return ValidateOptionsResult.Fail(
18+
$"The '{nameof(DistributedCacheEventStreamStoreOptions)}.{nameof(DistributedCacheEventStreamStoreOptions.Cache)}' property must be set. " +
19+
$"Register an {nameof(IDistributedCache)} in DI or set the {nameof(DistributedCacheEventStreamStoreOptions.Cache)} property " +
20+
$"in the '{nameof(HttpMcpServerBuilderExtensions.WithDistributedCacheEventStreamStore)}' configure callback.");
21+
}
22+
23+
return ValidateOptionsResult.Success;
24+
}
25+
}

src/ModelContextProtocol.AspNetCore/HttpMcpServerBuilderExtensions.cs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using Microsoft.AspNetCore.Authorization;
2+
using Microsoft.Extensions.Caching.Distributed;
23
using Microsoft.Extensions.DependencyInjection.Extensions;
34
using Microsoft.Extensions.Options;
45
using ModelContextProtocol.AspNetCore;
@@ -33,6 +34,7 @@ public static IMcpServerBuilder WithHttpTransport(this IMcpServerBuilder builder
3334
builder.Services.AddHostedService<IdleTrackingBackgroundService>();
3435

3536
builder.Services.TryAddEnumerable(ServiceDescriptor.Transient<IPostConfigureOptions<McpServerOptions>, AuthorizationFilterSetup>());
37+
builder.Services.TryAddEnumerable(ServiceDescriptor.Transient<IConfigureOptions<HttpServerTransportOptions>, HttpServerTransportOptionsSetup>());
3638

3739
if (configureOptions is not null)
3840
{
@@ -64,4 +66,37 @@ public static IMcpServerBuilder AddAuthorizationFilters(this IMcpServerBuilder b
6466

6567
return builder;
6668
}
69+
70+
/// <summary>
71+
/// Registers a <see cref="DistributedCacheEventStreamStore"/> as the <see cref="ISseEventStreamStore"/> for SSE resumability.
72+
/// </summary>
73+
/// <param name="builder">The builder instance.</param>
74+
/// <param name="configureOptions">An optional action to configure <see cref="DistributedCacheEventStreamStoreOptions"/>.</param>
75+
/// <returns>The builder provided in <paramref name="builder"/>.</returns>
76+
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception>
77+
/// <remarks>
78+
/// <para>
79+
/// An <see cref="IDistributedCache"/> implementation must be registered in the service collection before calling this method.
80+
/// The registered cache is automatically assigned to <see cref="DistributedCacheEventStreamStoreOptions.Cache"/>.
81+
/// </para>
82+
/// <para>
83+
/// To use a specific <see cref="IDistributedCache"/> instance instead of the one registered in DI,
84+
/// set the <see cref="DistributedCacheEventStreamStoreOptions.Cache"/> property in the <paramref name="configureOptions"/> callback.
85+
/// </para>
86+
/// </remarks>
87+
public static IMcpServerBuilder WithDistributedCacheEventStreamStore(this IMcpServerBuilder builder, Action<DistributedCacheEventStreamStoreOptions>? configureOptions = null)
88+
{
89+
ArgumentNullException.ThrowIfNull(builder);
90+
91+
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IConfigureOptions<DistributedCacheEventStreamStoreOptions>, DistributedCacheEventStreamStoreOptionsSetup>());
92+
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IValidateOptions<DistributedCacheEventStreamStoreOptions>, DistributedCacheEventStreamStoreOptionsValidator>());
93+
builder.Services.AddSingleton<ISseEventStreamStore, DistributedCacheEventStreamStore>();
94+
95+
if (configureOptions is not null)
96+
{
97+
builder.Services.Configure(configureOptions);
98+
}
99+
100+
return builder;
101+
}
67102
}

src/ModelContextProtocol.AspNetCore/HttpServerTransportOptions.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,29 @@ public class HttpServerTransportOptions
5555
/// <item><description>Replay missed events when a client reconnects with a Last-Event-ID header</description></item>
5656
/// <item><description>Send priming events to establish resumability before any actual messages</description></item>
5757
/// </list>
58+
/// <para>
59+
/// This can be set directly, or an <see cref="ISseEventStreamStore"/> can be registered in DI.
60+
/// If this property is not set, the server will attempt to resolve an <see cref="ISseEventStreamStore"/> from DI.
61+
/// </para>
5862
/// </remarks>
5963
public ISseEventStreamStore? EventStreamStore { get; set; }
6064

65+
/// <summary>
66+
/// Gets or sets the session migration handler for cross-instance session migration.
67+
/// </summary>
68+
/// <remarks>
69+
/// <para>
70+
/// When configured, the server will support session migration between instances.
71+
/// If a request arrives with a session ID that is not found locally, the handler
72+
/// is consulted to determine if the session can be migrated from another instance.
73+
/// </para>
74+
/// <para>
75+
/// This can be set directly, or an <see cref="ISessionMigrationHandler"/> can be registered in DI.
76+
/// If this property is not set, the server will attempt to resolve an <see cref="ISessionMigrationHandler"/> from DI.
77+
/// </para>
78+
/// </remarks>
79+
public ISessionMigrationHandler? SessionMigrationHandler { get; set; }
80+
6181
/// <summary>
6282
/// Gets or sets a value that indicates whether the server uses a single execution context for the entire session.
6383
/// </summary>
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
using Microsoft.Extensions.DependencyInjection;
2+
using Microsoft.Extensions.Options;
3+
using ModelContextProtocol.Server;
4+
5+
namespace ModelContextProtocol.AspNetCore;
6+
7+
/// <summary>
8+
/// Post-configures <see cref="HttpServerTransportOptions"/> by resolving services from DI
9+
/// when they haven't been explicitly set on the options.
10+
/// </summary>
11+
internal sealed class HttpServerTransportOptionsSetup(IServiceProvider serviceProvider) : IConfigureOptions<HttpServerTransportOptions>
12+
{
13+
public void Configure(HttpServerTransportOptions options)
14+
{
15+
options.EventStreamStore ??= serviceProvider.GetService<ISseEventStreamStore>();
16+
options.SessionMigrationHandler ??= serviceProvider.GetService<ISessionMigrationHandler>();
17+
}
18+
}

src/ModelContextProtocol.AspNetCore/StreamableHttpHandler.cs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,7 @@ internal sealed class StreamableHttpHandler(
2121
StatefulSessionManager sessionManager,
2222
IHostApplicationLifetime hostApplicationLifetime,
2323
IServiceProvider applicationServices,
24-
ILoggerFactory loggerFactory,
25-
ISessionMigrationHandler? sessionMigrationHandler = null)
24+
ILoggerFactory loggerFactory)
2625
{
2726
private const string McpSessionIdHeaderName = "Mcp-Session-Id";
2827
private const string McpProtocolVersionHeaderName = "MCP-Protocol-Version";
@@ -255,7 +254,7 @@ await WriteJsonRpcErrorAsync(context,
255254

256255
private async ValueTask<StreamableHttpSession?> TryMigrateSessionAsync(HttpContext context, string sessionId)
257256
{
258-
if (sessionMigrationHandler is not { } handler)
257+
if (HttpServerTransportOptions.SessionMigrationHandler is not { } handler)
259258
{
260259
return null;
261260
}
@@ -336,7 +335,7 @@ private async ValueTask<StreamableHttpSession> StartNewSessionAsync(HttpContext
336335
SessionId = sessionId,
337336
FlowExecutionContextFromRequests = !HttpServerTransportOptions.PerSessionExecutionContext,
338337
EventStreamStore = HttpServerTransportOptions.EventStreamStore,
339-
OnSessionInitialized = sessionMigrationHandler is { } handler
338+
OnSessionInitialized = HttpServerTransportOptions.SessionMigrationHandler is { } handler
340339
? (initParams, ct) => handler.OnSessionInitializedAsync(context, sessionId, initParams, ct)
341340
: null,
342341
};

src/ModelContextProtocol/McpServerOptionsSetup.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,12 @@ namespace ModelContextProtocol;
99
/// <param name="serverTools">The individually registered tools.</param>
1010
/// <param name="serverPrompts">The individually registered prompts.</param>
1111
/// <param name="serverResources">The individually registered resources.</param>
12+
/// <param name="taskStore">The optional task store registered in DI.</param>
1213
internal sealed class McpServerOptionsSetup(
1314
IEnumerable<McpServerTool> serverTools,
1415
IEnumerable<McpServerPrompt> serverPrompts,
15-
IEnumerable<McpServerResource> serverResources) : IConfigureOptions<McpServerOptions>
16+
IEnumerable<McpServerResource> serverResources,
17+
IMcpTaskStore? taskStore = null) : IConfigureOptions<McpServerOptions>
1618
{
1719
/// <summary>
1820
/// Configures the given McpServerOptions instance by setting server information
@@ -23,6 +25,8 @@ public void Configure(McpServerOptions options)
2325
{
2426
Throw.IfNull(options);
2527

28+
options.TaskStore ??= taskStore;
29+
2630
// Collect all of the provided tools into a tools collection. If the options already has
2731
// a collection, add to it, otherwise create a new one. We want to maintain the identity
2832
// of an existing collection in case someone has provided their own derived type, wants

src/ModelContextProtocol/McpServerServiceCollectionExtensions.cs

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -26,17 +26,6 @@ public static IMcpServerBuilder AddMcpServer(this IServiceCollection services, A
2626
services.Configure(configureOptions);
2727
}
2828

29-
// Register IMcpTaskStore from options if not already registered.
30-
// This allows users to either:
31-
// 1. Register IMcpTaskStore directly in DI (takes precedence)
32-
// 2. Set options.TaskStore in the configuration callback (used as fallback)
33-
// If neither is done, resolving IMcpTaskStore will throw.
34-
services.TryAddSingleton<IMcpTaskStore>(sp =>
35-
{
36-
var options = sp.GetRequiredService<IOptions<McpServerOptions>>().Value;
37-
return options.TaskStore ?? throw new InvalidOperationException("No IMcpTaskStore has been configured. Either register an IMcpTaskStore in the service collection or set McpServerOptions.TaskStore when configuring the MCP server.");
38-
});
39-
4029
return new DefaultMcpServerBuilder(services);
4130
}
4231
}

src/ModelContextProtocol/Server/DistributedCacheEventStreamStore.cs

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using Microsoft.Extensions.Caching.Distributed;
22
using Microsoft.Extensions.Logging;
33
using Microsoft.Extensions.Logging.Abstractions;
4+
using Microsoft.Extensions.Options;
45
using ModelContextProtocol.Protocol;
56
using System.Net.ServerSentEvents;
67
using System.Runtime.CompilerServices;
@@ -31,14 +32,16 @@ public sealed partial class DistributedCacheEventStreamStore : ISseEventStreamSt
3132
/// <summary>
3233
/// Initializes a new instance of the <see cref="DistributedCacheEventStreamStore"/> class.
3334
/// </summary>
34-
/// <param name="cache">The distributed cache to use for storage.</param>
35-
/// <param name="options">Optional configuration options for the store.</param>
35+
/// <param name="options">Configuration options for the store, including the <see cref="IDistributedCache"/> to use.</param>
3636
/// <param name="logger">Optional logger for diagnostic output.</param>
37-
public DistributedCacheEventStreamStore(IDistributedCache cache, DistributedCacheEventStreamStoreOptions? options = null, ILogger<DistributedCacheEventStreamStore>? logger = null)
37+
public DistributedCacheEventStreamStore(IOptions<DistributedCacheEventStreamStoreOptions> options, ILogger<DistributedCacheEventStreamStore>? logger = null)
3838
{
39-
Throw.IfNull(cache);
40-
_cache = cache;
41-
_options = options ?? new();
39+
Throw.IfNull(options);
40+
41+
var optionsValue = options.Value;
42+
_cache = optionsValue.Cache ?? throw new InvalidOperationException(
43+
$"The '{nameof(DistributedCacheEventStreamStoreOptions)}.{nameof(DistributedCacheEventStreamStoreOptions.Cache)}' property must be set.");
44+
_options = optionsValue;
4245
_logger = logger ?? NullLogger<DistributedCacheEventStreamStore>.Instance;
4346
}
4447

src/ModelContextProtocol/Server/DistributedCacheEventStreamStoreOptions.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,22 @@
1+
using Microsoft.Extensions.Caching.Distributed;
2+
13
namespace ModelContextProtocol.Server;
24

35
/// <summary>
46
/// Configuration options for <see cref="DistributedCacheEventStreamStore"/>.
57
/// </summary>
68
public sealed class DistributedCacheEventStreamStoreOptions
79
{
10+
/// <summary>
11+
/// Gets or sets the <see cref="IDistributedCache"/> to use for event storage.
12+
/// </summary>
13+
/// <remarks>
14+
/// When using dependency injection with <c>WithDistributedCacheEventStreamStore()</c>, this is
15+
/// automatically populated from the <see cref="IDistributedCache"/> registered in DI.
16+
/// Set this property explicitly to use a specific cache instance.
17+
/// </remarks>
18+
public IDistributedCache? Cache { get; set; }
19+
820
/// <summary>
921
/// Gets or sets the sliding expiration for individual events in the cache.
1022
/// </summary>

0 commit comments

Comments
 (0)