A modern-era `subscriptions/listen` is a long-lived request: the first message on its stream must be `notifications/subscriptions/acknowledged`, and the JSON-RPC `result` for the listen id is reserved as the graceful-closure marker. A server that answers the listen request with a bare `result` is saying "acknowledged and closed in the same breath" — the SDK settles the subscription `graceful` and rejects the pending `listen()`. The Inspector read that rejection as an ordinary drop: eight re-lists over roughly a minute, the badge flickering `Reconnecting…`, and a final bare `Stream ended` that said nothing about why. Accepting the message was never wrong — it is a well-formed graceful-closure response. The silence was. - `core/mcp/subscriptionAck.ts` owns the predicate. The SDK gives this close no code of its own — it is the same `SdkError(ConnectionClosed)` any pre-ack close carries — so the predicate reads the message, since the alternative is refusing to retry a genuinely transient drop. - The condition is deterministic, so the stream ends on the first occurrence instead of spending the whole backoff run. Both routes are covered: the one a subscribe/unsubscribe reaches, and the one a reconnect reaches when an acknowledged stream drops and the server has since started refusing. - A `"never-acknowledged"` status of its own, not `"ended"` — `ended` means an expected close. It surfaces as the thrown error's message, an orange "Not acknowledged" badge, and a notice in the panel. - `subscriptions-never-acknowledged-http.json` reproduces it. It acknowledges the first listen that subscribes to a resource and refuses every one after, which is what makes the badge reachable by hand. Closes #2097 Signed-off-by: cliffhall <cliff@futurescale.com>
59 lines
2.3 KiB
TypeScript
59 lines
2.3 KiB
TypeScript
import type { ResourceSubscriptionStreamStatus } from "../../../../../../core/mcp/types.js";
|
|
import { NEVER_ACKNOWLEDGED_SUBSCRIPTION_MESSAGE } from "../../../../../../core/mcp/subscriptionAck.js";
|
|
|
|
export interface StreamPresentation {
|
|
/** Mantine palette color name conveying the status. */
|
|
color: string;
|
|
/** Short label shown on the panel badge. */
|
|
label: string;
|
|
/** Full explanation shown in the tooltip (both variants). */
|
|
tooltip: string;
|
|
}
|
|
|
|
const STREAM_INTRO =
|
|
"On modern (2026-07-28) servers, resource subscriptions are a filter over one long-lived subscriptions/listen stream.";
|
|
|
|
// Keyed by status so it's exhaustive at compile time: a new
|
|
// `ResourceSubscriptionStreamStatus` that isn't handled here is a type error
|
|
// (no unreachable `default` needed, so it stays fully covered).
|
|
const PRESENTATION: Record<
|
|
ResourceSubscriptionStreamStatus,
|
|
StreamPresentation
|
|
> = {
|
|
connecting: {
|
|
color: "blue",
|
|
label: "Connecting…",
|
|
tooltip: `${STREAM_INTRO} Opening the stream — waiting for the server to acknowledge the subscription.`,
|
|
},
|
|
acknowledged: {
|
|
color: "green",
|
|
label: "Listening",
|
|
tooltip: `${STREAM_INTRO} The server acknowledged the subscription and the stream is open, carrying resources/updated notifications.`,
|
|
},
|
|
reconnecting: {
|
|
color: "yellow",
|
|
label: "Reconnecting…",
|
|
tooltip: `${STREAM_INTRO} The stream dropped unexpectedly; re-listening to re-establish it (there is no resumability, so the full filter is re-sent).`,
|
|
},
|
|
ended: {
|
|
color: "gray",
|
|
label: "Stream ended",
|
|
tooltip: `${STREAM_INTRO} The stream is closed and won't reconnect on its own — either the server ended it (for example, on shutdown) or reconnection was abandoned after repeated failures. Re-subscribe to try again.`,
|
|
},
|
|
"never-acknowledged": {
|
|
color: "orange",
|
|
label: "Not acknowledged",
|
|
tooltip: `${STREAM_INTRO} ${NEVER_ACKNOWLEDGED_SUBSCRIPTION_MESSAGE} Re-subscribe to try again.`,
|
|
},
|
|
};
|
|
|
|
/**
|
|
* Maps a modern listen-stream status to its badge color, label, and tooltip
|
|
* copy (#1630). Kept in its own module so the badge component file exports only
|
|
* a component (react-refresh rule).
|
|
*/
|
|
export function subscriptionStreamPresentation(
|
|
status: ResourceSubscriptionStreamStatus,
|
|
): StreamPresentation {
|
|
return PRESENTATION[status];
|
|
}
|