1- using Microsoft . AspNetCore . DataProtection ;
2- using Microsoft . AspNetCore . Http ;
1+ using Microsoft . AspNetCore . Http ;
32using Microsoft . AspNetCore . Http . Features ;
43using Microsoft . AspNetCore . WebUtilities ;
54using Microsoft . Extensions . Logging ;
65using Microsoft . Extensions . Options ;
76using Microsoft . Net . Http . Headers ;
8- using ModelContextProtocol . AspNetCore . Stateless ;
97using ModelContextProtocol . Protocol ;
108using ModelContextProtocol . Server ;
119using System . IO . Pipelines ;
@@ -21,7 +19,6 @@ internal sealed class StreamableHttpHandler(
2119 IOptionsFactory < McpServerOptions > mcpServerOptionsFactory ,
2220 IOptions < HttpServerTransportOptions > httpServerTransportOptions ,
2321 StatefulSessionManager sessionManager ,
24- IDataProtectionProvider dataProtection ,
2522 ILoggerFactory loggerFactory ,
2623 IServiceProvider applicationServices )
2724{
@@ -30,8 +27,6 @@ internal sealed class StreamableHttpHandler(
3027
3128 public HttpServerTransportOptions HttpServerTransportOptions => httpServerTransportOptions . Value ;
3229
33- private IDataProtector Protector { get ; } = dataProtection . CreateProtector ( "Microsoft.AspNetCore.StreamableHttpHandler.StatelessSessionId" ) ;
34-
3530 public async Task HandlePostRequestAsync ( HttpContext context )
3631 {
3732 // The Streamable HTTP spec mandates the client MUST accept both application/json and text/event-stream.
@@ -120,14 +115,10 @@ public async Task HandleDeleteRequestAsync(HttpContext context)
120115 }
121116 else if ( HttpServerTransportOptions . Stateless )
122117 {
123- var sessionJson = Protector . Unprotect ( sessionId ) ;
124- var statelessSessionId = JsonSerializer . Deserialize ( sessionJson , StatelessSessionIdJsonContext . Default . StatelessSessionId ) ;
125- var transport = new StreamableHttpServerTransport
126- {
127- Stateless = true ,
128- SessionId = sessionId ,
129- } ;
130- session = await CreateSessionAsync ( context , transport , sessionId , statelessSessionId ) ;
118+ // In stateless mode, we should not be getting existing sessions via sessionId
119+ // This path should not be reached in stateless mode
120+ await WriteJsonRpcErrorAsync ( context , "Bad Request: Session management not supported in stateless mode" , StatusCodes . Status400BadRequest ) ;
121+ return null ;
131122 }
132123 else if ( ! sessionManager . TryGetValue ( sessionId , out session ) )
133124 {
@@ -154,6 +145,12 @@ await WriteJsonRpcErrorAsync(context,
154145
155146 private async ValueTask < StreamableHttpSession ? > GetOrCreateSessionAsync ( HttpContext context )
156147 {
148+ if ( HttpServerTransportOptions . Stateless )
149+ {
150+ // In stateless mode, always create a new session for each request
151+ return await StartNewSessionAsync ( context ) ;
152+ }
153+
157154 var sessionId = context . Request . Headers [ McpSessionIdHeaderName ] . ToString ( ) ;
158155
159156 if ( string . IsNullOrEmpty ( sessionId ) )
@@ -183,14 +180,13 @@ private async ValueTask<StreamableHttpSession> StartNewSessionAsync(HttpContext
183180 }
184181 else
185182 {
186- // "(uninitialized stateless id)" is not written anywhere. We delay writing the MCP-Session-Id
187- // until after we receive the initialize request with the client info we need to serialize.
188- sessionId = "(uninitialized stateless id)" ;
183+ // In stateless mode, don't set any session ID - each request is independent
184+ sessionId = MakeNewSessionId ( ) ; // Still need an internal ID for logging/tracking
189185 transport = new ( )
190186 {
191187 Stateless = true ,
192188 } ;
193- ScheduleStatelessSessionIdWrite ( context , transport ) ;
189+ // Do not set Mcp-Session-Id header in stateless mode
194190 }
195191
196192 return await CreateSessionAsync ( context , transport , sessionId ) ;
@@ -199,21 +195,19 @@ private async ValueTask<StreamableHttpSession> StartNewSessionAsync(HttpContext
199195 private async ValueTask < StreamableHttpSession > CreateSessionAsync (
200196 HttpContext context ,
201197 StreamableHttpServerTransport transport ,
202- string sessionId ,
203- StatelessSessionId ? statelessId = null )
198+ string sessionId )
204199 {
205200 var mcpServerServices = applicationServices ;
206201 var mcpServerOptions = mcpServerOptionsSnapshot . Value ;
207- if ( statelessId is not null || HttpServerTransportOptions . ConfigureSessionOptions is not null )
202+ if ( HttpServerTransportOptions . Stateless || HttpServerTransportOptions . ConfigureSessionOptions is not null )
208203 {
209204 mcpServerOptions = mcpServerOptionsFactory . Create ( Options . DefaultName ) ;
210205
211- if ( statelessId is not null )
206+ if ( HttpServerTransportOptions . Stateless )
212207 {
213208 // The session does not outlive the request in stateless mode.
214209 mcpServerServices = context . RequestServices ;
215210 mcpServerOptions . ScopeRequests = false ;
216- mcpServerOptions . KnownClientInfo = statelessId . ClientInfo ;
217211 }
218212
219213 if ( HttpServerTransportOptions . ConfigureSessionOptions is { } configureSessionOptions )
@@ -225,7 +219,7 @@ private async ValueTask<StreamableHttpSession> CreateSessionAsync(
225219 var server = McpServerFactory . Create ( transport , mcpServerOptions , loggerFactory , mcpServerServices ) ;
226220 context . Features . Set ( server ) ;
227221
228- var userIdClaim = statelessId ? . UserIdClaim ?? GetUserIdClaim ( context . User ) ;
222+ var userIdClaim = GetUserIdClaim ( context . User ) ;
229223 var session = new StreamableHttpSession ( sessionId , transport , server , userIdClaim , sessionManager ) ;
230224
231225 var runSessionAsync = HttpServerTransportOptions . RunSessionHandler ?? RunSessionAsync ;
@@ -264,23 +258,6 @@ internal static string MakeNewSessionId()
264258 return WebEncoders . Base64UrlEncode ( buffer ) ;
265259 }
266260
267- private void ScheduleStatelessSessionIdWrite ( HttpContext context , StreamableHttpServerTransport transport )
268- {
269- transport . OnInitRequestReceived = initRequestParams =>
270- {
271- var statelessId = new StatelessSessionId
272- {
273- ClientInfo = initRequestParams ? . ClientInfo ,
274- UserIdClaim = GetUserIdClaim ( context . User ) ,
275- } ;
276-
277- var sessionJson = JsonSerializer . Serialize ( statelessId , StatelessSessionIdJsonContext . Default . StatelessSessionId ) ;
278- transport . SessionId = Protector . Protect ( sessionJson ) ;
279- context . Response . Headers [ McpSessionIdHeaderName ] = transport . SessionId ;
280- return ValueTask . CompletedTask ;
281- } ;
282- }
283-
284261 internal static Task RunSessionAsync ( HttpContext httpContext , IMcpServer session , CancellationToken requestAborted )
285262 => session . RunAsync ( requestAborted ) ;
286263
0 commit comments