@@ -46,10 +46,14 @@ type StreamableHTTPHandler struct {
4646type StreamableHTTPOptions struct {
4747 // GetSessionID provides the next session ID to use for an incoming request.
4848 //
49- // If GetSessionID returns an empty string, the session is 'stateless',
50- // meaning it is not persisted and no session validation is performed.
49+ // FIXME: update doc.
5150 GetSessionID func () string
5251
52+ // Stateless controls whether the session is 'stateless'.
53+ //
54+ // FIXME: update doc.
55+ Stateless bool
56+
5357 // TODO: support session retention (?)
5458
5559 // jsonResponse is forwarded to StreamableServerTransport.jsonResponse.
@@ -119,11 +123,12 @@ func (h *StreamableHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Reque
119123 }
120124
121125 var transport * StreamableServerTransport
122- if id := req .Header .Get (sessionIDHeader ); id != "" {
126+ sessionID := req .Header .Get (sessionIDHeader )
127+ if id := sessionID ; id != "" {
123128 h .mu .Lock ()
124129 transport , _ = h .transports [id ]
125130 h .mu .Unlock ()
126- if transport == nil {
131+ if transport == nil && ! h . opts . Stateless {
127132 http .Error (w , "session not found" , http .StatusNotFound )
128133 return
129134 }
@@ -132,22 +137,24 @@ func (h *StreamableHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Reque
132137 // TODO(rfindley): simplify the locking so that each request has only one
133138 // critical section.
134139 if req .Method == http .MethodDelete {
135- if transport == nil {
140+ if sessionID == "" {
136141 // => Mcp-Session-Id was not set; else we'd have returned NotFound above.
137142 http .Error (w , "DELETE requires an Mcp-Session-Id header" , http .StatusBadRequest )
138143 return
139144 }
140- h .mu .Lock ()
141- delete (h .transports , transport .SessionID )
142- h .mu .Unlock ()
143- transport .connection .Close ()
145+ if transport != nil { // transport may be nil in stateless mode
146+ h .mu .Lock ()
147+ delete (h .transports , transport .SessionID )
148+ h .mu .Unlock ()
149+ transport .connection .Close ()
150+ }
144151 w .WriteHeader (http .StatusNoContent )
145152 return
146153 }
147154
148155 switch req .Method {
149156 case http .MethodPost , http .MethodGet :
150- if req .Method == http .MethodGet && transport == nil {
157+ if req .Method == http .MethodGet && sessionID == "" {
151158 http .Error (w , "GET requires an active session" , http .StatusMethodNotAllowed )
152159 return
153160 }
@@ -164,37 +171,76 @@ func (h *StreamableHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Reque
164171 http .Error (w , "no server available" , http .StatusBadRequest )
165172 return
166173 }
167- sessionID := h .opts .GetSessionID ()
168- s := & StreamableServerTransport {SessionID : sessionID , jsonResponse : h .opts .jsonResponse }
174+ if sessionID == "" {
175+ // In stateless mode, sessionID may be nonempty even if there's no
176+ // existing transport.
177+ sessionID = h .opts .GetSessionID ()
178+ }
179+ transport = & StreamableServerTransport {
180+ SessionID : sessionID ,
181+ Stateless : h .opts .Stateless ,
182+ jsonResponse : h .opts .jsonResponse ,
183+ }
169184
170185 // To support stateless mode, we initialize the session with a default
171186 // state, so that it doesn't reject subsequent requests.
172187 var connectOpts * ServerSessionOptions
173- if sessionID == "" {
188+ if h .opts .Stateless {
189+ // Peek at the body to see if it is an initialize request.
190+ // We want that to be handled as usual.
191+ var hasInitialize , hasInitialized bool
192+
193+ // TODO: verify that this allows protocol version negotiation for
194+ // stateless servers.
195+ body , err := io .ReadAll (req .Body )
196+ if err != nil {
197+ http .Error (w , "failed to read body" , http .StatusBadRequest )
198+ return
199+ }
200+ // Reset the body to be read later.
201+ req .Body = io .NopCloser (bytes .NewBuffer (body ))
202+
203+ msgs , _ , err := readBatch (body )
204+ if err == nil {
205+ for _ , msg := range msgs {
206+ if req , ok := msg .(* jsonrpc.Request ); ok {
207+ switch req .Method {
208+ case methodInitialize :
209+ hasInitialize = true
210+ case notificationInitialized :
211+ hasInitialized = true
212+ }
213+ }
214+ }
215+ }
216+ state := new (ServerSessionState )
217+ if ! hasInitialize {
218+ state .InitializeParams = new (InitializeParams )
219+ }
220+ if ! hasInitialized {
221+ state .InitializedParams = new (InitializedParams )
222+ }
174223 connectOpts = & ServerSessionOptions {
175- State : & ServerSessionState {
176- InitializeParams : new (InitializeParams ),
177- InitializedParams : new (InitializedParams ),
178- },
224+ State : state ,
179225 }
180226 }
227+
181228 // Pass req.Context() here, to allow middleware to add context values.
182229 // The context is detached in the jsonrpc2 library when handling the
183230 // long-running stream.
184- ss , err := server .Connect (req .Context (), s , connectOpts )
231+ ss , err := server .Connect (req .Context (), transport , connectOpts )
185232 if err != nil {
186233 http .Error (w , "failed connection" , http .StatusInternalServerError )
187234 return
188235 }
189- if sessionID == "" {
236+ if h . opts . Stateless {
190237 // Stateless mode: close the session when the request exits.
191238 defer ss .Close () // close the fake session after handling the request
192239 } else {
193240 h .mu .Lock ()
194- h .transports [s .SessionID ] = s
241+ h .transports [transport .SessionID ] = transport
195242 h .mu .Unlock ()
196243 }
197- transport = s
198244 }
199245
200246 transport .ServeHTTP (w , req )
@@ -225,6 +271,9 @@ type StreamableServerTransport struct {
225271 // generator to produce one, as with [crypto/rand.Text].)
226272 SessionID string
227273
274+ // FIXME: doc
275+ Stateless bool
276+
228277 // Storage for events, to enable stream resumption.
229278 // If nil, a [MemoryEventStore] with the default maximum size will be used.
230279 EventStore EventStore
@@ -265,6 +314,7 @@ func (t *StreamableServerTransport) Connect(context.Context) (Connection, error)
265314 }
266315 t .connection = & streamableServerConn {
267316 sessionID : t .SessionID ,
317+ stateless : t .Stateless ,
268318 eventStore : t .EventStore ,
269319 jsonResponse : t .jsonResponse ,
270320 incoming : make (chan jsonrpc.Message , 10 ),
@@ -285,6 +335,7 @@ func (t *StreamableServerTransport) Connect(context.Context) (Connection, error)
285335
286336type streamableServerConn struct {
287337 sessionID string
338+ stateless bool
288339 jsonResponse bool
289340 eventStore EventStore
290341
@@ -759,6 +810,10 @@ func (c *streamableServerConn) Read(ctx context.Context) (jsonrpc.Message, error
759810
760811// Write implements the [Connection] interface.
761812func (c * streamableServerConn ) Write (ctx context.Context , msg jsonrpc.Message ) error {
813+ if req , ok := msg .(* jsonrpc.Request ); ok && req .ID .IsValid () && (c .stateless || c .sessionID == "" ) {
814+ // Requests aren't possible with stateless servers.
815+ return fmt .Errorf ("%w: stateless servers cannot make requests" , jsonrpc2 .ErrRejected )
816+ }
762817 // Find the incoming request that this write relates to, if any.
763818 var forRequest jsonrpc.ID
764819 isResponse := false
0 commit comments