mcp-ai-agent-guidelines
Version:
A comprehensive Model Context Protocol server providing advanced tools, resources, and prompts for implementing AI agent best practices
441 lines • 15.5 kB
JavaScript
/**
* Project Scanner - Real Project Directory Scanner
*
* Scans project directories to extract structure, dependencies, frameworks, and configuration.
* Used by project-onboarding to generate accurate, project-specific documentation.
*/
import { promises as fs } from "node:fs";
import * as path from "node:path";
import { ErrorCode } from "../shared/error-codes.js";
import { McpToolError } from "../shared/errors.js";
import { logger } from "../shared/logger.js";
/**
* ProjectScanner class - scans real project directories
*/
export class ProjectScanner {
defaultOptions = {
maxDepth: 5,
includeNodeModules: false,
includeDotFiles: false,
};
/**
* Scan a project directory and extract structure
*/
async scan(projectPath, options) {
const opts = { ...this.defaultOptions, ...options };
// Validate path exists
await this.validatePath(projectPath);
// Scan directory structure
const directoryStructure = await this.scanDirectory(projectPath, 0, opts, path.basename(projectPath));
// Find config files
const configFiles = await this.findConfigFiles(projectPath);
// Parse config files
const packageJson = await this.parsePackageJson(projectPath);
const tsconfigJson = await this.parseTsconfig(projectPath);
// Detect project type and frameworks
const type = this.detectProjectType(configFiles);
const frameworks = this.detectFrameworks(packageJson, configFiles);
// Find entry points
const entryPoints = await this.findEntryPoints(packageJson, tsconfigJson, projectPath);
return {
name: packageJson?.name ?? path.basename(projectPath),
type,
rootPath: projectPath,
entryPoints,
dependencies: this.extractDependencies(packageJson, false),
devDependencies: this.extractDependencies(packageJson, true),
scripts: packageJson?.scripts ?? {},
frameworks,
configFiles,
directoryStructure,
};
}
/**
* Validate that the path exists and is a directory
*/
async validatePath(projectPath) {
try {
const stats = await fs.stat(projectPath);
if (!stats.isDirectory()) {
throw new McpToolError(ErrorCode.VALIDATION_FAILED, `Path is not a directory: ${projectPath}`, { path: projectPath });
}
}
catch (error) {
if (error instanceof McpToolError) {
throw error;
}
throw new McpToolError(ErrorCode.FILE_NOT_FOUND, `Path does not exist: ${projectPath}`, { path: projectPath, error: String(error) });
}
}
/**
* Recursively scan directory structure with depth limiting
*/
async scanDirectory(dirPath, depth, options, nodeName) {
const node = {
name: nodeName,
type: "directory",
children: [],
};
// Stop if max depth reached
const maxDepth = options.maxDepth ?? this.defaultOptions.maxDepth ?? 5;
if (depth >= maxDepth) {
return node;
}
try {
const entries = await fs.readdir(dirPath, { withFileTypes: true });
for (const entry of entries) {
// Skip node_modules unless explicitly included
if (!options.includeNodeModules && entry.name === "node_modules") {
continue;
}
// Skip dot files unless explicitly included
if (!options.includeDotFiles && entry.name.startsWith(".")) {
continue;
}
const fullPath = path.join(dirPath, entry.name);
if (entry.isDirectory()) {
const childNode = await this.scanDirectory(fullPath, depth + 1, options, entry.name);
node.children?.push(childNode);
}
else if (entry.isFile()) {
node.children?.push({
name: entry.name,
type: "file",
});
}
}
}
catch (error) {
// Log error but don't fail the scan
// This allows scanning to continue even if some directories are inaccessible
logger.debug("Directory scan error", {
path: dirPath,
depth,
error: error instanceof Error ? error.message : String(error),
});
}
return node;
}
/**
* Find all configuration files in the project root
*/
async findConfigFiles(projectPath) {
const configFiles = [];
const configPatterns = [
{ pattern: "package.json", type: "json" },
{ pattern: "tsconfig.json", type: "json" },
{ pattern: "jsconfig.json", type: "json" },
{ pattern: "babel.config.js", type: "other" },
{ pattern: ".babelrc", type: "json" },
{ pattern: "webpack.config.js", type: "other" },
{ pattern: "vite.config.ts", type: "other" },
{ pattern: "vitest.config.ts", type: "other" },
{ pattern: "jest.config.js", type: "other" },
{ pattern: ".eslintrc", type: "json" },
{ pattern: ".prettierrc", type: "json" },
{ pattern: "biome.json", type: "json" },
{ pattern: "Cargo.toml", type: "toml" },
{ pattern: "go.mod", type: "other" },
{ pattern: "requirements.txt", type: "other" },
{ pattern: "pyproject.toml", type: "toml" },
{ pattern: "setup.py", type: "other" },
{ pattern: "Gemfile", type: "other" },
{ pattern: "pom.xml", type: "xml" },
{ pattern: "build.gradle", type: "other" },
{ pattern: ".gitignore", type: "other" },
{ pattern: ".env", type: "env" },
{ pattern: "docker-compose.yml", type: "yaml" },
{ pattern: "Dockerfile", type: "other" },
];
for (const { pattern, type } of configPatterns) {
const filePath = path.join(projectPath, pattern);
try {
await fs.access(filePath);
configFiles.push({
name: pattern,
path: filePath,
type,
});
}
catch {
// File doesn't exist, skip
}
}
return configFiles;
}
/**
* Parse package.json if it exists
*/
async parsePackageJson(projectPath) {
try {
const content = await fs.readFile(path.join(projectPath, "package.json"), "utf-8");
return JSON.parse(content);
}
catch {
return null;
}
}
/**
* Parse tsconfig.json if it exists
*/
async parseTsconfig(projectPath) {
try {
const content = await fs.readFile(path.join(projectPath, "tsconfig.json"), "utf-8");
// Remove comments from JSON (TypeScript config allows comments)
const cleanContent = content.replace(/\/\*[\s\S]*?\*\/|\/\/.*/g, "");
return JSON.parse(cleanContent);
}
catch {
return null;
}
}
/**
* Detect project type based on config files
*/
detectProjectType(configFiles) {
const fileNames = configFiles.map((f) => f.name);
// Check for polyglot FIRST (multiple language indicators)
const languageCount = [
fileNames.includes("package.json") || fileNames.includes("tsconfig.json"),
fileNames.includes("requirements.txt") ||
fileNames.includes("pyproject.toml") ||
fileNames.includes("setup.py"),
fileNames.includes("Cargo.toml"),
fileNames.includes("go.mod"),
fileNames.includes("pom.xml") || fileNames.includes("build.gradle"),
fileNames.includes("Gemfile"),
].filter(Boolean).length;
if (languageCount > 1) {
return "polyglot";
}
// Check for TypeScript
if (fileNames.includes("tsconfig.json")) {
return "typescript";
}
// Check for JavaScript
if (fileNames.includes("package.json")) {
return "javascript";
}
// Check for Python
if (fileNames.includes("requirements.txt") ||
fileNames.includes("pyproject.toml") ||
fileNames.includes("setup.py")) {
return "python";
}
// Check for Rust
if (fileNames.includes("Cargo.toml")) {
return "rust";
}
// Check for Go
if (fileNames.includes("go.mod")) {
return "go";
}
// Check for Java
if (fileNames.includes("pom.xml") || fileNames.includes("build.gradle")) {
return "java";
}
// Check for Ruby
if (fileNames.includes("Gemfile")) {
return "ruby";
}
return "unknown";
}
/**
* Detect frameworks from package.json and config files
*/
detectFrameworks(packageJson, configFiles) {
const frameworks = [];
if (!packageJson) {
return frameworks;
}
const dependencies = {
...packageJson.dependencies,
...packageJson.devDependencies,
};
// React
if (dependencies?.react) {
frameworks.push({
name: "React",
version: dependencies.react,
confidence: "high",
});
}
// Next.js
if (dependencies?.next) {
frameworks.push({
name: "Next.js",
version: dependencies.next,
confidence: "high",
});
}
// Vue
if (dependencies?.vue) {
frameworks.push({
name: "Vue",
version: dependencies.vue,
confidence: "high",
});
}
// Nuxt
if (dependencies?.nuxt) {
frameworks.push({
name: "Nuxt",
version: dependencies.nuxt,
confidence: "high",
});
}
// Angular
if (dependencies?.["@angular/core"]) {
frameworks.push({
name: "Angular",
version: dependencies["@angular/core"],
confidence: "high",
});
}
// Svelte
if (dependencies?.svelte) {
frameworks.push({
name: "Svelte",
version: dependencies.svelte,
confidence: "high",
});
}
// Express
if (dependencies?.express) {
frameworks.push({
name: "Express",
version: dependencies.express,
confidence: "high",
});
}
// Fastify
if (dependencies?.fastify) {
frameworks.push({
name: "Fastify",
version: dependencies.fastify,
confidence: "high",
});
}
// NestJS
if (dependencies?.["@nestjs/core"]) {
frameworks.push({
name: "NestJS",
version: dependencies["@nestjs/core"],
confidence: "high",
});
}
// Vite
if (dependencies?.vite ||
configFiles.some((f) => f.name === "vite.config.ts")) {
frameworks.push({
name: "Vite",
version: dependencies?.vite,
confidence: "high",
});
}
// Vitest
if (dependencies?.vitest ||
configFiles.some((f) => f.name === "vitest.config.ts")) {
frameworks.push({
name: "Vitest",
version: dependencies?.vitest,
confidence: "high",
});
}
// Jest
if (dependencies?.jest ||
configFiles.some((f) => f.name === "jest.config.js")) {
frameworks.push({
name: "Jest",
version: dependencies?.jest,
confidence: "high",
});
}
return frameworks;
}
/**
* Extract dependencies from package.json
*/
extractDependencies(packageJson, isDevDependency) {
if (!packageJson) {
return [];
}
const deps = isDevDependency
? packageJson.devDependencies
: packageJson.dependencies;
if (!deps) {
return [];
}
return Object.entries(deps).map(([name, version]) => ({
name,
version,
isDevDependency,
}));
}
/**
* Find entry points based on package.json, tsconfig.json, and common patterns
*/
async findEntryPoints(packageJson, tsconfigJson, projectPath) {
const entryPoints = [];
// Check package.json main field
if (packageJson?.main && typeof packageJson.main === "string") {
entryPoints.push(packageJson.main);
}
// Check package.json module field
if (packageJson?.module && typeof packageJson.module === "string") {
entryPoints.push(packageJson.module);
}
// Check package.json bin field
if (packageJson?.bin) {
if (typeof packageJson.bin === "string") {
entryPoints.push(packageJson.bin);
}
else if (typeof packageJson.bin === "object") {
entryPoints.push(...Object.values(packageJson.bin));
}
}
// Check tsconfig.json for entry points
if (tsconfigJson?.compilerOptions) {
const compilerOptions = tsconfigJson.compilerOptions;
if (compilerOptions.outDir &&
typeof compilerOptions.outDir === "string") {
// Add common entry patterns in output directory
const outDir = compilerOptions.outDir;
entryPoints.push(`${outDir}/index.js`);
}
}
// Add common entry point patterns if nothing found yet
if (entryPoints.length === 0) {
const commonPatterns = [
"src/index.ts",
"src/index.js",
"src/main.ts",
"src/main.js",
"src/app.ts",
"src/app.js",
"index.ts",
"index.js",
"main.ts",
"main.js",
];
// Check which patterns actually exist
for (const pattern of commonPatterns) {
const fullPath = path.join(projectPath, pattern);
try {
await fs.access(fullPath);
entryPoints.push(pattern);
break; // Only add the first match
}
catch {
// File doesn't exist, continue
}
}
}
return entryPoints;
}
}
/**
* Singleton instance for easy import
*/
export const projectScanner = new ProjectScanner();
//# sourceMappingURL=project-scanner.js.map