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

@ -562,6 +562,30 @@ describe("RemoteClientTransport (focused branch coverage)", () => {
).rejects.toThrow(/Transport is closed/);
});
it("does not wait for an SSE response on subscriptions/listen (long-lived stream)", async () => {
// A `subscriptions/listen` never yields a JSON-RPC response — only an
// acknowledged notification over the event channel — so send() must
// resolve promptly instead of hanging on the SSE response wait (#1630).
// With a short response timeout, a request that *did* wait would reject.
const t = makeTransport(
{
events: () => openEventsResponse(),
send: () => jsonResponse({ ok: true }),
},
{ sseResponseTimeoutMs: 50 },
);
await t.start();
await expect(
t.send({
jsonrpc: "2.0",
id: "listen:0",
method: "subscriptions/listen",
params: {},
}),
).resolves.toBeUndefined();
await t.close();
});
it("posts a message including relatedRequestId and succeeds", async () => {
let sentBody: { relatedRequestId?: unknown } | undefined;
const encoder = new TextEncoder();

View file

@ -19,7 +19,10 @@ import { join } from "node:path";
import { serve } from "@hono/node-server";
import type { ServerType } from "@hono/node-server";
import type pinoType from "pino";
import { createRemoteApp } from "@inspector/core/mcp/remote/node/server.js";
import {
createRemoteApp,
requestIdForSendWait,
} from "@inspector/core/mcp/remote/node/server.js";
import {
InMemorySecretStore,
KeychainUnavailableError,
@ -108,6 +111,34 @@ function makeCapturingLogger(): {
}
describe("server.ts supplemental coverage", () => {
describe("requestIdForSendWait (#1630)", () => {
it("returns the id for an ordinary request", () => {
expect(
requestIdForSendWait({ jsonrpc: "2.0", id: 7, method: "tools/list" }),
).toBe(7);
});
it("does not wait for a response on subscriptions/listen (long-lived stream)", () => {
expect(
requestIdForSendWait({
jsonrpc: "2.0",
id: "listen:0",
method: "subscriptions/listen",
params: {},
}),
).toBeUndefined();
});
it("returns undefined for a notification (no id)", () => {
expect(
requestIdForSendWait({
jsonrpc: "2.0",
method: "notifications/initialized",
}),
).toBeUndefined();
});
});
describe("/api/log forwardLogEvent shapes", () => {
let h: Harness;
let records: Array<{ level: string; args: unknown[] }>;