159 lines
5.2 KiB
JavaScript
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 });
|