Files
toir-automatization/tools/mcp/nest-validator.mjs
2026-04-07 19:40:41 +03:00

159 lines
5.2 KiB
JavaScript

#!/usr/bin/env node
/**
* MCP server: NestJS module/controller validator (read-only).
*
* Tools:
* - validate_module_source : assert a TypeScript source string is a
* well-formed Nest module
* - check_app_registration : verify that server/src/app.module.ts
* imports and registers a given module class
*
* Purely lexical — we do not use the TypeScript compiler API (that would
* add a dependency). Regex-level checks are sufficient for the structural
* invariants the orchestrator cares about.
*/
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();
// Regex constants for NestJS module validation
const DECORATOR_KEYS_RE = /(?:^|[,{\s])(imports|controllers|providers|exports)\s*:/g;
const IMPORTS_BLOCK_RE = /imports\s*:\s*\[([\s\S]*?)\]/;
const tools = [
{
name: "validate_module_source",
description:
"Validate that a TypeScript source string declares a well-formed NestJS module: imports @Module from @nestjs/common, applies the @Module decorator with an object argument, and exports a class. Returns the class name and the decorator keys it declares.",
inputSchema: {
type: "object",
properties: {
source: { type: "string", description: "Contents of a *.module.ts file" },
},
required: ["source"],
},
execute: async ({ source }) => {
if (typeof source !== "string" || source.length === 0) {
return { success: false, error: "source must be a non-empty string" };
}
const issues = [];
if (!/from ['"]@nestjs\/common['"]/.test(source)) {
issues.push("does not import from @nestjs/common");
}
if (!/\bModule\b/.test(source)) {
issues.push("does not reference the Module symbol");
}
const decoratorMatch = source.match(/@Module\s*\(\s*\{([\s\S]*?)\}\s*\)/);
if (!decoratorMatch) {
issues.push("no @Module({...}) decorator found");
}
const classMatch = source.match(/export\s+class\s+(\w+)/);
if (!classMatch) {
issues.push("no `export class <Name>` declaration");
}
// Extract the top-level keys inside the decorator object so callers
// can sanity-check providers/controllers/imports presence.
const declaredKeys = [];
if (decoratorMatch) {
const body = decoratorMatch[1];
let m;
while ((m = DECORATOR_KEYS_RE.exec(body)) !== null) {
declaredKeys.push(m[1]);
}
}
return {
success: issues.length === 0,
result: {
className: classMatch?.[1] ?? null,
declaredKeys,
issues,
},
};
},
},
{
name: "check_app_registration",
description:
"Check whether server/src/app.module.ts imports and registers a given Nest module class. Useful after generator_nest_resources produces a module, to verify the orchestrator has wired it into the parent-owned app.module.ts.",
inputSchema: {
type: "object",
properties: {
moduleClass: {
type: "string",
description: "e.g. `ChangeEquipmentStatusModule`",
},
appModulePath: {
type: "string",
description:
"Optional override for the app.module.ts path (relative to project root). Defaults to server/src/app.module.ts.",
},
},
required: ["moduleClass"],
},
execute: async ({ moduleClass, appModulePath }) => {
if (typeof moduleClass !== "string" || !/^[A-Z]\w*Module$/.test(moduleClass)) {
return {
success: false,
error: "moduleClass must be a PascalCase class name ending in Module",
};
}
const relPath = appModulePath ?? "server/src/app.module.ts";
const absPath = path.isAbsolute(relPath)
? relPath
: path.join(projectRoot, relPath);
let source: string;
try {
source = fs.readFileSync(absPath, "utf8");
} catch (err) {
return {
success: false,
error: `failed to read ${relPath}: ${err instanceof Error ? err.message : String(err)}`,
};
}
const importRe = new RegExp(
`import\\s*\\{[^}]*\\b${moduleClass}\\b[^}]*\\}\\s*from\\s*['"]([^'"]+)['"]`
);
const importMatch = source.match(importRe);
const decoratorMatch = source.match(/@Module\s*\(\s*\{([\s\S]*?)\}\s*\)/);
let registered = false;
if (decoratorMatch) {
const importsBlock = decoratorMatch[1].match(IMPORTS_BLOCK_RE);
if (importsBlock) {
const importedModules = importsBlock[1]
.split(",")
.map((s) => s.trim())
.filter(Boolean);
registered = importedModules.includes(moduleClass);
}
}
return {
success: Boolean(importMatch) && registered,
result: {
moduleClass,
appModulePath: relPath,
imported: Boolean(importMatch),
importedFrom: importMatch?.[1] ?? null,
registered,
},
};
},
},
];
runMCPServer({ name: "kis-toir-nest-validator", version: "0.1.0", tools });