@@ -59,8 +59,13 @@ pub struct StreamableHttpServerConfig {
5959 pub sse_retry : Option < Duration > ,
6060 /// If true, the server will create a session for each request and keep it alive.
6161 /// When enabled, SSE priming events are sent to enable client reconnection.
62- pub stateful_mode : bool ,
63- /// When true and `stateful_mode` is false, the server prefers
62+ ///
63+ /// Only applies to legacy protocol versions (`< 2026-07-28`). Per SEP-2567,
64+ /// sessions are removed from the `2026-07-28` draft version, so requests
65+ /// negotiating that version are always served statelessly regardless of
66+ /// this setting.
67+ pub legacy_session_mode : bool ,
68+ /// When true and `legacy_session_mode` is false, the server prefers
6469 /// `Content-Type: application/json` for simple request-response tools.
6570 /// If the handler emits a notification or request before the final response,
6671 /// the server falls back to `text/event-stream` so no message is lost.
@@ -130,7 +135,7 @@ impl Default for StreamableHttpServerConfig {
130135 Self {
131136 sse_keep_alive : Some ( Duration :: from_secs ( 15 ) ) ,
132137 sse_retry : Some ( Duration :: from_secs ( 3 ) ) ,
133- stateful_mode : true ,
138+ legacy_session_mode : true ,
134139 json_response : false ,
135140 cancellation_token : CancellationToken :: new ( ) ,
136141 allowed_hosts : vec ! [ "localhost" . into( ) , "127.0.0.1" . into( ) , "::1" . into( ) ] ,
@@ -176,8 +181,8 @@ impl StreamableHttpServerConfig {
176181 self
177182 }
178183
179- pub fn with_stateful_mode ( mut self , stateful : bool ) -> Self {
180- self . stateful_mode = stateful ;
184+ pub fn with_legacy_session_mode ( mut self , legacy_session_mode : bool ) -> Self {
185+ self . legacy_session_mode = legacy_session_mode ;
181186 self
182187 }
183188
@@ -250,6 +255,57 @@ fn message_has_per_request_protocol_version(message: &ClientJsonRpcMessage) -> b
250255 }
251256}
252257
258+ #[ expect(
259+ clippy:: result_large_err,
260+ reason = "BoxResponse is intentionally large; matches other handlers in this file"
261+ ) ]
262+ // SEP-2567: sessions are removed from 2026-07-28; older versions are legacy.
263+ // Validates protocol-version consistency and returns `Ok(true)` only for a valid legacy request.
264+ fn is_legacy_request (
265+ message : Option < & ClientJsonRpcMessage > ,
266+ headers : & HeaderMap ,
267+ ) -> Result < bool , BoxResponse > {
268+ let has_per_request_version = message. is_some_and ( message_has_per_request_protocol_version) ;
269+ validate_protocol_version_header ( headers, has_per_request_version) ?;
270+ if let Some ( message) = message {
271+ if let ClientJsonRpcMessage :: Request ( req) = message {
272+ if let ClientRequest :: InitializeRequest ( init) = & req. request {
273+ validate_header_matches_init_body (
274+ headers,
275+ init. params . protocol_version . as_str ( ) ,
276+ Some ( req. id . clone ( ) ) ,
277+ ) ?;
278+ }
279+ }
280+ validate_request_protocol_version_meta ( headers, message) ?;
281+ }
282+
283+ let from_body = match message {
284+ Some ( ClientJsonRpcMessage :: Request ( req) ) => match & req. request {
285+ ClientRequest :: InitializeRequest ( init) => Some ( init. params . protocol_version . clone ( ) ) ,
286+ _ => req. request . get_meta ( ) . protocol_version ( ) ,
287+ } ,
288+ _ => None ,
289+ } ;
290+ let version = from_body
291+ . or_else ( || {
292+ headers
293+ . get ( HEADER_MCP_PROTOCOL_VERSION )
294+ . and_then ( |value| value. to_str ( ) . ok ( ) )
295+ . and_then ( |s| serde_json:: from_value ( serde_json:: Value :: String ( s. to_owned ( ) ) ) . ok ( ) )
296+ } )
297+ . unwrap_or ( ProtocolVersion :: V_2025_03_26 ) ;
298+ Ok ( version < ProtocolVersion :: V_2026_07_28 )
299+ }
300+
301+ fn method_not_allowed_response ( ) -> BoxResponse {
302+ Response :: builder ( )
303+ . status ( http:: StatusCode :: METHOD_NOT_ALLOWED )
304+ . header ( ALLOW , "POST" )
305+ . body ( Full :: new ( Bytes :: from ( "Method Not Allowed" ) ) . boxed ( ) )
306+ . expect ( "valid response" )
307+ }
308+
253309fn invalid_request_jsonrpc_response (
254310 id : Option < RequestId > ,
255311 message : impl Into < Cow < ' static , str > > ,
@@ -662,7 +718,7 @@ fn validate_origin_header(
662718///
663719/// ## Session management
664720///
665- /// When [`StreamableHttpServerConfig::stateful_mode `] is `true` (the default),
721+ /// When [`StreamableHttpServerConfig::legacy_session_mode `] is `true` (the default),
666722/// the server creates a session for each client that sends an `initialize`
667723/// request. The session ID is returned in the `Mcp-Session-Id` response header
668724/// and the client must include it on all subsequent requests.
@@ -1158,13 +1214,13 @@ where
11581214 return response;
11591215 }
11601216 let method = request. method ( ) . clone ( ) ;
1161- let allowed_methods = match self . config . stateful_mode {
1217+ let allowed_methods = match self . config . legacy_session_mode {
11621218 true => "GET, POST, DELETE" ,
11631219 false => "POST" ,
11641220 } ;
1165- let result = match ( method, self . config . stateful_mode ) {
1221+ let result = match ( method, self . config . legacy_session_mode ) {
11661222 ( Method :: POST , _) => self . handle_post ( request) . await ,
1167- // if we're not in stateful mode , we don't support GET or DELETE because there is no session
1223+ // if legacy session mode is disabled , we don't support GET or DELETE because there is no session
11681224 ( Method :: GET , true ) => self . handle_get ( request) . await ,
11691225 ( Method :: DELETE , true ) => self . handle_delete ( request) . await ,
11701226 _ => {
@@ -1187,6 +1243,9 @@ where
11871243 B : Body + Send + ' static ,
11881244 B :: Error : Display ,
11891245 {
1246+ if !is_legacy_request ( None , request. headers ( ) ) ? {
1247+ return Ok ( method_not_allowed_response ( ) ) ;
1248+ }
11901249 // check accept header
11911250 if !request
11921251 . headers ( )
@@ -1340,7 +1399,10 @@ where
13401399 Err ( response) => return Ok ( response) ,
13411400 } ;
13421401
1343- if self . config . stateful_mode {
1402+ let use_session =
1403+ self . config . legacy_session_mode && is_legacy_request ( Some ( & message) , & part. headers ) ?;
1404+
1405+ if use_session {
13441406 // do we have a session id?
13451407 let session_id = part
13461408 . headers
@@ -1673,6 +1735,9 @@ where
16731735 B : Body + Send + ' static ,
16741736 B :: Error : Display ,
16751737 {
1738+ if !is_legacy_request ( None , request. headers ( ) ) ? {
1739+ return Ok ( method_not_allowed_response ( ) ) ;
1740+ }
16761741 // check session id
16771742 let session_id = request
16781743 . headers ( )
0 commit comments