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
3 changed files with 1656 additions and 90 deletions

View file

@ -176,7 +176,7 @@ const subscriptionNotificationEntry: MessageEntry = {
jsonrpc: "2.0",
method: "notifications/resources/list_changed",
params: {
_meta: { "io.modelcontextprotocol/subscriptionId": "sub-abc123" },
_meta: { "io.modelcontextprotocol/subscriptionId": "listen:0" },
},
},
};
@ -231,7 +231,8 @@ export const ModernInputRequired: Story = {
},
};
// Modern-era: a `server/discover` probe, flagged with the "modern" frame badge.
// Modern-era: a `server/discover` probe (a modern-only method). The connection
// era is shown once in the panel header, not per entry.
export const ModernDiscoverFrame: Story = {
args: {
entry: discoverEntry,
@ -249,6 +250,25 @@ export const ModernSubscriptionNotification: Story = {
},
};
// Embedded (monitoring-sidebar) layout of a subscription notification: the
// `sub … listen-id` tag rides the top line (beside the direction) so the method
// badge on the line below gets the full narrow-column width (#1630).
export const EmbeddedSubscriptionNotification: Story = {
args: {
entry: subscriptionNotificationEntry,
isPinned: false,
isListExpanded: false,
embedded: true,
},
decorators: [
(Story) => (
<Box w={340}>
<Story />
</Box>
),
],
};
// Embedded (monitoring-sidebar) layout with a long resource URI: the target sits
// in a horizontal scroll area rather than truncating with an ellipsis. Wrapped
// in the pinned column's width so the overflow is visible.

View file

@ -471,14 +471,10 @@ describe("ProtocolEntry — modern vocabulary", () => {
expect(screen.queryByText("complete")).not.toBeInTheDocument();
});
it("flags a modern-only frame with a 'modern' badge", () => {
it("renders a modern-only frame's method (no per-frame era badge)", () => {
renderWithMantine(<ProtocolEntry {...baseProps} entry={discoverEntry} />);
expect(screen.getByText("server/discover")).toBeInTheDocument();
expect(screen.getByText("modern")).toBeInTheDocument();
});
it("does not flag an ordinary frame as modern", () => {
renderWithMantine(<ProtocolEntry {...baseProps} entry={successEntry} />);
// The connection era is shown once in the panel header, not per entry.
expect(screen.queryByText("modern")).not.toBeInTheDocument();
});
@ -494,6 +490,17 @@ describe("ProtocolEntry — modern vocabulary", () => {
).toBeGreaterThanOrEqual(1);
});
it("shows the subscriptionId in the embedded compact layout", () => {
renderWithMantine(
<ProtocolEntry
{...baseProps}
entry={subscriptionNotificationEntry}
embedded
/>,
);
expect(screen.getByText("sub-abc")).toBeInTheDocument();
});
it("shows the modern badges in the embedded compact layout", () => {
renderWithMantine(
<ProtocolEntry {...baseProps} entry={inputRequiredEntry} embedded />,

View file

@ -29,7 +29,6 @@ import {
extractMethod,
extractResultType,
extractSubscriptionId,
isModernFrame,
isReplayableProtocolMethod,
} from "../protocolUtils.js";
@ -109,15 +108,6 @@ const DurationText = Text.withProps({
c: "dimmed",
});
// Flags a frame whose method only exists in the modern (2026-07-28) era
// (`server/discover`, `subscriptions/listen`, its acknowledgement). This labels
// the *frame*, not the connection's negotiated era (see the transcript caveat in
// protocolUtils) — those methods genuinely cannot occur on a legacy connection.
const ModernFrameBadge = Badge.withProps({
color: "blue",
variant: "outline",
});
// The `subscriptionId` tag on a modern push notification (spec §7.4). Shown with
// a copy button so the id can be correlated against the `subscriptions/listen`
// stream that opened it.
@ -280,7 +270,6 @@ export function ProtocolEntry({
const canReplay = isReplayableProtocolMethod(method);
const resultType = extractResultType(entry);
const subscriptionId = extractSubscriptionId(entry);
const isModern = isModernFrame(method);
useEffect(() => {
setIsExpanded(isListExpanded);
@ -319,9 +308,6 @@ export function ProtocolEntry({
{statusLabel(status)}
</Badge>
);
const modernFrameBadge = isModern && (
<ModernFrameBadge>modern</ModernFrameBadge>
);
const subscriptionBadge = subscriptionId && (
<SubscriptionCluster>
<SubscriptionLabel>sub</SubscriptionLabel>
@ -350,13 +336,16 @@ export function ProtocolEntry({
{durationText}
{resultTypeBadge}
{statusBadge}
{/* The subscription-id tag rides the top line's trailing edge
(a notification row's duration/status slots are empty) so the
method badge on the line below gets the full column width and
doesn't truncate against the pin control (#1630). */}
{subscriptionBadge}
</ControlsCluster>
</HeaderRow>
<HeaderRow>
<HeaderCluster flex={1}>
<MethodBadge method={method} />
{modernFrameBadge}
{subscriptionBadge}
{target && (
<>
{resourceUri && <CopyButton value={resourceUri} />}
@ -395,7 +384,6 @@ export function ProtocolEntry({
{directionBadge}
<MethodBadge method={method} />
{specErrorBadge}
{modernFrameBadge}
{subscriptionBadge}
{target && (
<>