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 . Security . Claims ;
1210using System . Security . Cryptography ;
13- using System . Text . Json ;
1411using System . Text . Json . Serialization . Metadata ;
1512
1613namespace ModelContextProtocol . AspNetCore ;
@@ -20,7 +17,6 @@ internal sealed class StreamableHttpHandler(
2017 IOptionsFactory < McpServerOptions > mcpServerOptionsFactory ,
2118 IOptions < HttpServerTransportOptions > httpServerTransportOptions ,
2219 StatefulSessionManager sessionManager ,
23- IDataProtectionProvider dataProtection ,
2420 ILoggerFactory loggerFactory ,
2521 IServiceProvider applicationServices )
2622{
@@ -31,8 +27,6 @@ internal sealed class StreamableHttpHandler(
3127
3228 public HttpServerTransportOptions HttpServerTransportOptions => httpServerTransportOptions . Value ;
3329
34- private IDataProtector Protector { get ; } = dataProtection . CreateProtector ( "Microsoft.AspNetCore.StreamableHttpHandler.StatelessSessionId" ) ;
35-
3630 public async Task HandlePostRequestAsync ( HttpContext context )
3731 {
3832 // The Streamable HTTP spec mandates the client MUST accept both application/json and text/event-stream.
@@ -128,17 +122,6 @@ public async Task HandleDeleteRequestAsync(HttpContext context)
128122 await WriteJsonRpcErrorAsync ( context , "Bad Request: Mcp-Session-Id header is required" , StatusCodes . Status400BadRequest ) ;
129123 return null ;
130124 }
131- else if ( HttpServerTransportOptions . Stateless )
132- {
133- var sessionJson = Protector . Unprotect ( sessionId ) ;
134- var statelessSessionId = JsonSerializer . Deserialize ( sessionJson , StatelessSessionIdJsonContext . Default . StatelessSessionId ) ;
135- var transport = new StreamableHttpServerTransport
136- {
137- Stateless = true ,
138- SessionId = sessionId ,
139- } ;
140- session = await CreateSessionAsync ( context , transport , sessionId , statelessSessionId ) ;
141- }
142125 else if ( ! sessionManager . TryGetValue ( sessionId , out session ) )
143126 {
144127 // -32001 isn't part of the MCP standard, but this is what the typescript-sdk currently does.
@@ -170,6 +153,13 @@ await WriteJsonRpcErrorAsync(context,
170153 {
171154 return await StartNewSessionAsync ( context ) ;
172155 }
156+ else if ( HttpServerTransportOptions . Stateless )
157+ {
158+ // In stateless mode, we should not be getting existing sessions via sessionId
159+ // This path should not be reached in stateless mode
160+ await WriteJsonRpcErrorAsync ( context , "Bad Request: The Mcp-Session-Id header is not supported in stateless mode" , StatusCodes . Status400BadRequest ) ;
161+ return null ;
162+ }
173163 else
174164 {
175165 return await GetSessionAsync ( context , sessionId ) ;
@@ -193,14 +183,12 @@ private async ValueTask<StreamableHttpSession> StartNewSessionAsync(HttpContext
193183 }
194184 else
195185 {
196- // "(uninitialized stateless id)" is not written anywhere. We delay writing the MCP-Session-Id
197- // until after we receive the initialize request with the client info we need to serialize.
198- sessionId = "(uninitialized stateless id)" ;
186+ // In stateless mode, each request is independent. Don't set any session ID on the transport.
187+ sessionId = "" ;
199188 transport = new ( )
200189 {
201190 Stateless = true ,
202191 } ;
203- ScheduleStatelessSessionIdWrite ( context , transport ) ;
204192 }
205193
206194 return await CreateSessionAsync ( context , transport , sessionId ) ;
@@ -209,21 +197,19 @@ private async ValueTask<StreamableHttpSession> StartNewSessionAsync(HttpContext
209197 private async ValueTask < StreamableHttpSession > CreateSessionAsync (
210198 HttpContext context ,
211199 StreamableHttpServerTransport transport ,
212- string sessionId ,
213- StatelessSessionId ? statelessId = null )
200+ string sessionId )
214201 {
215202 var mcpServerServices = applicationServices ;
216203 var mcpServerOptions = mcpServerOptionsSnapshot . Value ;
217- if ( statelessId is not null || HttpServerTransportOptions . ConfigureSessionOptions is not null )
204+ if ( HttpServerTransportOptions . Stateless || HttpServerTransportOptions . ConfigureSessionOptions is not null )
218205 {
219206 mcpServerOptions = mcpServerOptionsFactory . Create ( Options . DefaultName ) ;
220207
221- if ( statelessId is not null )
208+ if ( HttpServerTransportOptions . Stateless )
222209 {
223210 // The session does not outlive the request in stateless mode.
224211 mcpServerServices = context . RequestServices ;
225212 mcpServerOptions . ScopeRequests = false ;
226- mcpServerOptions . KnownClientInfo = statelessId . ClientInfo ;
227213 }
228214
229215 if ( HttpServerTransportOptions . ConfigureSessionOptions is { } configureSessionOptions )
@@ -235,7 +221,7 @@ private async ValueTask<StreamableHttpSession> CreateSessionAsync(
235221 var server = McpServer . Create ( transport , mcpServerOptions , loggerFactory , mcpServerServices ) ;
236222 context . Features . Set ( server ) ;
237223
238- var userIdClaim = statelessId ? . UserIdClaim ?? GetUserIdClaim ( context . User ) ;
224+ var userIdClaim = GetUserIdClaim ( context . User ) ;
239225 var session = new StreamableHttpSession ( sessionId , transport , server , userIdClaim , sessionManager ) ;
240226
241227 var runSessionAsync = HttpServerTransportOptions . RunSessionHandler ?? RunSessionAsync ;
@@ -273,7 +259,6 @@ internal static string MakeNewSessionId()
273259 RandomNumberGenerator . Fill ( buffer ) ;
274260 return WebEncoders . Base64UrlEncode ( buffer ) ;
275261 }
276-
277262 internal static async Task < JsonRpcMessage ? > ReadJsonRpcMessageAsync ( HttpContext context )
278263 {
279264 // Implementation for reading a JSON-RPC message from the request body
@@ -290,22 +275,6 @@ internal static string MakeNewSessionId()
290275 return message ;
291276 }
292277
293- private void ScheduleStatelessSessionIdWrite ( HttpContext context , StreamableHttpServerTransport transport )
294- {
295- transport . OnInitRequestReceived = initRequestParams =>
296- {
297- var statelessId = new StatelessSessionId
298- {
299- ClientInfo = initRequestParams ? . ClientInfo ,
300- UserIdClaim = GetUserIdClaim ( context . User ) ,
301- } ;
302-
303- var sessionJson = JsonSerializer . Serialize ( statelessId , StatelessSessionIdJsonContext . Default . StatelessSessionId ) ;
304- transport . SessionId = Protector . Protect ( sessionJson ) ;
305- context . Response . Headers [ McpSessionIdHeaderName ] = transport . SessionId ;
306- return ValueTask . CompletedTask ;
307- } ;
308- }
309278
310279 internal static Task RunSessionAsync ( HttpContext httpContext , McpServer session , CancellationToken requestAborted )
311280 => session . RunAsync ( requestAborted ) ;
0 commit comments