Resources: subscriptions/listen model on modern servers (era fork) (#1630) (#1730)

Fork resource subscriptions by protocol era: legacy keeps resources/subscribe; modern (2026-07-28) models subscriptions as a filter over one long-lived subscriptions/listen stream with reconnect-by-re-listen (capped backoff), an optimistic Connecting→Listening header badge, and a fix so the browser proxy no longer blocks listen() ~60s. Closes #1630.
This commit is contained in:
Cliff Hall 2026-07-20 19:33:40 -04:00 • committed by GitHub
commit 320daf2f67
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 1656 additions and 90 deletions

View file

@ -333,4 +333,66 @@ describe("ResourceControls", () => {
await user.type(screen.getByPlaceholderText("Search..."), "special");
expect(screen.getByText("URIs (1)")).toBeInTheDocument();
});
describe("modern listen-stream chrome (#1630)", () => {
const activeAck = {
active: true as const,
status: "acknowledged" as const,
honoredUris: ["file:///config.json"],
};
it("shows the stream badge in the section header on the modern era", () => {
renderWithMantine(
<ControlledResourceControls
protocolEra="modern"
subscriptionStreamState={activeAck}
/>,
);
// The labelled badge sits in the accordion header (next to the count).
expect(screen.getByText("Listening")).toBeInTheDocument();
});
it("renders no stream chrome on the legacy era even when a stream is active", () => {
renderWithMantine(
<ControlledResourceControls subscriptionStreamState={activeAck} />,
);
expect(screen.queryByText("Listening")).not.toBeInTheDocument();
});
it("renders no stream chrome on the modern era when the stream is inactive", () => {
renderWithMantine(
<ControlledResourceControls
protocolEra="modern"
subscriptionStreamState={{
active: false,
status: "ended",
honoredUris: [],
}}
/>,
);
expect(
screen.queryByText(/Listening|Stream ended/),
).not.toBeInTheDocument();
});
it("hides the stream badge when a search filters out all subscriptions", async () => {
const user = userEvent.setup();
renderWithMantine(
<ControlledResourceControls
protocolEra="modern"
subscriptionStreamState={activeAck}
/>,
);
expect(screen.getByText("Listening")).toBeInTheDocument();
// A query matching none of the (live) subscriptions empties the section;
// the badge hides with it rather than sitting next to "Subscriptions (0)".
await user.type(
screen.getByPlaceholderText("Search..."),
"no-such-resource",
);
expect(screen.getByText("Subscriptions (0)")).toBeInTheDocument();
expect(screen.queryByText("Listening")).not.toBeInTheDocument();
});
});
});

View file

@ -1,11 +1,17 @@
import { Accordion, Group, Stack, TextInput, Title } from "@mantine/core";
import { Accordion, Group, Stack, Text, TextInput, Title } from "@mantine/core";
import { ClearButton } from "../../elements/ClearButton/ClearButton";
import { RiArrowRightSLine } from "react-icons/ri";
import type {
ProtocolEra,
Resource,
ResourceTemplateType as ResourceTemplate,
} from "@modelcontextprotocol/client";
import type { InspectorResourceSubscription } from "../../../../../../core/mcp/types.js";
import type {
InspectorResourceSubscription,
ResourceSubscriptionStreamState,
} from "../../../../../../core/mcp/types.js";
import { isModernEra } from "../../elements/EraBadge/eraUtils";
import { SubscriptionStreamBadge } from "../../elements/SubscriptionStreamBadge/SubscriptionStreamBadge";
import { ListChangedIndicator } from "../../elements/ListChangedIndicator/ListChangedIndicator";
import {
ListPaginationControls,
@ -26,6 +32,15 @@ export interface ResourceControlsProps {
* explicitly marks subscriptions unsupported.
*/
subscriptionsSupported?: boolean;
/**
* Modern-era `subscriptions/listen` stream state (#1630). When `active`
* (modern era with at least one subscription) the Subscriptions section shows
* a stream-status badge in its panel and a status dot in its header. Legacy
* connections pass `active: false` (or omit it) and see neither.
*/
subscriptionStreamState?: ResourceSubscriptionStreamState;
/** Negotiated protocol era; gates the modern subscription stream chrome. */
protocolEra?: ProtocolEra;
selectedUri?: string;
selectedTemplateUri?: string;
// Search text + accordion open-sections are controlled by the parent (App,
@ -71,6 +86,8 @@ export function ResourceControls({
templates,
subscriptions,
subscriptionsSupported = true,
subscriptionStreamState,
protocolEra,
selectedUri,
selectedTemplateUri,
searchText = "",
@ -106,6 +123,20 @@ export function ResourceControls({
s.resource.uri.toLowerCase().includes(query),
);
// Modern-era chrome for the single `subscriptions/listen` stream (#1630):
// a status badge in the section header (so it stays visible while the section
// is collapsed). Only shown on the modern era while the stream is active
// (≥1 subscription); the legacy per-URI `resources/subscribe` model has no
// persistent stream. Also gated on the *filtered* count so the badge hides
// alongside the section when a search matches none of the live subscriptions
// (rather than sitting next to a disabled "Subscriptions (0)" header).
const streamStatus =
isModernEra(protocolEra) &&
subscriptionStreamState?.active === true &&
filteredSubscriptions.length > 0
? subscriptionStreamState.status
: undefined;
// Subscriptions are only meaningful when the server advertises the
// `resources.subscribe` capability; otherwise the section is omitted
// entirely (no header, no panel) — see #1478.
@ -271,10 +302,17 @@ export function ResourceControls({
)}
>
<Accordion.Control disabled={filteredSubscriptions.length === 0}>
{formatSectionCount(
"Subscriptions",
filteredSubscriptions.length,
)}
<Group gap="xs" wrap="nowrap">
<Text span>
{formatSectionCount(
"Subscriptions",
filteredSubscriptions.length,
)}
</Text>
{streamStatus && (
<SubscriptionStreamBadge status={streamStatus} />
)}
</Group>
</Accordion.Control>
<Accordion.Panel>
<Stack gap="xs">