rebase generation
This commit is contained in:
158
tools/mcp/nest-validator.mjs
Normal file
158
tools/mcp/nest-validator.mjs
Normal file
@@ -0,0 +1,158 @@
|
||||
#!/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 });
|
||||
Reference in New Issue
Block a user