@@ -8,11 +8,13 @@ module MCP
88 class Server
99 module Transports
1010 class StreamableHTTPTransport < Transport
11- def initialize ( server )
12- super
11+ def initialize ( server , stateless : false )
12+ super ( server )
1313 # { session_id => { stream: stream_object }
1414 @sessions = { }
1515 @mutex = Mutex . new
16+
17+ @stateless = stateless
1618 end
1719
1820 def handle_request ( request )
@@ -24,7 +26,7 @@ def handle_request(request)
2426 when "DELETE"
2527 handle_delete ( request )
2628 else
27- [ 405 , { "Content-Type" => "application/json" } , [ { error : "Method not allowed" } . to_json ] ]
29+ method_not_allowed_response
2830 end
2931 end
3032
@@ -35,6 +37,9 @@ def close
3537 end
3638
3739 def send_notification ( method , params = nil , session_id : nil )
40+ # Stateless mode doesn't support notifications
41+ raise "Stateless mode does not support notifications" if @stateless
42+
3843 notification = {
3944 jsonrpc : "2.0" ,
4045 method :,
@@ -119,6 +124,10 @@ def handle_post(request)
119124 end
120125
121126 def handle_get ( request )
127+ if @stateless
128+ return method_not_allowed_response
129+ end
130+
122131 session_id = extract_session_id ( request )
123132
124133 return missing_session_id_response unless session_id
@@ -128,6 +137,13 @@ def handle_get(request)
128137 end
129138
130139 def handle_delete ( request )
140+ success_response = [ 200 , { "Content-Type" => "application/json" } , [ { success : true } . to_json ] ]
141+
142+ if @stateless
143+ # Stateless mode doesn't support sessions, so we can just return a success response
144+ return success_response
145+ end
146+
131147 session_id = request . env [ "HTTP_MCP_SESSION_ID" ]
132148
133149 return [
@@ -137,7 +153,7 @@ def handle_delete(request)
137153 ] unless session_id
138154
139155 cleanup_session ( session_id )
140- [ 200 , { "Content-Type" => "application/json" } , [ { success : true } . to_json ] ]
156+ success_response
141157 end
142158
143159 def cleanup_session ( session_id )
@@ -177,21 +193,26 @@ def response?(body)
177193 end
178194
179195 def handle_initialization ( body_string , body )
180- session_id = SecureRandom . uuid
196+ session_id = nil
181197
182- @mutex . synchronize do
183- @sessions [ session_id ] = {
184- stream : nil ,
185- }
198+ unless @stateless
199+ session_id = SecureRandom . uuid
200+
201+ @mutex . synchronize do
202+ @sessions [ session_id ] = {
203+ stream : nil ,
204+ }
205+ end
186206 end
187207
188208 response = @server . handle_json ( body_string )
189209
190210 headers = {
191211 "Content-Type" => "application/json" ,
192- "Mcp-Session-Id" => session_id ,
193212 }
194213
214+ headers [ "Mcp-Session-Id" ] = session_id if session_id
215+
195216 [ 200 , headers , [ response ] ]
196217 end
197218
@@ -200,21 +221,32 @@ def handle_accepted
200221 end
201222
202223 def handle_regular_request ( body_string , session_id )
203- # If session ID is provided, but not in the sessions hash, return an error
204- if session_id && !@sessions . key? ( session_id )
205- return [ 400 , { "Content-Type" => "application/json" } , [ { error : "Invalid session ID" } . to_json ] ]
224+ unless @stateless
225+ # If session ID is provided, but not in the sessions hash, return an error
226+ if session_id && !@sessions . key? ( session_id )
227+ return [ 400 , { "Content-Type" => "application/json" } , [ { error : "Invalid session ID" } . to_json ] ]
228+ end
206229 end
207230
208231 response = @server . handle_json ( body_string )
232+
233+ # Stream can be nil since stateless mode doesn't retain streams
209234 stream = get_session_stream ( session_id ) if session_id
210235
211236 if stream
212237 send_response_to_stream ( stream , response , session_id )
238+ elsif response . nil? && notification_request? ( body_string )
239+ [ 202 , { "Content-Type" => "application/json" } , [ response ] ]
213240 else
214241 [ 200 , { "Content-Type" => "application/json" } , [ response ] ]
215242 end
216243 end
217244
245+ def notification_request? ( body_string )
246+ body = parse_request_body ( body_string )
247+ body . is_a? ( Hash ) && body [ "method" ] . start_with? ( "notifications/" )
248+ end
249+
218250 def get_session_stream ( session_id )
219251 @mutex . synchronize { @sessions [ session_id ] &.fetch ( :stream , nil ) }
220252 end
@@ -236,6 +268,10 @@ def session_exists?(session_id)
236268 @mutex . synchronize { @sessions . key? ( session_id ) }
237269 end
238270
271+ def method_not_allowed_response
272+ [ 405 , { "Content-Type" => "application/json" } , [ { error : "Method not allowed" } . to_json ] ]
273+ end
274+
239275 def missing_session_id_response
240276 [ 400 , { "Content-Type" => "application/json" } , [ { error : "Missing session ID" } . to_json ] ]
241277 end
0 commit comments