From 198a7f15bed81b375a4349865d733153c8a3a1fa Mon Sep 17 00:00:00 2001 From: Aditya Singh Date: Sun, 24 May 2026 04:54:06 -0700 Subject: [PATCH] fix(validate): only recommend 512x512 icon size when icon differs mcpb validate always printed the 512x512 size recommendation for any valid PNG icon, because the icon check verified the PNG signature but never read the actual image dimensions. A correctly-sized 512x512 icon still got told to be 512x512. Read the width and height from the PNG IHDR chunk and only emit the recommendation when the icon is not already 512x512. The warning now also reports the icon's actual dimensions so authors know what to fix. Fixes #218 --- src/node/validate.ts | 46 +++++++++++++++++++-- test/icon-validation.test.ts | 79 ++++++++++++++++++++++++++++++++++-- 2 files changed, 118 insertions(+), 7 deletions(-) diff --git a/src/node/validate.ts b/src/node/validate.ts index b958798..1e6b85a 100644 --- a/src/node/validate.ts +++ b/src/node/validate.ts @@ -13,6 +13,8 @@ import { import { getManifestVersionFromRawData } from "../shared/manifestVersionResolve.js"; import { getAllFilesWithCount, readMcpbIgnorePatterns } from "./files.js"; +const RECOMMENDED_ICON_SIZE = 512; + /** * Check if a buffer contains a valid PNG file signature */ @@ -31,6 +33,33 @@ function isPNG(buffer: Buffer): boolean { ); } +/** + * Read the pixel dimensions of a PNG from its IHDR chunk. + * + * The IHDR chunk is required by the spec to be the first chunk and to + * immediately follow the 8-byte signature: a 4-byte length, the "IHDR" type, + * then the 4-byte big-endian width and height. Returns null if the buffer is + * too short or the IHDR chunk is not where the spec requires it to be. + */ +function getPNGDimensions( + buffer: Buffer, +): { width: number; height: number } | null { + // 8 (signature) + 4 (length) + 4 (type) + 4 (width) + 4 (height) = 24 + if (buffer.length < 24) { + return null; + } + + // Bytes 12-15 must spell "IHDR" for the width/height offsets to be valid. + if (buffer.toString("ascii", 12, 16) !== "IHDR") { + return null; + } + + return { + width: buffer.readUInt32BE(16), + height: buffer.readUInt32BE(20), + }; +} + /** * Validate icon field in manifest * @param iconPath - The icon path from manifest.json @@ -89,10 +118,19 @@ function validateIcon( `Icon file must be PNG format. The file at "${iconPath}" does not appear to be a valid PNG file.`, ); } else { - // File exists and is a valid PNG - add recommendation - warnings.push( - "Icon validation passed. Recommended size is 512×512 pixels for best display in Claude Desktop.", - ); + // File exists and is a valid PNG. Only recommend a different size + // when the icon is not already at the recommended dimensions. + const dimensions = getPNGDimensions(buffer); + if ( + dimensions && + (dimensions.width !== RECOMMENDED_ICON_SIZE || + dimensions.height !== RECOMMENDED_ICON_SIZE) + ) { + warnings.push( + `Icon is ${dimensions.width}×${dimensions.height} pixels. ` + + `Recommended size is ${RECOMMENDED_ICON_SIZE}×${RECOMMENDED_ICON_SIZE} pixels for best display in Claude Desktop.`, + ); + } } } catch (error) { errors.push( diff --git a/test/icon-validation.test.ts b/test/icon-validation.test.ts index bdc7bbe..b93ad2b 100644 --- a/test/icon-validation.test.ts +++ b/test/icon-validation.test.ts @@ -87,6 +87,61 @@ describe("Icon Validation", () => { ]); fs.writeFileSync(join(testFixturesDir, "valid-icon.png"), validPngBuffer); + // Create a valid PNG file declaring the recommended 512x512 dimensions. + // Only the signature and the IHDR width/height are read by validation, so + // a minimal chunk layout is enough to exercise the size check. + const recommendedSizePngBuffer = Buffer.from([ + 0x89, + 0x50, + 0x4e, + 0x47, + 0x0d, + 0x0a, + 0x1a, + 0x0a, // PNG signature + 0x00, + 0x00, + 0x00, + 0x0d, + 0x49, + 0x48, + 0x44, + 0x52, // IHDR chunk + 0x00, + 0x00, + 0x02, + 0x00, + 0x00, + 0x00, + 0x02, + 0x00, // 512x512 dimensions + 0x08, + 0x06, + 0x00, + 0x00, + 0x00, + 0x1f, + 0x15, + 0xc4, + 0x89, // IHDR data + CRC + 0x00, + 0x00, + 0x00, + 0x00, + 0x49, + 0x45, + 0x4e, + 0x44, // IEND chunk + 0xae, + 0x42, + 0x60, + 0x82, + ]); + fs.writeFileSync( + join(testFixturesDir, "recommended-size-icon.png"), + recommendedSizePngBuffer, + ); + // Create an invalid (non-PNG) file fs.writeFileSync( join(testFixturesDir, "invalid-icon.jpg"), @@ -102,6 +157,10 @@ describe("Icon Validation", () => { icon: "valid-icon.png", }); + createTestManifest("recommended-size-icon.json", { + icon: "recommended-size-icon.png", + }); + createTestManifest("invalid-remote-url.json", { icon: "https://example.com/icon.png", }); @@ -161,14 +220,28 @@ describe("Icon Validation", () => { } describe("Valid icon configurations", () => { - it("should pass validation with a valid local PNG icon", () => { + it("should recommend the standard size for a PNG that is not 512x512", () => { const manifestPath = join(testFixturesDir, "valid-local-icon.json"); const result = execSync(`node ${cliPath} validate ${manifestPath}`, { encoding: "utf-8", }); expect(result).toContain("Manifest schema validation passes!"); - expect(result).toContain("Icon validation passed"); + expect(result).toContain("Icon validation warnings"); + expect(result).toContain("Icon is 1×1 pixels"); + expect(result).toContain("Recommended size is 512×512 pixels"); + expect(result).not.toContain("ERROR"); + }); + + it("should not recommend a size when the PNG is already 512x512", () => { + const manifestPath = join(testFixturesDir, "recommended-size-icon.json"); + const result = execSync(`node ${cliPath} validate ${manifestPath}`, { + encoding: "utf-8", + }); + + expect(result).toContain("Manifest schema validation passes!"); + expect(result).not.toContain("Recommended size"); + expect(result).not.toContain("ERROR"); }); it("should pass validation when no icon is specified", () => { @@ -326,7 +399,7 @@ describe("Icon Validation", () => { }); expect(result).toContain("Manifest schema validation passes!"); - expect(result).toContain("Icon validation passed"); + expect(result).not.toContain("ERROR"); }); }); });