@@ -51,6 +51,7 @@ type StreamableHTTPHandler struct {
5151 onTransportDeletion func (sessionID string ) // for testing
5252
5353 mu sync.Mutex
54+ closed bool
5455 sessions map [string ]* sessionInfo // keyed by session ID
5556}
5657
@@ -219,27 +220,30 @@ func NewStreamableHTTPHandler(getServer func(*http.Request) *Server, opts *Strea
219220 return h
220221}
221222
222- // closeAll closes all ongoing sessions, for tests.
223- //
224- // TODO(rfindley): investigate the best API for callers to configure their
225- // session lifecycle. (?)
223+ // Close closes the handler, closing and removing all connected sessions,
224+ // and preventing new sessions from being added.
226225//
227- // Should we allow passing in a session store? That would allow the handler to
228- // be stateless.
229- func (h * StreamableHTTPHandler ) closeAll () {
230- // TODO: if we ever expose this outside of tests, we'll need to do better
231- // than simply collecting sessions while holding the lock: we need to prevent
232- // new sessions from being added.
233- //
234- // Currently, sessions remove themselves from h.sessions when closed, so we
235- // can't call Close while holding the lock.
226+ // Close is idempotent.
227+ func (h * StreamableHTTPHandler ) Close () error {
236228 h .mu .Lock ()
229+ if h .closed {
230+ h .mu .Unlock ()
231+ return nil
232+ }
233+ h .closed = true
237234 sessionInfos := slices .Collect (maps .Values (h .sessions ))
238- h .sessions = nil
235+ h .sessions = make ( map [ string ] * sessionInfo )
239236 h .mu .Unlock ()
240- for _ , s := range sessionInfos {
241- s .session .Close ()
237+
238+ for _ , info := range sessionInfos {
239+ info .session .Close ()
242240 }
241+ return nil
242+ }
243+
244+ // closeAll closes all ongoing sessions, for tests.
245+ func (h * StreamableHTTPHandler ) closeAll () {
246+ _ = h .Close ()
243247}
244248
245249// disablelocalhostprotection is a compatibility parameter that allows to disable
@@ -308,6 +312,14 @@ func (h *StreamableHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Reque
308312 }
309313 }
310314
315+ h .mu .Lock ()
316+ closed := h .closed
317+ h .mu .Unlock ()
318+ if closed {
319+ http .Error (w , "handler closed" , http .StatusServiceUnavailable )
320+ return
321+ }
322+
311323 // [§2.7] of the spec (2025-06-18): validate the MCP-Protocol-Version
312324 // header. If provided, it must be a supported version. If absent, the
313325 // version is unknown (the request may be an initialize for any version).
0 commit comments