How is an MCP subscriptions/listen stream cancelled or closed gracefully?
answer
- ends like any long-lived request
- closing it is itself the signal
- stdio needs an explicit message
- the server's polite goodbye is a result
- and that result is empty
basics
~20 sThe client ends it by closing the stream or by sending notifications/cancelled for that request. The server ends it gracefully by returning an empty SubscriptionsListenResult, which completes the original request. There is no unsubscribe method and no DELETE — both were removed in MCP 2026-07-28.
solid answer
~50 sThere are two directions and three moves. From the client side in MCP revision 2026-07-28, cancelling a subscription means either simply closing the response stream — on Streamable HTTP, closing the stream of a request IS cancellation and the server MUST treat it as such — or sending `notifications/cancelled` naming that request's id, which is the route stdio needs since there is only one shared channel to close. From the server side, a graceful shutdown of the stream is an **empty `SubscriptionsListenResult`**: the original `subscriptions/listen` request finally completes with an empty result, which tells the client the subscription ended cleanly rather than dropping. There is no `subscriptions/unlisten` method, and the HTTP `DELETE` that used to terminate a session was removed in 2026-07-28 along with sessions themselves. A client that wants a different filter closes the stream and opens a new `subscriptions/listen`.
code
json · 8 lines{
"jsonrpc": "2.0",
"method": "notifications/cancelled",
"params": {
"requestId": 7,
"reason": "user closed the workspace"
}
}go deeper
Know that the client ends a subscription by closing the stream or sending a cancellation, and that there is no separate unsubscribe call to learn.
Be able to name all three moves — close the stream, notifications/cancelled, and the server's empty SubscriptionsListenResult — and say which one stdio forces you to use.
Demonstrate the operational split: distinguish an orderly close from a fault, reconnect with backoff, re-list after any gap because nothing is replayed, and release server-side watchers on stream close.
Own the policy: bounded stream lifetimes ended with an empty result rather than silent drops, client backoff that a fleet-wide restart will not turn into a thundering herd, and where subscription bookkeeping lives when any node may serve any request.
## Three ways a subscription ends A `subscriptions/listen` stream in MCP revision **2026-07-28** is just a very long-lived JSON-RPC request, and it ends the way requests end. **Client closes the stream.** On the Streamable HTTP transport, closing the response stream of a request is cancellation, and the server MUST treat it that way. That applies to a listen stream exactly as it applies to a long-running `tools/call`. No extra message is required; the transport-level close is the signal. **Client sends `notifications/cancelled`.** This names the id of the original `subscriptions/listen` request and asks the server to stop. It is the route that matters on stdio, where there is a single shared stdin/stdout channel and no per-request stream to close — you cannot "close the stream" without killing the whole connection. It is also useful over HTTP when a client wants to cancel deliberately rather than by dropping a connection an intermediary might interpret differently. **Server completes the request with an empty `SubscriptionsListenResult`.** This is the graceful server-side close. The original request has been outstanding all along; the server finally answers it, and the empty result says "this subscription is over, cleanly". A server does this when it is shutting down, rotating a backend, or has decided the subscription should not continue. Because it is a normal result rather than an error, the client can distinguish an orderly end from a dropped connection. ## Why there is no unsubscribe method Earlier revisions had `resources/subscribe` and `resources/unsubscribe`; both were removed in 2026-07-28. Watching is now expressed entirely by the filter on the listen request, so "unsubscribe" is not a separate operation — it is a different filter, or no stream at all. Changing what you watch means ending the current stream and issuing a new `subscriptions/listen` with the filter you now want. Similarly, the HTTP `DELETE` that terminated a session in the 2025-11-25 and earlier era is gone, because protocol-level sessions are gone. A modern-only server should answer a `DELETE` on its MCP endpoint with `405 Method Not Allowed`, the same as a `GET`. ## What the client must handle A dropped stream and a gracefully closed stream call for different reactions, and a production client should tell them apart: - **Empty `SubscriptionsListenResult` received** — the server ended it deliberately. Reconnect if you still want notifications, but do so with backoff; hammering a server that is shutting down helps nobody. - **Stream broke without a result** — a network fault. There is no resumability in 2026-07-28: `Last-Event-ID` and SSE event ids were removed, so nothing is replayed. Open a brand-new `subscriptions/listen` request and, because notifications during the gap were lost, re-fetch the lists you care about to resynchronise. In both cases the new subscription is a new request with a new id and, in due course, a new `io.modelcontextprotocol/subscriptionId`. Late notifications carrying the old id should be discarded. ## Server-side obligations A server should not leak subscription bookkeeping when a client vanishes. Because closing the stream is cancellation, the server's stream-close handler is where it releases whatever it was watching — file watchers, database change feeds, upstream subscriptions. Over stdio, the equivalent trigger is `notifications/cancelled` for that request id, plus process exit. A server that wants to bound resource usage — say, a maximum stream lifetime — has a sanctioned way to do it: complete the request with an empty `SubscriptionsListenResult`. That is far better behaviour than silently dropping the connection, because the client sees an orderly end rather than a fault. ## Interview framing Answer in the two directions. Client: close the stream, or send `notifications/cancelled` (and say why stdio needs the latter). Server: empty `SubscriptionsListenResult`. Then add the two negatives that date your knowledge to the current revision: no `unsubscribe` method, and no `DELETE`-to-terminate, because there is no session to terminate. Finish with the recovery rule — no replay, so re-list after any unplanned break.
- Why does stdio need notifications/cancelled when HTTP can just close the stream?Because stdio has one shared stdin/stdout channel carrying every request; there is no per-request stream to close, and closing the channel would tear down the whole connection. So the client sends notifications/cancelled naming the listen request's id, which is the transport-neutral way to end an outstanding request.
- How does a client change which resources it is watching?It ends the current stream and opens a new subscriptions/listen with the new filter. There is no incremental update and no unsubscribe call — resources/subscribe and resources/unsubscribe were removed in 2026-07-28, so the filter on the listen request is the only expression of what the client watches.
- What is the difference for the client between an empty result and a broken stream?An empty SubscriptionsListenResult is an orderly server-side end, so the client can reconnect calmly with backoff. A break with no result is a fault: nothing is replayed, since resumability was removed in 2026-07-28, so the client opens a new listen request and re-fetches the lists it cares about to close the gap.
saying these in an interview costs you the question
- Calling subscriptions/unlisten to end the stream
- Sending HTTP DELETE to terminate the subscription
- Expecting a broken stream to resume where it left off
- Treating a graceful empty result as an error
- Assuming the server keeps watching after the client disconnects