Make subscriptions/listen refusable for hosts that cannot stream - #533
Open
koic wants to merge 1 commit into
Open
Make subscriptions/listen refusable for hosts that cannot stream#533koic wants to merge 1 commit into
subscriptions/listen refusable for hosts that cannot stream#533koic wants to merge 1 commit into
Conversation
koic
force-pushed
the
make_subscriptions_listen_refusable
branch
3 times, most recently
from
August 24, 2026 17:52
b1a8edc to
392f15e
Compare
## Motivation and Context The documented Rails controller pattern builds a fresh transport per request and renders `body.first`, but a modern client's `subscriptions/listen` made `handle_request` return the streaming `Proc` body, so exactly the documented configuration answered every notification-stream request with a 500 - quietly, since tool calls and listings kept working. Nothing gated the route: `serves_subscriptions_listen?` was hardcoded `true` and consulted only by `Server#discover` for capability stripping, and the modern path deliberately ignores `stateless:`. `StreamableHTTPTransport.new` now accepts `serve_subscriptions_listen:` (default `true`, behavior unchanged). When `false`, the transport skips the listen interception so the method falls through the dispatcher as unimplemented - the spec's 404 with JSON-RPC `-32601`, in an Array body the controller pattern can render - and `serves_subscriptions_listen?` reflects the setting, so `Server#discover` stops advertising the `listChanged`/`subscribe` capability flags through its existing wiring and the advertisement agrees with the actual behavior. The documented controller example passes the flag and explains why; refusing implicitly in `stateless:` mode was rejected because a stateless transport mounted as a long-lived Rack app serves listen streams correctly (listen delivery deliberately precedes the stateless notification guard). On the default streaming side, the listen body is now a small streaming-body object rather than a bare `Proc`: it still responds to `call` and deliberately not to `each` (Rack would otherwise classify it as enumerable and break the streaming), while `first` raises an error naming `serve_subscriptions_listen: false` and pointing at the transports documentation, so a host that buffers the body the documented way fails with guidance instead of a bare `NoMethodError`. Fixes modelcontextprotocol#531. ## How Has This Been Tested? With the issue's reproduction script (default: a streaming body whose `body.first` raises the guidance; with the flag: 404, `application/json`, and an Array body whose first element is the `-32601` error), new transport tests covering the refusal shape, the capability stripping on `server/discover`, and the guidance raise with the body's streaming classification, the full suite, RuboCop, and the conformance suite, all green; the docs build passes every internal link and anchor check. ## Breaking Changes None. The keyword defaults to the current behavior; refusal is opt-in.
koic
force-pushed
the
make_subscriptions_listen_refusable
branch
from
August 24, 2026 17:53
392f15e to
3da0c10
Compare
atesgoral
approved these changes
Aug 24, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation and Context
The documented Rails controller pattern builds a fresh transport per request and renders
body.first, but a modern client'ssubscriptions/listenmadehandle_requestreturn the streamingProcbody, so exactly the documented configuration answered every notification-stream request with a 500 - quietly, since tool calls and listings kept working. Nothing gated the route:serves_subscriptions_listen?was hardcodedtrueand consulted only byServer#discoverfor capability stripping, and the modern path deliberately ignoresstateless:.StreamableHTTPTransport.newnow acceptsserve_subscriptions_listen:(defaulttrue, behavior unchanged). Whenfalse, the transport skips the listen interception so the method falls through the dispatcher as unimplemented - the spec's 404 with JSON-RPC-32601, in an Array body the controller pattern can render - andserves_subscriptions_listen?reflects the setting, soServer#discoverstops advertising thelistChanged/subscribecapability flags through its existing wiring and the advertisement agrees with the actual behavior. The documented controller example passes the flag and explains why; refusing implicitly instateless:mode was rejected because a stateless transport mounted as a long-lived Rack app serves listen streams correctly (listen delivery deliberately precedes the stateless notification guard).On the default streaming side, the listen body is now a small streaming-body object rather than a bare
Proc: it still responds tocalland deliberately not toeach(Rack would otherwise classify it as enumerable and break the streaming), whilefirstraises an error namingserve_subscriptions_listen: falseand pointing at the transports documentation, so a host that buffers the body the documented way fails with guidance instead of a bareNoMethodError.Fixes #531.
How Has This Been Tested?
With the issue's reproduction script (default: a streaming body whose
body.firstraises the guidance; with the flag: 404,application/json, and an Array body whose first element is the-32601error), new transport tests covering the refusal shape, the capability stripping onserver/discover, and the guidance raise with the body's streaming classification, the full suite, RuboCop, and the conformance suite, all green; the docs build passes every internal link and anchor check.Breaking Changes
None. The keyword defaults to the current behavior; refusal is opt-in.
Types of changes
Checklist