@@ -128,12 +128,13 @@ func (i *sessionInfo) stopTimer() {
128128type StreamableHTTPOptions struct {
129129 // Stateless controls whether the session is 'stateless'.
130130 //
131- // A stateless server does not validate the Mcp-Session-Id header, and uses a
131+ // A stateless server ignores the Mcp-Session-Id header, and uses a
132132 // temporary session with default initialization parameters. Any
133133 // server->client request is rejected immediately as there's no way for the
134134 // client to respond. Server->Client notifications may reach the client if
135135 // they are made in the context of an incoming request, as described in the
136136 // documentation for [StreamableServerTransport].
137+ // In Stateless mode DELETE requests will return 405 Method Not Allowed.
137138 Stateless bool
138139
139140 // TODO(#148): support session retention (?)
@@ -247,6 +248,16 @@ var disablelocalhostprotection = mcpgodebug.Value("disablelocalhostprotection")
247248// The option will be removed in the 1.8.0 version of the SDK.
248249var enableoriginverification = mcpgodebug .Value ("enableoriginverification" )
249250
251+ // allowsessionsinstateless is a compatibility parameter that restores the old
252+ // behavior of reading and using Mcp-Session-Id headers in stateless mode. When
253+ // set to "1", stateless servers will read the session ID from the request
254+ // header (or generate one via GetSessionID), set it on response headers, and
255+ // accept DELETE requests. When unset (the default), stateless servers ignore
256+ // session IDs entirely and reject DELETE with 405.
257+ // See the documentation for the mcpgodebug package for instructions how to enable it.
258+ // The option will be removed in the 1.9.0 version of the SDK.
259+ var allowsessionsinstateless = mcpgodebug .Value ("allowsessionsinstateless" )
260+
250261func (h * StreamableHTTPHandler ) ServeHTTP (w http.ResponseWriter , req * http.Request ) {
251262 // DNS rebinding protection: auto-enabled for localhost servers.
252263 // See: https://modelcontextprotocol.io/specification/2025-11-25/basic/security_best_practices#local-mcp-server-compromise
@@ -288,7 +299,17 @@ func (h *StreamableHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Reque
288299// serveStateless handles requests for stateless servers.
289300// Stateless servers only support POST. Each request creates a temporary
290301// session that is closed when the request completes.
302+ //
303+ // When the allowsessionsinstateless compatibility flag is set, DELETE is also
304+ // accepted (as a no-op) and session IDs are read from the request header.
291305func (h * StreamableHTTPHandler ) serveStateless (w http.ResponseWriter , req * http.Request ) {
306+ legacySessions := allowsessionsinstateless == "1"
307+
308+ if req .Method == http .MethodDelete && legacySessions {
309+ h .serveStatelessLegacyDELETE (w , req )
310+ return
311+ }
312+
292313 if req .Method != http .MethodPost {
293314 // RFC 9110 §15.5.6: 405 responses MUST include Allow header.
294315 w .Header ().Set ("Allow" , "POST" )
@@ -314,11 +335,12 @@ func (h *StreamableHTTPHandler) serveStateless(w http.ResponseWriter, req *http.
314335 return
315336 }
316337
317- // In stateless mode, the client may provide a session ID for application-
318- // level state correlation. If absent, generate one.
319- sessionID := req .Header .Get (sessionIDHeader )
320- if sessionID == "" {
321- sessionID = server .opts .GetSessionID ()
338+ var sessionID string
339+ if legacySessions {
340+ sessionID = req .Header .Get (sessionIDHeader )
341+ if sessionID == "" {
342+ sessionID = server .opts .GetSessionID ()
343+ }
322344 }
323345
324346 transport := & StreamableServerTransport {
@@ -345,6 +367,19 @@ func (h *StreamableHTTPHandler) serveStateless(w http.ResponseWriter, req *http.
345367 transport .ServeHTTP (w , req )
346368}
347369
370+ // serveStatelessLegacyDELETE handles DELETE requests in stateless mode when the
371+ // allowsessionsinstateless compatibility flag is set. DELETE requires a
372+ // Mcp-Session-Id header but is otherwise a no-op since stateless servers don't
373+ // persist sessions.
374+ func (h * StreamableHTTPHandler ) serveStatelessLegacyDELETE (w http.ResponseWriter , req * http.Request ) {
375+ sessionID := req .Header .Get (sessionIDHeader )
376+ if sessionID == "" {
377+ http .Error (w , "Bad Request: DELETE requires an Mcp-Session-Id header" , http .StatusBadRequest )
378+ return
379+ }
380+ w .WriteHeader (http .StatusNoContent )
381+ }
382+
348383// ephemeralConnectOpts peeks at the request body to determine whether it
349384// contains an initialize or initialized message. If not, default session state
350385// is constructed so that the session doesn't reject the request.
0 commit comments