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
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
"@types/jest": "^29.5.14",
"@types/node": "^22.15.3",
"@types/node-forge": "1.3.11",
"@types/semver": "^7.7.1",
"@typescript-eslint/eslint-plugin": "^5.62.0",
"@typescript-eslint/parser": "^5.62.0",
"eslint": "^8.43.0",
Expand All @@ -91,6 +92,7 @@
"ignore": "^7.0.5",
"node-forge": "^1.3.2",
"pretty-bytes": "^5.6.0",
"semver": "^7.7.4",
"zod": "^3.25.67",
"zod-to-json-schema": "^3.24.6"
},
Expand Down
30 changes: 30 additions & 0 deletions src/node/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { DestroyerOfModules } from "galactus";
import * as os from "os";
import { dirname, extname, isAbsolute, join, resolve } from "path";
import prettyBytes from "pretty-bytes";
import semver from "semver";

import { unpackExtension } from "../cli/unpack.js";
import {
Expand Down Expand Up @@ -238,6 +239,25 @@ function validateCommandVariables(manifest: {
return { valid: errors.length === 0, errors, warnings };
}

/**
* Validate that the version field is a valid semantic version.
* Non-semver versions cause fatal crashes in Claude Desktop.
*/
function validateVersion(version: string): ValidationResult {
const errors: string[] = [];
const warnings: string[] = [];

if (!semver.valid(version)) {
errors.push(
`Version "${version}" is not valid semver. ` +
`Expected format: MAJOR.MINOR.PATCH (e.g., "1.0.0", "2.1.0-beta.1"). ` +
`Non-semver versions cause fatal crashes in Claude Desktop.`,
);
}

return { valid: errors.length === 0, errors, warnings };
}

// Sensitive file patterns not already covered by EXCLUDE_PATTERNS in files.ts
const SENSITIVE_PATTERNS = [
/(^|\/)credentials\.json$/i,
Expand Down Expand Up @@ -323,6 +343,16 @@ export function validateManifest(
: manifestDir;
let hasErrors = false;

// Validate version format (non-semver crashes Claude Desktop)
const versionValidation = validateVersion(manifestData.version);
if (versionValidation.errors.length > 0) {
console.log("\nERROR: Version validation failed:\n");
versionValidation.errors.forEach((error) => {
console.log(` - ${error}`);
});
hasErrors = true;
}

// Validate icon if present (always relative to manifest directory)
if (manifestData.icon) {
const iconValidation = validateIcon(manifestData.icon, manifestDir);
Expand Down
62 changes: 62 additions & 0 deletions test/validate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,68 @@ describe("Enhanced Validation", () => {
});
});

describe("version validation", () => {
it("should reject non-semver calver version", () => {
const dir = join(fixturesDir, "version-calver");
fs.mkdirSync(join(dir, "server"), { recursive: true });
fs.writeFileSync(join(dir, "server", "index.js"), "// fixture");
createManifest(dir, { version: "2024.03.31.01" });

try {
execSync(`node ${cliPath} validate ${dir}`, { encoding: "utf-8" });
fail("Expected validation to fail");
} catch (e) {
const error = e as ExecSyncError;
expect(error.status).toBe(1);
expect(error.stdout.toString()).toContain("not valid semver");
}
});

it("should reject non-numeric version string", () => {
const dir = join(fixturesDir, "version-string");
fs.mkdirSync(join(dir, "server"), { recursive: true });
fs.writeFileSync(join(dir, "server", "index.js"), "// fixture");
createManifest(dir, { version: "latest" });

try {
execSync(`node ${cliPath} validate ${dir}`, { encoding: "utf-8" });
fail("Expected validation to fail");
} catch (e) {
const error = e as ExecSyncError;
expect(error.status).toBe(1);
expect(error.stdout.toString()).toContain("not valid semver");
}
});

it("should accept standard semver version", () => {
const dir = join(fixturesDir, "version-semver");
fs.mkdirSync(join(dir, "server"), { recursive: true });
fs.writeFileSync(join(dir, "server", "index.js"), "// fixture");
createManifest(dir, { version: "1.0.0" });

const result = execSync(`node ${cliPath} validate ${dir}`, {
encoding: "utf-8",
});

expect(result).toContain("Manifest schema validation passes!");
expect(result).not.toContain("not valid semver");
});

it("should accept semver with prerelease and build metadata", () => {
const dir = join(fixturesDir, "version-prerelease");
fs.mkdirSync(join(dir, "server"), { recursive: true });
fs.writeFileSync(join(dir, "server", "index.js"), "// fixture");
createManifest(dir, { version: "2.1.0-beta.1+build.123" });

const result = execSync(`node ${cliPath} validate ${dir}`, {
encoding: "utf-8",
});

expect(result).toContain("Manifest schema validation passes!");
expect(result).not.toContain("not valid semver");
});
});

describe("happy path", () => {
it("should pass with all files present and correct types", () => {
const dir = join(fixturesDir, "happy-path");
Expand Down
18 changes: 18 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ __metadata:
"@types/jest": "npm:^29.5.14"
"@types/node": "npm:^22.15.3"
"@types/node-forge": "npm:1.3.11"
"@types/semver": "npm:^7.7.1"
"@typescript-eslint/eslint-plugin": "npm:^5.62.0"
"@typescript-eslint/parser": "npm:^5.62.0"
commander: "npm:^13.1.0"
Expand All @@ -39,6 +40,7 @@ __metadata:
node-forge: "npm:^1.3.2"
prettier: "npm:^3.3.3"
pretty-bytes: "npm:^5.6.0"
semver: "npm:^7.7.4"
ts-jest: "npm:^29.3.2"
typescript: "npm:^5.6.3"
zod: "npm:^3.25.67"
Expand Down Expand Up @@ -1257,6 +1259,13 @@ __metadata:
languageName: node
linkType: hard

"@types/semver@npm:^7.7.1":
version: 7.7.1
resolution: "@types/semver@npm:7.7.1"
checksum: 10c0/c938aef3bf79a73f0f3f6037c16e2e759ff40c54122ddf0b2583703393d8d3127130823facb880e694caa324eb6845628186aac1997ee8b31dc2d18fafe26268
languageName: node
linkType: hard

"@types/stack-utils@npm:^2.0.0":
version: 2.0.3
resolution: "@types/stack-utils@npm:2.0.3"
Expand Down Expand Up @@ -5394,6 +5403,15 @@ __metadata:
languageName: node
linkType: hard

"semver@npm:^7.7.4":
version: 7.7.4
resolution: "semver@npm:7.7.4"
bin:
semver: bin/semver.js
checksum: 10c0/5215ad0234e2845d4ea5bb9d836d42b03499546ddafb12075566899fc617f68794bb6f146076b6881d755de17d6c6cc73372555879ec7dce2c2feee947866ad2
languageName: node
linkType: hard

"set-function-length@npm:^1.2.2":
version: 1.2.2
resolution: "set-function-length@npm:1.2.2"
Expand Down
Loading