Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/everything-5434-generated-resource-icons.md
Original file line number Diff line number Diff line change
@@ -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.
3 changes: 3 additions & 0 deletions src/everything/__tests__/gzip-file-as-resource.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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")}`;
Expand Down Expand Up @@ -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");
Expand All @@ -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,
Expand Down
37 changes: 36 additions & 1 deletion src/everything/__tests__/icons.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand All @@ -17,6 +18,8 @@ import {
connect,
connectModern,
connectRaw,
contentOf,
contentOfType,
MODERN_CAPABILITIES,
type RawSession,
type Session,
Expand All @@ -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();
Expand Down Expand Up @@ -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", () => {
Expand Down
4 changes: 3 additions & 1 deletion src/everything/resources/minted.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
};
}

Expand Down
6 changes: 4 additions & 2 deletions src/everything/tools/gzip-file-as-resource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -230,7 +230,9 @@ export const registerGZipFileAsResourceTool = (
const uri = getSessionResourceURI(name);
const blob = compressedBuffer.toString("base64");
const mimeType = "application/gzip";
const resource = <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 = <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") {
Expand Down
Loading