Skip to content
Open
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
46 changes: 42 additions & 4 deletions src/node/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand All @@ -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
Expand Down Expand Up @@ -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(
Expand Down
79 changes: 76 additions & 3 deletions test/icon-validation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand All @@ -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",
});
Expand Down Expand Up @@ -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", () => {
Expand Down Expand Up @@ -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");
});
});
});