diff --git a/.changeset/everything-5434-generated-resource-icons.md b/.changeset/everything-5434-generated-resource-icons.md new file mode 100644 index 0000000000..d9bf697a26 --- /dev/null +++ b/.changeset/everything-5434-generated-resource-icons.md @@ -0,0 +1,5 @@ +--- +"@modelcontextprotocol/server-everything": patch +--- + +Give the resource `gzip-file-as-resource` creates the light and dark resource icons, like every other resource. A 2025-era client now sees them on the session resource's `resources/list` entry and on the returned link; a 2026-07-28 client sees them on the minted resource link. diff --git a/src/everything/__tests__/gzip-file-as-resource.test.ts b/src/everything/__tests__/gzip-file-as-resource.test.ts index ddfe33b30d..7f4c6032b2 100644 --- a/src/everything/__tests__/gzip-file-as-resource.test.ts +++ b/src/everything/__tests__/gzip-file-as-resource.test.ts @@ -22,6 +22,7 @@ import { textOf, type Session, } from "./harness.js"; +import { RESOURCE_ICONS } from "../server/icons.js"; const TEXT = "hello, gzip"; const DATA_URI = `data:text/plain;base64,${Buffer.from(TEXT).toString("base64")}`; @@ -100,6 +101,7 @@ describe("gzip-file-as-resource", () => { uri: "demo://resource/session/hello.txt.gz", name: "hello.txt.gz", mimeType: "application/gzip", + icons: RESOURCE_ICONS, }, ]); const blob = await readBlob(s, "demo://resource/session/hello.txt.gz"); @@ -114,6 +116,7 @@ describe("gzip-file-as-resource", () => { uri: "demo://resource/session/listed.gz", name: "listed.gz", mimeType: "application/gzip", + icons: RESOURCE_ICONS, }); expect( ofMethod(s.notifications, "notifications/resources/list_changed").length, diff --git a/src/everything/__tests__/icons.test.ts b/src/everything/__tests__/icons.test.ts index e55c63b7ee..f4b700bebf 100644 --- a/src/everything/__tests__/icons.test.ts +++ b/src/everything/__tests__/icons.test.ts @@ -6,7 +6,8 @@ * (server/icons.ts). These tests read what crosses the wire, in both * protocol eras: `serverInfo` from `initialize` (2025-era) or * `server/discover` (2026-07-28), and every entry of `tools/list`, - * `prompts/list`, `resources/list` and `resources/templates/list`. Each + * `prompts/list`, `resources/list` and `resources/templates/list`, and the + * resource link a tool returns for a resource it creates (#5434). Each * must carry a "light" and a "dark" icon, and each icon must be a scalable * SVG `data:` URI that decodes to an SVG document. */ @@ -17,6 +18,8 @@ import { connect, connectModern, connectRaw, + contentOf, + contentOfType, MODERN_CAPABILITIES, type RawSession, type Session, @@ -41,6 +44,9 @@ afterEach(async () => { const SVG_DATA_URI = "data:image/svg+xml;base64,"; +/** A small file for gzip-file-as-resource; a data: URI needs no network. */ +const DATA_URI = `data:text/plain;base64,${Buffer.from("icons").toString("base64")}`; + /** Assert that `icons` is a themed pair of decodable SVG icons. */ function expectThemedPair(icons: Icon[] | undefined): void { expect(icons).toBeDefined(); @@ -126,6 +132,35 @@ describe.each([ expect(template.icons).toEqual(RESOURCE_TEMPLATE_ICONS); } }); + + // #5434: a resource a tool creates is a resource too: its link carries the + // resource pair, whichever era's mechanism keeps it readable (a session + // resource for a 2025-era client, a minted URI on 2026-07-28). + it("puts the resource pair on the link gzip-file-as-resource returns", async () => { + session = await open(); + const result = await session.client.callTool({ + name: "gzip-file-as-resource", + arguments: { name: "icons.gz", data: DATA_URI }, + }); + const link = contentOfType(contentOf(result)[0], "resource_link"); + expect(link.icons).toEqual(RESOURCE_ICONS); + }); +}); + +// #5434: on the 2025 era the tool also registers the file as a session +// resource, which then appears in resources/list with the pair. +it("puts the resource pair on the session resource gzip-file-as-resource lists", async () => { + session = await connect({ capabilities: ALL_CAPABILITIES }); + await session.client.callTool({ + name: "gzip-file-as-resource", + arguments: { name: "listed-icons.gz", data: DATA_URI }, + }); + const { resources } = await session.client.listResources(); + const generated = resources.find((r) => r.name === "listed-icons.gz"); + expect(generated?.icons).toEqual(RESOURCE_ICONS); + for (const resource of resources) { + expect(resource.icons).toEqual(RESOURCE_ICONS); + } }); describe("icons on the raw 2026-07-28 wire", () => { diff --git a/src/everything/resources/minted.ts b/src/everything/resources/minted.ts index 05a9f71886..b9211a363c 100644 --- a/src/everything/resources/minted.ts +++ b/src/everything/resources/minted.ts @@ -20,7 +20,7 @@ import { ResourceTemplate, type ResourceLink, } from "@modelcontextprotocol/server"; -import { RESOURCE_TEMPLATE_ICONS } from "../server/icons.js"; +import { RESOURCE_ICONS, RESOURCE_TEMPLATE_ICONS } from "../server/icons.js"; /** How long a minted resource stays readable, in milliseconds. */ export const MINTED_RESOURCE_TTL_MS = 10 * 60_000; @@ -110,6 +110,8 @@ export function mintBlobResource( name, mimeType, description: `Created by a tool call; readable for ${MINTED_RESOURCE_TTL_MS / 60_000} minutes at this URI`, + // #5434: a minted resource is a resource like any other (#3131). + icons: RESOURCE_ICONS, }; } diff --git a/src/everything/tools/gzip-file-as-resource.ts b/src/everything/tools/gzip-file-as-resource.ts index fa0885e3d2..2ad289f34c 100644 --- a/src/everything/tools/gzip-file-as-resource.ts +++ b/src/everything/tools/gzip-file-as-resource.ts @@ -34,7 +34,7 @@ import { registerSessionResource, } from "../resources/session.js"; import type { ServerContext } from "../server/context.js"; -import { TOOL_ICONS } from "../server/icons.js"; +import { RESOURCE_ICONS, TOOL_ICONS } from "../server/icons.js"; // Maximum input file size - 10 MB default const GZIP_MAX_FETCH_SIZE = Number( @@ -230,7 +230,9 @@ export const registerGZipFileAsResourceTool = ( const uri = getSessionResourceURI(name); const blob = compressedBuffer.toString("base64"); const mimeType = "application/gzip"; - const resource = { uri, name, mimeType }; + // #5434: the session resource is listed like any other, so it carries + // the resource icons (#3131) in resources/list and in its link. + const resource = { uri, name, mimeType, icons: RESOURCE_ICONS }; // Return the resource or a resource link that can be used to access this resource later if (outputType === "resource") {