Skip to content

Modern subscriptions: say "the server closed the subscription without acknowledging it" instead of silently retrying eight times #2097

Description

@cliffhall

Split out of #2063, which resolved into a documentation/spec question everywhere except this one point.

The gap

A modern-era (2026-07-28) subscriptions/listen is a long-lived request. The first message on the stream must be notifications/subscriptions/acknowledged; the JSON-RPC result for the listen id is reserved as the graceful-closure marker (spec: Subscriptions → Graceful Closure).

A server that answers the listen request with a result immediately — never acknowledging — is therefore saying "acknowledged and closed in the same breath". In @modelcontextprotocol/client, Client._onresponse settles that subscription with cause graceful and rejects the pending acknowledgment promise, so the Inspector's re-listen rejects.

What the Inspector does with that today (core/mcp/inspectorClient.ts):

  • refreshModernSubscription rejects → onModernReconnectFailed()scheduleModernReconnect()
  • up to MODERN_RECONNECT_MAX_ATTEMPTS = 8 consecutive failures, with capped exponential backoff
  • then the stream badge lands on ended

From the user's side that is: the Subscriptions badge flickers reconnecting, eight subscriptions/listen requests with increasing SDK-assigned ids go out over roughly a minute, and it finally settles on ended — with no statement anywhere about why. That is exactly the report in #2063, where it read as the Inspector "accepting" an invalid response.

Expected

Say what happened. Something on the order of:

The server closed the subscription without acknowledging it. A subscriptions/listen request must first be answered with a notifications/subscriptions/acknowledged notification; a JSON-RPC result for the listen id means graceful closure.

Concretely:

  1. Distinguish the case. A graceful settlement that arrives before any acknowledgment is not the same as a graceful shutdown of an established stream. Only the latter is the expected ended path.
  2. Don't retry it. Eight re-lists against a server that will answer the same way each time is noise, not recovery — the condition is deterministic. This case should end the stream on the first occurrence.
  3. Surface the reason in the UI. The Subscriptions section badge already carries a status; the never-acknowledged case needs an accompanying message rather than a bare ended.
  4. Optionally note the missing resultType discriminator, which the reporter's payload also lacked and which the spec's example carries.

Notes

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingv2Issues and PRs for v2

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions