1
1
using Microsoft . AspNetCore . Http ;
2
2
using Microsoft . AspNetCore . Http . Features ;
3
3
using Microsoft . AspNetCore . Routing ;
4
+ using Microsoft . AspNetCore . Routing . Patterns ;
4
5
using Microsoft . AspNetCore . WebUtilities ;
5
6
using Microsoft . Extensions . DependencyInjection ;
7
+ using Microsoft . Extensions . Hosting ;
6
8
using Microsoft . Extensions . Logging ;
7
9
using Microsoft . Extensions . Options ;
8
10
using ModelContextProtocol . Protocol . Messages ;
9
11
using ModelContextProtocol . Protocol . Transport ;
10
12
using ModelContextProtocol . Server ;
11
13
using ModelContextProtocol . Utils . Json ;
12
14
using System . Collections . Concurrent ;
15
+ using System . Diagnostics . CodeAnalysis ;
13
16
using System . Security . Cryptography ;
14
17
15
18
namespace Microsoft . AspNetCore . Builder ;
@@ -23,53 +26,87 @@ public static class McpEndpointRouteBuilderExtensions
23
26
/// Sets up endpoints for handling MCP HTTP Streaming transport.
24
27
/// </summary>
25
28
/// <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>
27
32
/// <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 )
29
52
{
30
53
ConcurrentDictionary < string , SseResponseStreamTransport > _sessions = new ( StringComparer . Ordinal ) ;
31
54
32
55
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 > ( ) ;
34
59
35
- var routeGroup = endpoints . MapGroup ( "" ) ;
60
+ var routeGroup = endpoints . MapGroup ( pattern ) ;
36
61
37
62
routeGroup . MapGet ( "/sse" , async context =>
38
63
{
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 ;
41
68
69
+ var response = context . Response ;
42
70
response . Headers . ContentType = "text/event-stream" ;
43
71
response . Headers . CacheControl = "no-cache,no-store" ;
44
72
73
+ // Make sure we disable all response buffering for SSE
74
+ context . Response . Headers . ContentEncoding = "identity" ;
75
+ context . Features . GetRequiredFeature < IHttpResponseBodyFeature > ( ) . DisableBuffering ( ) ;
76
+
45
77
var sessionId = MakeNewSessionId ( ) ;
46
78
await using var transport = new SseResponseStreamTransport ( response . Body , $ "/message?sessionId={ sessionId } ") ;
47
79
if ( ! _sessions . TryAdd ( sessionId , transport ) )
48
80
{
49
81
throw new Exception ( $ "Unreachable given good entropy! Session with ID '{ sessionId } ' has already been created.") ;
50
82
}
51
83
52
- try
84
+ var options = optionsSnapshot . Value ;
85
+ if ( configureOptionsAsync is not null )
53
86
{
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
+ }
57
90
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 ) ;
60
94
61
95
try
62
96
{
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 ) ;
65
102
}
66
103
finally
67
104
{
68
105
await transport . DisposeAsync ( ) ;
69
106
await transportTask ;
70
107
}
71
108
}
72
- catch ( OperationCanceledException ) when ( requestAborted . IsCancellationRequested )
109
+ catch ( OperationCanceledException ) when ( cancellationToken . IsCancellationRequested )
73
110
{
74
111
// RequestAborted always triggers when the client disconnects before a complete response body is written,
75
112
// but this is how SSE connections are typically closed.
0 commit comments