## 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 #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.
2.8 KiB
| layout | title | nav_order | redirect_from | |
|---|---|---|---|---|
| default | Subscriptions | 12 |
|
Subscriptions
subscriptions/listen is the long-lived notification subscription stream of MCP 2026-07-28 (SEP-2575),
replacing the legacy HTTP GET listening stream.
Subscribing
The client opts in via the notifications filter (toolsListChanged / promptsListChanged / resourcesListChanged / resourceSubscriptions),
the server acknowledges the honored subset with notifications/subscriptions/acknowledged as the first stream message,
and every delivered notification carries the correlating io.modelcontextprotocol/subscriptionId in _meta.
The honored subset follows the capabilities the server declares (listChanged and subscribe flags).
The client opens the stream with a subscriptions/listen request:
{
"jsonrpc": "2.0",
"id": "listen-1",
"method": "subscriptions/listen",
"params": {
"notifications": { "toolsListChanged": true, "resourceSubscriptions": ["file:///a.txt"] },
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientInfo": { "name": "my_client", "version": "1.0.0" },
"io.modelcontextprotocol/clientCapabilities": {}
}
}
}
The first SSE event on the stream is the acknowledgement:
{
"jsonrpc": "2.0",
"method": "notifications/subscriptions/acknowledged",
"params": {
"notifications": { "toolsListChanged": true, "resourceSubscriptions": ["file:///a.txt"] },
"_meta": { "io.modelcontextprotocol/subscriptionId": "listen-1" }
}
}
Transport Support
The stream is served on the Streamable HTTP modern path; stdio answers -32601.
A host that cannot hold an SSE response open (such as the Rails controller pattern,
which buffers the body) declines the method with serve_subscriptions_listen: false, answering -32601 and dropping
the listChanged/subscribe flags from discovery.
Limits and Keepalives
Concurrent streams are capped by max_listen_subscriptions: (default 1000; pass nil to remove the cap), and each stream receives an SSE keepalive
comment frame every listen_keepalive_interval: seconds (default 15) so a dropped connection frees its slot; pass listen_keepalive_interval: nil
when an upstream proxy already keeps the stream alive.
transport = MCP::Server::Transports::StreamableHTTPTransport.new(
server,
max_listen_subscriptions: 500,
listen_keepalive_interval: 30,
)
A stream stays open until the client closes the connection; on graceful shutdown via transport.close,
each open stream receives its subscriptions/listen response before closing.
See Notifications for the notification types themselves and their session scoping.