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 ;
1210using System . Security . Claims ;
1311using System . Security . Cryptography ;
14- using System . Text . Json ;
1512using System . Text . Json . Serialization . Metadata ;
1613
1714namespace ModelContextProtocol . AspNetCore ;
@@ -21,7 +18,6 @@ internal sealed class StreamableHttpHandler(
2118 IOptionsFactory < McpServerOptions > mcpServerOptionsFactory ,
2219 IOptions < HttpServerTransportOptions > httpServerTransportOptions ,
2320 StatefulSessionManager sessionManager ,
24- IDataProtectionProvider dataProtection ,
2521 ILoggerFactory loggerFactory ,
2622 IServiceProvider applicationServices )
2723{
@@ -30,8 +26,6 @@ internal sealed class StreamableHttpHandler(
3026
3127 public HttpServerTransportOptions HttpServerTransportOptions => httpServerTransportOptions . Value ;
3228
33- private IDataProtector Protector { get ; } = dataProtection . CreateProtector ( "Microsoft.AspNetCore.StreamableHttpHandler.StatelessSessionId" ) ;
34-
3529 public async Task HandlePostRequestAsync ( HttpContext context )
3630 {
3731 // The Streamable HTTP spec mandates the client MUST accept both application/json and text/event-stream.
@@ -118,17 +112,6 @@ public async Task HandleDeleteRequestAsync(HttpContext context)
118112 await WriteJsonRpcErrorAsync ( context , "Bad Request: Mcp-Session-Id header is required" , StatusCodes . Status400BadRequest ) ;
119113 return null ;
120114 }
121- else if ( HttpServerTransportOptions . Stateless )
122- {
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 ) ;
131- }
132115 else if ( ! sessionManager . TryGetValue ( sessionId , out session ) )
133116 {
134117 // -32001 isn't part of the MCP standard, but this is what the typescript-sdk currently does.
@@ -160,6 +143,13 @@ await WriteJsonRpcErrorAsync(context,
160143 {
161144 return await StartNewSessionAsync ( context ) ;
162145 }
146+ else if ( HttpServerTransportOptions . Stateless )
147+ {
148+ // In stateless mode, we should not be getting existing sessions via sessionId
149+ // This path should not be reached in stateless mode
150+ await WriteJsonRpcErrorAsync ( context , "Bad Request: The Mcp-Session-Id header is not supported in stateless mode" , StatusCodes . Status400BadRequest ) ;
151+ return null ;
152+ }
163153 else
164154 {
165155 return await GetSessionAsync ( context , sessionId ) ;
@@ -183,14 +173,12 @@ private async ValueTask<StreamableHttpSession> StartNewSessionAsync(HttpContext
183173 }
184174 else
185175 {
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)" ;
176+ // In stateless mode, each request is independent. Don't set any session ID on the transport.
177+ sessionId = "" ;
189178 transport = new ( )
190179 {
191180 Stateless = true ,
192181 } ;
193- ScheduleStatelessSessionIdWrite ( context , transport ) ;
194182 }
195183
196184 return await CreateSessionAsync ( context , transport , sessionId ) ;
@@ -199,21 +187,19 @@ private async ValueTask<StreamableHttpSession> StartNewSessionAsync(HttpContext
199187 private async ValueTask < StreamableHttpSession > CreateSessionAsync (
200188 HttpContext context ,
201189 StreamableHttpServerTransport transport ,
202- string sessionId ,
203- StatelessSessionId ? statelessId = null )
190+ string sessionId )
204191 {
205192 var mcpServerServices = applicationServices ;
206193 var mcpServerOptions = mcpServerOptionsSnapshot . Value ;
207- if ( statelessId is not null || HttpServerTransportOptions . ConfigureSessionOptions is not null )
194+ if ( HttpServerTransportOptions . Stateless || HttpServerTransportOptions . ConfigureSessionOptions is not null )
208195 {
209196 mcpServerOptions = mcpServerOptionsFactory . Create ( Options . DefaultName ) ;
210197
211- if ( statelessId is not null )
198+ if ( HttpServerTransportOptions . Stateless )
212199 {
213200 // The session does not outlive the request in stateless mode.
214201 mcpServerServices = context . RequestServices ;
215202 mcpServerOptions . ScopeRequests = false ;
216- mcpServerOptions . KnownClientInfo = statelessId . ClientInfo ;
217203 }
218204
219205 if ( HttpServerTransportOptions . ConfigureSessionOptions is { } configureSessionOptions )
@@ -225,7 +211,7 @@ private async ValueTask<StreamableHttpSession> CreateSessionAsync(
225211 var server = McpServerFactory . Create ( transport , mcpServerOptions , loggerFactory , mcpServerServices ) ;
226212 context . Features . Set ( server ) ;
227213
228- var userIdClaim = statelessId ? . UserIdClaim ?? GetUserIdClaim ( context . User ) ;
214+ var userIdClaim = GetUserIdClaim ( context . User ) ;
229215 var session = new StreamableHttpSession ( sessionId , transport , server , userIdClaim , sessionManager ) ;
230216
231217 var runSessionAsync = HttpServerTransportOptions . RunSessionHandler ?? RunSessionAsync ;
@@ -264,23 +250,6 @@ internal static string MakeNewSessionId()
264250 return WebEncoders . Base64UrlEncode ( buffer ) ;
265251 }
266252
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-
284253 internal static Task RunSessionAsync ( HttpContext httpContext , IMcpServer session , CancellationToken requestAborted )
285254 => session . RunAsync ( requestAborted ) ;
286255
0 commit comments