rebase generation
This commit is contained in:
141
tools/mcp/npm-validator.mjs
Normal file
141
tools/mcp/npm-validator.mjs
Normal file
@@ -0,0 +1,141 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* MCP server: npm package & semver validator (read-only).
|
||||
*
|
||||
* Tools:
|
||||
* - validate_semver : check that a version string is valid semver
|
||||
* and optionally reject ranges/latest
|
||||
* - check_lockfile_package : look up a package in the root package-lock.json
|
||||
* and return its locked version without touching
|
||||
* the registry
|
||||
*
|
||||
* No network access, no CLI invocations. Pure parsing over local files.
|
||||
*/
|
||||
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { runMCPServer } from "./lib/mcp-server.mjs";
|
||||
import { getProjectRoot } from "../lib/project-root.mjs";
|
||||
|
||||
const projectRoot = getProjectRoot();
|
||||
|
||||
// SemVer 2.0.0 regex from https://semver.org/ (BSD-licensed).
|
||||
const SEMVER_RE =
|
||||
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/;
|
||||
|
||||
function loadLockfile() {
|
||||
const lockPath = path.join(projectRoot, "package-lock.json");
|
||||
if (!fs.existsSync(lockPath)) return null;
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(lockPath, "utf8"));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const tools = [
|
||||
{
|
||||
name: "validate_semver",
|
||||
description:
|
||||
"Validate a version string as strict SemVer 2.0.0. When `rejectRanges` is true, reject caret/tilde/latest and require an exact pinned version (the KIS-TOiR version policy). Returns parsed components on success.",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
version: { type: "string", description: "Version string to validate" },
|
||||
rejectRanges: {
|
||||
type: "boolean",
|
||||
description:
|
||||
"If true, treat ^, ~, >, <, *, latest as invalid. Default true.",
|
||||
},
|
||||
},
|
||||
required: ["version"],
|
||||
},
|
||||
execute: async ({ version, rejectRanges = true }) => {
|
||||
if (typeof version !== "string" || !version.trim()) {
|
||||
return { success: false, error: "version must be a non-empty string" };
|
||||
}
|
||||
|
||||
const trimmed = version.trim();
|
||||
|
||||
if (rejectRanges) {
|
||||
if (/^[\^~><=*]/.test(trimmed) || trimmed === "latest") {
|
||||
return {
|
||||
success: false,
|
||||
error: `version '${trimmed}' is a range or tag; policy requires an exact pinned version`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Strip a leading range operator if ranges are allowed.
|
||||
const candidate = rejectRanges ? trimmed : trimmed.replace(/^[\^~><=]+/, "");
|
||||
|
||||
const m = candidate.match(SEMVER_RE);
|
||||
if (!m) {
|
||||
return {
|
||||
success: false,
|
||||
error: `'${trimmed}' is not a valid SemVer 2.0.0 version`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
result: {
|
||||
raw: trimmed,
|
||||
major: Number(m[1]),
|
||||
minor: Number(m[2]),
|
||||
patch: Number(m[3]),
|
||||
prerelease: m[4] ?? null,
|
||||
build: m[5] ?? null,
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "check_lockfile_package",
|
||||
description:
|
||||
"Look up a package inside the root package-lock.json and return its locked version. Read-only; does not contact the npm registry. Returns { found: boolean, version?: string, paths: string[] }.",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
name: { type: "string", description: "Package name, e.g. @nestjs/core" },
|
||||
},
|
||||
required: ["name"],
|
||||
},
|
||||
execute: async ({ name }) => {
|
||||
if (typeof name !== "string" || !name.trim()) {
|
||||
return { success: false, error: "name must be a non-empty string" };
|
||||
}
|
||||
|
||||
const lock = loadLockfile();
|
||||
if (!lock) {
|
||||
return {
|
||||
success: false,
|
||||
error: "package-lock.json not found or unreadable at project root",
|
||||
};
|
||||
}
|
||||
|
||||
const matches = [];
|
||||
const packages = lock.packages ?? {};
|
||||
for (const [key, value] of Object.entries(packages)) {
|
||||
// npm v7+ lockfile: keys look like "node_modules/<pkg>" or
|
||||
// "node_modules/<scope>/<pkg>". Strip the prefix and compare.
|
||||
if (!key) continue;
|
||||
const stripped = key.replace(/^.*node_modules\//, "");
|
||||
if (stripped === name) {
|
||||
matches.push({ path: key, version: value.version });
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
result: {
|
||||
name,
|
||||
found: matches.length > 0,
|
||||
occurrences: matches,
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
runMCPServer({ name: "kis-toir-npm-validator", version: "0.1.0", tools });
|
||||
Reference in New Issue
Block a user