From 342a3fa8ddc1eac870da35cce43a368684bfd8a0 Mon Sep 17 00:00:00 2001 From: tiancheng-Xu <271251549@qq.com> Date: Tue, 15 Sep 2026 10:32:45 -0400 Subject: [PATCH 1/2] fix(everything): scope session resources to server Ported from #4810 onto v2/main (session.ts only; the original resources.test.ts additions targeted main's unit-test file, which v2/main replaced with protocol-level suites). (cherry picked from commit 1f2a610c) Signed-off-by: cliffhall --- src/everything/resources/session.ts | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/everything/resources/session.ts b/src/everything/resources/session.ts index 38da832e19..c9d3477938 100644 --- a/src/everything/resources/session.ts +++ b/src/everything/resources/session.ts @@ -9,7 +9,10 @@ import { Resource, ResourceLink } from "@modelcontextprotocol/sdk/types.js"; * This prevents "Resource already registered" errors when a tool creates a resource * with the same URI multiple times during a session. */ -const registeredResources = new Map(); +const registeredResources = new WeakMap< + McpServer, + Map +>(); /** * Generates a session-scoped resource URI string based on the provided resource name. @@ -57,11 +60,16 @@ export const registerSessionResource = ( blob: payload, }; - // Check if a resource with this URI is already registered and remove it - const existingResource = registeredResources.get(uri); + const serverResources = + registeredResources.get(server) ?? new Map(); + registeredResources.set(server, serverResources); + + // Check if a resource with this URI is already registered on this server and remove it + const resourceKey = uri.toString(); + const existingResource = serverResources.get(resourceKey); if (existingResource) { existingResource.remove(); - registeredResources.delete(uri); + serverResources.delete(resourceKey); } // Register file resource @@ -77,7 +85,7 @@ export const registerSessionResource = ( ); // Track the registered resource for potential future removal - registeredResources.set(uri, registeredResource); + serverResources.set(resourceKey, registeredResource); return { type: "resource_link", ...resource }; }; From d4fb2f4f11efc084d97958dbecefa12c03020bd9 Mon Sep 17 00:00:00 2001 From: cliffhall Date: Sun, 4 Oct 2026 19:23:18 -0400 Subject: [PATCH 2/2] test(everything): assert per-session resources for #4808, drop KNOWN BUG marker The gzip-file-as-resource suite pinned #4808 (a second session's registration evicting the first session's resource of the same name). With session resources now tracked per server, the test asserts the correct behavior: both sessions read their own content and session A still lists the resource. The KNOWN BUG marker is removed, the testing skill's marker example no longer cites #4808, and a changeset records the fix. Co-Authored-By: Claude Opus 5.5 Signed-off-by: cliffhall --- ...everything-session-resources-per-server.md | 5 +++ .claude/skills/testing/SKILL.md | 2 +- .../__tests__/gzip-file-as-resource.test.ts | 31 +++++++++---------- src/everything/resources/session.ts | 7 +++-- 4 files changed, 24 insertions(+), 21 deletions(-) create mode 100644 .changeset/everything-session-resources-per-server.md diff --git a/.changeset/everything-session-resources-per-server.md b/.changeset/everything-session-resources-per-server.md new file mode 100644 index 0000000000..caea9ac495 --- /dev/null +++ b/.changeset/everything-session-resources-per-server.md @@ -0,0 +1,5 @@ +--- +"@modelcontextprotocol/server-everything": patch +--- + +Session resources are now tracked per server, so two sessions that create a session resource with the same name (for example via `gzip-file-as-resource`) no longer evict each other's resource (#4808). diff --git a/.claude/skills/testing/SKILL.md b/.claude/skills/testing/SKILL.md index d7eb4da0cc..7ba7883001 100644 --- a/.claude/skills/testing/SKILL.md +++ b/.claude/skills/testing/SKILL.md @@ -238,7 +238,7 @@ above the test (or above its `describe`/class when the whole block pins the same bug): ```ts -// KNOWN BUG #4808: pins current (wrong) behavior; the fix changes this assertion. +// KNOWN BUG #: pins current (wrong) behavior; the fix changes this assertion. ``` In Python the same text follows `#`. A bug with no issue yet reads diff --git a/src/everything/__tests__/gzip-file-as-resource.test.ts b/src/everything/__tests__/gzip-file-as-resource.test.ts index d762444521..2f6d318db0 100644 --- a/src/everything/__tests__/gzip-file-as-resource.test.ts +++ b/src/everything/__tests__/gzip-file-as-resource.test.ts @@ -3,9 +3,9 @@ * (#4854), with its default limits: fetching a `data:` or local `http:` URL, * returning the gzipped file as a resource link or an embedded resource, * registering it for later reads, and the errors for bad protocols, oversized - * or empty responses and failed fetches. It also pins #4808: two sessions that - * use the same file name evict each other's resource. The configurable limits - * are in `gzip-limits.test.ts`. + * or empty responses and failed fetches. It also guards #4808: two sessions + * that use the same file name each keep their own resource. The configurable + * limits are in `gzip-limits.test.ts`. */ import { createServer as createHttpServer, type Server } from "node:http"; import { once } from "node:events"; @@ -150,24 +150,21 @@ describe("gzip-file-as-resource", () => { expect(gunzipSync(blob).toString()).toBe("second"); }); - // KNOWN BUG #4808: pins current (wrong) behavior; the fix changes this assertion. - it("evicts another session's resource of the same name (#4808)", async () => { - // Characterization of #4808: session resources are tracked in one - // module-level map keyed by URI, so the second session's registration - // removes the first session's resource from the first session's server. + it("keeps each session's resource when two sessions use the same name (#4808)", async () => { + // Regression guard for #4808: session resources are tracked per server, + // so the second session's registration leaves the first session's + // resource (and its content) in place. const a = await open(); const b = await open(); + const second = `data:text/plain;base64,${Buffer.from("from b").toString("base64")}`; await gzip(a, { name: "shared.gz", data: DATA_URI }); - await gzip(b, { name: "shared.gz", data: DATA_URI }); + await gzip(b, { name: "shared.gz", data: second }); - await expect( - a.client.readResource({ uri: "demo://resource/session/shared.gz" }), - ).rejects.toThrow("Resource demo://resource/session/shared.gz not found"); - expect( - gunzipSync( - await readBlob(b, "demo://resource/session/shared.gz"), - ).toString(), - ).toBe(TEXT); + const uri = "demo://resource/session/shared.gz"; + expect(gunzipSync(await readBlob(a, uri)).toString()).toBe(TEXT); + expect(gunzipSync(await readBlob(b, uri)).toString()).toBe("from b"); + const { resources } = await a.client.listResources(); + expect(resources.map((r) => r.uri)).toContain(uri); }); it("rejects a URL that is not http, https or data", async () => { diff --git a/src/everything/resources/session.ts b/src/everything/resources/session.ts index c9d3477938..db0e02c259 100644 --- a/src/everything/resources/session.ts +++ b/src/everything/resources/session.ts @@ -5,9 +5,10 @@ import { import { Resource, ResourceLink } from "@modelcontextprotocol/sdk/types.js"; /** - * Tracks registered session resources by URI to allow updating/removing on re-registration. - * This prevents "Resource already registered" errors when a tool creates a resource - * with the same URI multiple times during a session. + * Tracks registered session resources per server, by URI, to allow updating/removing on + * re-registration. This prevents "Resource already registered" errors when a tool creates + * a resource with the same URI multiple times during a session, without touching another + * session's server that registered the same URI (#4808). */ const registeredResources = new WeakMap< McpServer,