11using Microsoft . AspNetCore . Http ;
22using Microsoft . AspNetCore . Http . Features ;
33using Microsoft . AspNetCore . Routing ;
4+ using Microsoft . AspNetCore . Routing . Patterns ;
45using Microsoft . AspNetCore . WebUtilities ;
56using Microsoft . Extensions . DependencyInjection ;
7+ using Microsoft . Extensions . Hosting ;
68using Microsoft . Extensions . Logging ;
79using Microsoft . Extensions . Options ;
810using ModelContextProtocol . Protocol . Messages ;
911using ModelContextProtocol . Protocol . Transport ;
1012using ModelContextProtocol . Server ;
1113using ModelContextProtocol . Utils . Json ;
1214using System . Collections . Concurrent ;
15+ using System . Diagnostics . CodeAnalysis ;
1316using System . Security . Cryptography ;
1417
1518namespace Microsoft . AspNetCore . Builder ;
@@ -23,53 +26,87 @@ public static class McpEndpointRouteBuilderExtensions
2326 /// Sets up endpoints for handling MCP HTTP Streaming transport.
2427 /// </summary>
2528 /// <param name="endpoints">The web application to attach MCP HTTP endpoints.</param>
26- /// <param name="runSession">Provides an optional asynchronous callback for handling new MCP sessions.</param>
29+ /// <param name="pattern">The route pattern prefix to map to.</param>
30+ /// <param name="configureOptionsAsync">Configure per-session options.</param>
31+ /// <param name="runSessionAsync">Provides an optional asynchronous callback for handling new MCP sessions.</param>
2732 /// <returns>Returns a builder for configuring additional endpoint conventions like authorization policies.</returns>
28- public static IEndpointConventionBuilder MapMcp ( this IEndpointRouteBuilder endpoints , Func < HttpContext , IMcpServer , CancellationToken , Task > ? runSession = null )
33+ public static IEndpointConventionBuilder MapMcp (
34+ this IEndpointRouteBuilder endpoints ,
35+ [ StringSyntax ( "Route" ) ] string pattern = "" ,
36+ Func < HttpContext , McpServerOptions , CancellationToken , Task > ? configureOptionsAsync = null ,
37+ Func < HttpContext , IMcpServer , CancellationToken , Task > ? runSessionAsync = null )
38+ => endpoints . MapMcp ( RoutePatternFactory . Parse ( pattern ) , configureOptionsAsync , runSessionAsync ) ;
39+
40+ /// <summary>
41+ /// Sets up endpoints for handling MCP HTTP Streaming transport.
42+ /// </summary>
43+ /// <param name="endpoints">The web application to attach MCP HTTP endpoints.</param>
44+ /// <param name="pattern">The route pattern prefix to map to.</param>
45+ /// <param name="configureOptionsAsync">Configure per-session options.</param>
46+ /// <param name="runSessionAsync">Provides an optional asynchronous callback for handling new MCP sessions.</param>
47+ /// <returns>Returns a builder for configuring additional endpoint conventions like authorization policies.</returns>
48+ public static IEndpointConventionBuilder MapMcp ( this IEndpointRouteBuilder endpoints ,
49+ RoutePattern pattern ,
50+ Func < HttpContext , McpServerOptions , CancellationToken , Task > ? configureOptionsAsync = null ,
51+ Func < HttpContext , IMcpServer , CancellationToken , Task > ? runSessionAsync = null )
2952 {
3053 ConcurrentDictionary < string , SseResponseStreamTransport > _sessions = new ( StringComparer . Ordinal ) ;
3154
3255 var loggerFactory = endpoints . ServiceProvider . GetRequiredService < ILoggerFactory > ( ) ;
33- var mcpServerOptions = endpoints . ServiceProvider . GetRequiredService < IOptions < McpServerOptions > > ( ) ;
56+ var optionsSnapshot = endpoints . ServiceProvider . GetRequiredService < IOptions < McpServerOptions > > ( ) ;
57+ var optionsFactory = endpoints . ServiceProvider . GetRequiredService < IOptionsFactory < McpServerOptions > > ( ) ;
58+ var hostApplicationLifetime = endpoints . ServiceProvider . GetRequiredService < IHostApplicationLifetime > ( ) ;
3459
35- var routeGroup = endpoints . MapGroup ( "" ) ;
60+ var routeGroup = endpoints . MapGroup ( pattern ) ;
3661
3762 routeGroup . MapGet ( "/sse" , async context =>
3863 {
39- var response = context . Response ;
40- var requestAborted = context . RequestAborted ;
64+ // If the server is shutting down, we need to cancel all SSE connections immediately without waiting for HostOptions.ShutdownTimeout
65+ // which defaults to 30 seconds.
66+ using var sseCts = CancellationTokenSource . CreateLinkedTokenSource ( context . RequestAborted , hostApplicationLifetime . ApplicationStopping ) ;
67+ var cancellationToken = sseCts . Token ;
4168
69+ var response = context . Response ;
4270 response . Headers . ContentType = "text/event-stream" ;
4371 response . Headers . CacheControl = "no-cache,no-store" ;
4472
73+ // Make sure we disable all response buffering for SSE
74+ context . Response . Headers . ContentEncoding = "identity" ;
75+ context . Features . GetRequiredFeature < IHttpResponseBodyFeature > ( ) . DisableBuffering ( ) ;
76+
4577 var sessionId = MakeNewSessionId ( ) ;
4678 await using var transport = new SseResponseStreamTransport ( response . Body , $ "/message?sessionId={ sessionId } ") ;
4779 if ( ! _sessions . TryAdd ( sessionId , transport ) )
4880 {
4981 throw new Exception ( $ "Unreachable given good entropy! Session with ID '{ sessionId } ' has already been created.") ;
5082 }
5183
52- try
84+ var options = optionsSnapshot . Value ;
85+ if ( configureOptionsAsync is not null )
5386 {
54- // Make sure we disable all response buffering for SSE
55- context . Response . Headers . ContentEncoding = "identity" ;
56- context . Features . GetRequiredFeature < IHttpResponseBodyFeature > ( ) . DisableBuffering ( ) ;
87+ options = optionsFactory . Create ( Options . DefaultName ) ;
88+ await configureOptionsAsync . Invoke ( context , options , cancellationToken ) ;
89+ }
5790
58- var transportTask = transport . RunAsync ( cancellationToken : requestAborted ) ;
59- await using var server = McpServerFactory . Create ( transport , mcpServerOptions . Value , loggerFactory , endpoints . ServiceProvider ) ;
91+ try
92+ {
93+ var transportTask = transport . RunAsync ( cancellationToken ) ;
6094
6195 try
6296 {
63- runSession ??= RunSession ;
64- await runSession ( context , server , requestAborted ) ;
97+ await using var mcpServer = McpServerFactory . Create ( transport , options , loggerFactory , endpoints . ServiceProvider ) ;
98+ context . Features . Set ( mcpServer ) ;
99+
100+ runSessionAsync ??= RunSession ;
101+ await runSessionAsync ( context , mcpServer , cancellationToken ) ;
65102 }
66103 finally
67104 {
68105 await transport . DisposeAsync ( ) ;
69106 await transportTask ;
70107 }
71108 }
72- catch ( OperationCanceledException ) when ( requestAborted . IsCancellationRequested )
109+ catch ( OperationCanceledException ) when ( cancellationToken . IsCancellationRequested )
73110 {
74111 // RequestAborted always triggers when the client disconnects before a complete response body is written,
75112 // but this is how SSE connections are typically closed.
0 commit comments