@@ -129,6 +129,12 @@ class InvalidJsonError < StandardError; end
129129 # on a `subscriptions/listen` stream; the periodic write frees the stream's slot when the peer
130130 # has gone away. Defaults to `DEFAULT_LISTEN_KEEPALIVE_INTERVAL` (15); pass `nil` to disable
131131 # when an upstream proxy already keeps the stream alive.
132+ # @param serve_subscriptions_listen [Boolean] whether `subscriptions/listen` opens a stream.
133+ # A host that buffers responses and cannot serve an open SSE stream (e.g. the Rails controller pattern,
134+ # which builds a fresh transport per request and renders the body) passes `false`:
135+ # the method then answers 404 with JSON-RPC `-32601` like any unimplemented method,
136+ # and `Server#discover` stops advertising the `listChanged`/`subscribe` capability flags,
137+ # keeping the advertisement and the actual behavior in agreement. Defaults to `true`.
132138 # @param server_to_client_request_timeout [Numeric] seconds a server-to-client request waits for its
133139 # response before the transport stops waiting and raises `MCP::Server::RequestTimeoutError`.
134140 # Defaults to `DEFAULT_SERVER_TO_CLIENT_REQUEST_TIMEOUT` (600); individual calls override it with `timeout:`.
@@ -145,6 +151,7 @@ def initialize(
145151 max_request_bytes : DEFAULT_MAX_REQUEST_BYTES ,
146152 max_listen_subscriptions : DEFAULT_MAX_LISTEN_SUBSCRIPTIONS ,
147153 listen_keepalive_interval : DEFAULT_LISTEN_KEEPALIVE_INTERVAL ,
154+ serve_subscriptions_listen : true ,
148155 server_to_client_request_timeout : DEFAULT_SERVER_TO_CLIENT_REQUEST_TIMEOUT
149156 )
150157 super ( server )
@@ -211,6 +218,7 @@ def initialize(
211218 end
212219
213220 @listen_keepalive_interval = listen_keepalive_interval
221+ @serve_subscriptions_listen = serve_subscriptions_listen
214222
215223 unless server_to_client_request_timeout . is_a? ( Numeric ) && server_to_client_request_timeout . positive?
216224 raise ArgumentError , "server_to_client_request_timeout must be a positive number"
@@ -260,10 +268,12 @@ def call(env)
260268 handle_request ( Rack ::Request . new ( env ) )
261269 end
262270
263- # The `subscriptions/listen` notification stream (SEP-2575) is served on the modern path,
264- # so `Server#discover` may advertise `listChanged`/`subscribe` capability flags.
271+ # Whether this transport serves the `subscriptions/listen` notification stream (SEP-2575).
272+ # Gates both the route (a refusing transport answers the method as unimplemented) and
273+ # the `listChanged`/`subscribe` capability flags `Server#discover` advertises,
274+ # so the two always agree. Set via the `serve_subscriptions_listen:` constructor keyword.
265275 def serves_subscriptions_listen?
266- true
276+ @serve_subscriptions_listen
267277 end
268278
269279 def handle_request ( request )
@@ -723,8 +733,13 @@ def handle_modern(request, header_version, body_string: nil)
723733 return mismatch_error if mismatch_error
724734
725735 # `subscriptions/listen` is a long-lived notification stream served at the transport layer;
726- # it never dispatches through `Server#handle`.
727- return handle_subscriptions_listen ( body ) if body [ :method ] == Methods ::SUBSCRIPTIONS_LISTEN
736+ # it never dispatches through `Server#handle`. A transport constructed with
737+ # `serve_subscriptions_listen: false` skips the interception, so the method falls through
738+ # to the dispatcher as unimplemented (404 with `-32601`) - the refusal a host that cannot
739+ # serve an open SSE stream needs, instead of a `Proc` body it can never call.
740+ if body [ :method ] == Methods ::SUBSCRIPTIONS_LISTEN && serves_subscriptions_listen?
741+ return handle_subscriptions_listen ( body )
742+ end
728743
729744 session = modern_session
730745 notifications = @mutex . synchronize { @modern_request_sinks [ session . session_id ] = [ ] }
@@ -881,10 +896,34 @@ def too_many_listen_subscriptions_response(request_id)
881896 )
882897 end
883898
884- # The proc registers the stream and returns, leaving the response open like
899+ # The Rack streaming body of a `subscriptions/listen` response. It responds to `call`
900+ # and deliberately not to `each`, so Rack keeps classifying it as a streaming body;
901+ # `first` exists only to turn the buffered-host mistake (e.g. `render(json: body.first)`
902+ # in the Rails controller pattern) from a bare `NoMethodError` into guidance naming the fix.
903+ class ListenStreamBody
904+ def initialize ( &block )
905+ @block = block
906+ end
907+
908+ def call ( stream )
909+ @block . call ( stream )
910+ end
911+
912+ def first
913+ raise <<~MESSAGE
914+ subscriptions/listen returned a streaming SSE body, which cannot be buffered into a JSON response. \
915+ A host that cannot hold an SSE response open should construct the transport with `serve_subscriptions_listen: false`, \
916+ so the method is answered as unimplemented instead.
917+ See the Rails (controller) section at https://ruby.sdk.modelcontextprotocol.io/server/transports/ for the hosting patterns.
918+ MESSAGE
919+ end
920+ end
921+ private_constant :ListenStreamBody
922+
923+ # The body registers the stream and returns, leaving the response open like
885924 # the legacy GET stream (`create_sse_body`).
886925 def listen_sse_body ( request_id , honored )
887- proc do |stream |
926+ ListenStreamBody . new do |stream |
888927 rejected = false
889928 @mutex . synchronize do
890929 if @listen_subscriptions . key? ( request_id ) ||
0 commit comments