#!/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 ` 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 });