skip to content

How is an MCP subscriptions/listen stream cancelled or closed gracefully?

level: seniorimportance: should knowfreq 40%

answer

  1. ends like any long-lived request
  2. closing it is itself the signal
  3. stdio needs an explicit message
  4. the server's polite goodbye is a result
  5. and that result is empty

basics

~20 s

The 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 s

There 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
json
{
  "jsonrpc": "2.0",
  "method": "notifications/cancelled",
  "params": {
    "requestId": 7,
    "reason": "user closed the workspace"
  }
}

go deeper

for a junior

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.

for a middle

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.

for a senior

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.

for a principal

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

context