context-engine-mcp
Version:
Context engine MCP server for comprehensive project analysis and multi-file editing
586 lines • 25.9 kB
JavaScript
import path from 'path';
import { configManager } from '../config/index.js';
import logger, { PerformanceLogger, logMemoryUsage } from '../utils/logger.js';
import { validateInput, AnalyzeProjectSchema, SearchProjectSchema, EditMultipleFilesSchema, GetFileRelationshipsSchema } from '../utils/validation.js';
import { ProcessingError, createErrorFromUnknown } from '../utils/errors.js';
import { fileManager } from './file-manager.js';
import { LanguageAnalyzer } from './language-analyzer.js';
import { projectCache, analysisCache } from './cache-manager.js';
export class ContextEngine {
static instance;
watchedProjects = new Set();
constructor() {
logger.info('Context Engine initialized');
}
static getInstance() {
if (!ContextEngine.instance) {
ContextEngine.instance = new ContextEngine();
}
return ContextEngine.instance;
}
/**
* Analyze entire project structure and context
*/
async analyzeProject(projectPath, forceRefresh = false) {
// Validate input
const validatedInput = validateInput(AnalyzeProjectSchema, { projectPath, forceRefresh });
const { projectPath: validatedPath } = validatedInput;
const perf = new PerformanceLogger(`analyzeProject: ${validatedPath}`);
const projectKey = path.resolve(validatedPath);
try {
// Check cache unless force refresh
if (!forceRefresh) {
const cached = projectCache.get(projectKey);
if (cached) {
perf.end({ cacheHit: true });
logger.info('Project analysis retrieved from cache', { projectPath: validatedPath });
return cached;
}
}
logger.info('Starting project analysis', { projectPath: validatedPath, forceRefresh });
// Find all relevant files
const files = await fileManager.findFiles(validatedPath, configManager.get('filePatterns'));
perf.checkpoint('files_found');
// Extract project metadata
const metadata = await this.extractProjectMetadata(validatedPath);
perf.checkpoint('metadata_extracted');
// Analyze files concurrently with batching
const fileInfoMap = new Map();
const batchSize = 10; // Process files in batches to avoid overwhelming the system
for (let i = 0; i < files.length; i += batchSize) {
const batch = files.slice(i, i + batchSize);
const batchPromises = batch.map(async (filePath) => {
try {
const fileInfo = await this.analyzeFile(filePath, validatedPath);
if (fileInfo) {
const relativePath = path.relative(validatedPath, filePath);
fileInfoMap.set(relativePath, fileInfo);
}
}
catch (error) {
logger.warn('Failed to analyze file', {
filePath,
error: error instanceof Error ? error.message : String(error)
});
}
});
await Promise.all(batchPromises);
// Log progress for large projects
if (files.length > 50) {
logger.debug(`Analyzed ${Math.min(i + batchSize, files.length)}/${files.length} files`);
}
}
perf.checkpoint('files_analyzed');
logMemoryUsage('project analysis');
// Build project structure
const context = {
projectPath: projectKey,
timestamp: new Date().toISOString(),
files: fileInfoMap,
structure: this.buildProjectStructure(fileInfoMap),
metadata
};
// Cache the result
projectCache.set(projectKey, context, 3600000); // 1 hour TTL
this.watchedProjects.add(projectKey);
const duration = perf.end({
totalFiles: files.length,
analyzedFiles: fileInfoMap.size,
languages: context.structure.languages.length,
frameworks: metadata.frameworks.length
});
logger.info('Project analysis completed', {
projectPath: validatedPath,
duration: `${duration.toFixed(2)}ms`,
totalFiles: files.length,
analyzedFiles: fileInfoMap.size,
languages: context.structure.languages,
frameworks: metadata.frameworks
});
return context;
}
catch (error) {
perf.end({ error: true });
const contextError = createErrorFromUnknown(error);
logger.error('Project analysis failed', {
projectPath: validatedPath,
error: contextError.message,
stack: contextError.stack
});
throw new ProcessingError('project analysis', `Failed to analyze project: ${contextError.message}`, { projectPath: validatedPath });
}
}
/**
* Get comprehensive project context
*/
async getProjectContext(projectPath) {
const projectKey = path.resolve(projectPath);
const cached = projectCache.get(projectKey);
if (cached) {
logger.debug('Retrieved project context from cache', { projectPath });
return cached;
}
logger.warn('Project context not found, run analyze_project first', { projectPath });
return null;
}
/**
* Search across project files with intelligent filtering
*/
async searchInProject(projectPath, query, options = {}) {
// Validate input
const validatedInput = validateInput(SearchProjectSchema, {
projectPath,
query,
caseSensitive: options.caseSensitive,
includeStructure: options.includeStructure
});
const perf = new PerformanceLogger(`searchInProject: ${query}`);
try {
const context = await this.getProjectContext(projectPath);
if (!context) {
throw new ProcessingError('search', 'Project not analyzed. Please run analyze_project first.', { projectPath });
}
const { caseSensitive = false, includeStructure = true, filePatterns = [], maxResults = 100 } = options;
const results = [];
const searchRegex = new RegExp(query, caseSensitive ? 'g' : 'gi');
// Filter files by patterns if specified
const filesToSearch = filePatterns.length > 0
? Array.from(context.files.entries()).filter(([relativePath]) => filePatterns.some(pattern => new RegExp(pattern).test(relativePath)))
: Array.from(context.files.entries());
for (const [relativePath, fileInfo] of filesToSearch) {
if (results.length >= maxResults)
break;
const matches = [];
// Search in file content
if (fileInfo.content) {
const lines = fileInfo.content.split('\n');
for (let lineNum = 0; lineNum < lines.length; lineNum++) {
const line = lines[lineNum];
if (searchRegex.test(line)) {
const contextStart = Math.max(0, lineNum - 2);
const contextEnd = Math.min(lines.length, lineNum + 3);
matches.push({
line: lineNum + 1,
content: line.trim(),
context: lines.slice(contextStart, contextEnd)
});
// Reset regex for next search
searchRegex.lastIndex = 0;
}
}
}
// Search in code structure if requested
if (includeStructure && fileInfo.structure) {
const structureTypes = ['functions', 'classes', 'exports'];
for (const type of structureTypes) {
const items = fileInfo.structure[type] || [];
for (const item of items) {
if (searchRegex.test(item)) {
matches.push({
type: type.slice(0, -1), // Remove 's' from end
name: item
});
searchRegex.lastIndex = 0;
}
}
}
}
if (matches.length > 0) {
results.push({
file: relativePath,
language: fileInfo.language,
matches: matches.slice(0, 20) // Limit matches per file
});
}
}
perf.end({
resultsCount: results.length,
totalMatches: results.reduce((sum, r) => sum + r.matches.length, 0)
});
logger.info('Search completed', {
query,
projectPath,
resultsCount: results.length,
searchedFiles: filesToSearch.length
});
return results;
}
catch (error) {
perf.end({ error: true });
throw error;
}
}
/**
* Edit multiple files with coordinated changes and rollback capability
*/
async editMultipleFiles(projectPath, changes) {
// Validate input
const validatedInput = validateInput(EditMultipleFilesSchema, { projectPath, changes });
const { changes: validatedChanges } = validatedInput;
const perf = new PerformanceLogger(`editMultipleFiles: ${validatedChanges.length} files`);
try {
const results = [];
const processedFiles = [];
// Create backup timestamp for this operation
const operationId = Date.now().toString();
logger.info('Starting multi-file edit operation', {
operationId,
projectPath,
fileCount: validatedChanges.length
});
for (const change of validatedChanges) {
try {
const fullPath = path.resolve(projectPath, change.filePath);
// Validate path is within project boundaries
if (!fileManager.validateProjectBoundaries(fullPath, projectPath)) {
throw new ProcessingError('path validation', `File path is outside project boundaries: ${change.filePath}`, { filePath: change.filePath, projectPath });
}
switch (change.action) {
case 'create':
await fileManager.writeFile(fullPath, change.content || '', {
createBackup: change.backup !== false,
ensureDirectory: true
});
results.push({ file: change.filePath, status: 'created' });
break;
case 'update':
await fileManager.writeFile(fullPath, change.content || '', {
createBackup: change.backup !== false,
ensureDirectory: false
});
results.push({ file: change.filePath, status: 'updated' });
break;
case 'delete':
await fileManager.deleteFile(fullPath, change.backup !== false);
results.push({ file: change.filePath, status: 'deleted' });
break;
default:
throw new ProcessingError('invalid action', `Unknown action: ${change.action}`, { action: change.action });
}
processedFiles.push(change.filePath);
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
results.push({
file: change.filePath,
status: 'error',
error: errorMessage
});
logger.error('File operation failed', {
operationId,
filePath: change.filePath,
action: change.action,
error: errorMessage
});
}
}
// Invalidate project cache to force re-analysis
const projectKey = path.resolve(projectPath);
projectCache.delete(projectKey);
const duration = perf.end({
totalFiles: validatedChanges.length,
successfulFiles: results.filter(r => r.status !== 'error').length,
failedFiles: results.filter(r => r.status === 'error').length
});
logger.info('Multi-file edit operation completed', {
operationId,
duration: `${duration.toFixed(2)}ms`,
results: results.map(r => `${r.file}: ${r.status}`)
});
return results;
}
catch (error) {
perf.end({ error: true });
throw error;
}
}
/**
* Get file dependency relationships
*/
async getFileRelationships(projectPath, targetFilePath) {
// Validate input
const validatedInput = validateInput(GetFileRelationshipsSchema, {
projectPath,
filePath: targetFilePath
});
const perf = new PerformanceLogger('getFileRelationships');
try {
const context = await this.getProjectContext(projectPath);
if (!context) {
throw new ProcessingError('relationships', 'Project not analyzed. Please run analyze_project first.', { projectPath });
}
const relationships = {};
// If specific file requested, analyze only that file
if (targetFilePath) {
const fileInfo = context.files.get(targetFilePath);
if (fileInfo) {
relationships[targetFilePath] = {
dependencies: fileInfo.dependencies,
dependents: this.findDependents(targetFilePath, context)
};
}
}
else {
// Analyze all files
for (const [filePath, fileInfo] of context.files) {
relationships[filePath] = {
dependencies: fileInfo.dependencies,
dependents: this.findDependents(filePath, context)
};
}
}
perf.end({ filesAnalyzed: Object.keys(relationships).length });
return relationships;
}
catch (error) {
perf.end({ error: true });
throw error;
}
}
/**
* Get project statistics and health metrics
*/
async getProjectStats(projectPath) {
const perf = new PerformanceLogger('getProjectStats');
try {
const context = await this.getProjectContext(projectPath);
if (!context) {
throw new ProcessingError('stats', 'Project not analyzed. Please run analyze_project first.', { projectPath });
}
const files = Array.from(context.files.values());
const totalLines = files.reduce((sum, file) => sum + file.lines, 0);
const totalSize = files.reduce((sum, file) => sum + file.size, 0);
// Find largest files
const largestFiles = files
.sort((a, b) => b.size - a.size)
.slice(0, 10)
.map(file => ({
file: file.path,
size: file.size,
lines: file.lines
}));
// Find orphaned files (files with no dependencies and no dependents)
const relationships = await this.getFileRelationships(projectPath);
const orphanedFiles = Object.entries(relationships)
.filter(([, rel]) => rel.dependencies.length === 0 && rel.dependents.length === 0)
.map(([filePath]) => filePath);
// Get cache statistics
const cacheStats = projectCache.getStats();
const stats = {
overview: {
totalFiles: files.length,
totalLines,
languages: context.structure.languages,
frameworks: context.metadata.frameworks
},
dependencies: {
totalDependencies: Object.values(relationships).reduce((sum, rel) => sum + rel.dependencies.length, 0),
circularDependencies: [], // TODO: Implement circular dependency detection
orphanedFiles
},
codeHealth: {
averageFileSize: Math.round(totalSize / files.length),
largestFiles,
duplicatedCode: [] // TODO: Implement code duplication detection
},
performance: {
analysisTime: 'N/A', // Would need to track from last analysis
cacheHitRate: cacheStats.size > 0 ? 0.8 : 0, // Rough estimate
memoryUsage: cacheStats.memoryUsage
}
};
perf.end(stats.overview);
return stats;
}
catch (error) {
perf.end({ error: true });
throw error;
}
}
/**
* Clear all caches and reset state
*/
clearAllCaches() {
projectCache.clear();
analysisCache.clear();
fileManager.clearCache();
this.watchedProjects.clear();
logger.info('All caches cleared');
}
/**
* Private helper methods
*/
async analyzeFile(filePath, projectRoot) {
try {
const fileInfo = await fileManager.getFileInfo(filePath);
if (!fileInfo)
return null;
// Extract dependencies using language analyzer
const dependencies = await LanguageAnalyzer.extractDependencies(fileInfo.content, fileInfo.language);
// Extract code structure using language analyzer
const structure = await LanguageAnalyzer.extractCodeStructure(fileInfo.content, fileInfo.language);
return {
...fileInfo,
dependencies,
structure
};
}
catch (error) {
logger.error('Error analyzing file', {
filePath,
error: error instanceof Error ? error.message : String(error)
});
return null;
}
}
async extractProjectMetadata(projectPath) {
const metadata = {
name: path.basename(projectPath),
type: 'unknown',
version: '0.0.0',
description: '',
dependencies: {},
scripts: {},
frameworks: []
};
try {
// Check for package.json (Node.js)
const packageJsonPath = path.join(projectPath, 'package.json');
try {
const packageJsonContent = await fileManager.readFile(packageJsonPath, false);
const packageJson = JSON.parse(packageJsonContent);
metadata.name = packageJson.name || metadata.name;
metadata.version = packageJson.version || metadata.version;
metadata.description = packageJson.description || '';
metadata.dependencies = {
...(packageJson.dependencies || {}),
...(packageJson.devDependencies || {})
};
metadata.scripts = packageJson.scripts || {};
metadata.type = 'nodejs';
// Detect frameworks
const deps = Object.keys(metadata.dependencies);
if (deps.includes('react'))
metadata.frameworks.push('React');
if (deps.includes('vue'))
metadata.frameworks.push('Vue');
if (deps.includes('@angular/core'))
metadata.frameworks.push('Angular');
if (deps.includes('express'))
metadata.frameworks.push('Express');
if (deps.includes('next'))
metadata.frameworks.push('Next.js');
if (deps.includes('nuxt'))
metadata.frameworks.push('Nuxt.js');
if (deps.includes('svelte'))
metadata.frameworks.push('Svelte');
}
catch (e) {
// package.json not found or invalid
}
// Check for requirements.txt (Python)
const reqPath = path.join(projectPath, 'requirements.txt');
try {
const requirements = await fileManager.readFile(reqPath, false);
metadata.type = metadata.type === 'unknown' ? 'python' : metadata.type;
const deps = requirements.split('\n')
.filter(line => line.trim())
.reduce((acc, line) => {
const [pkg] = line.split(/[>=<]/);
const trimmedPkg = pkg ? pkg.trim() : '';
if (trimmedPkg.length > 0) {
acc[trimmedPkg] = line;
}
return acc;
}, {});
metadata.dependencies = { ...metadata.dependencies, ...deps };
// Detect Python frameworks
const depNames = Object.keys(deps);
if (depNames.includes('django'))
metadata.frameworks.push('Django');
if (depNames.includes('flask'))
metadata.frameworks.push('Flask');
if (depNames.includes('fastapi'))
metadata.frameworks.push('FastAPI');
}
catch (e) {
// requirements.txt not found
}
// Check for README files
const readmePatterns = ['README.md', 'README.txt', 'README.rst', 'readme.md'];
for (const readme of readmePatterns) {
try {
const readmePath = path.join(projectPath, readme);
const readmeContent = await fileManager.readFile(readmePath, false);
metadata.readme = readmeContent.substring(0, 2000); // First 2000 chars
break;
}
catch (e) {
// README not found
}
}
}
catch (error) {
logger.error('Error extracting project metadata', {
projectPath,
error: error instanceof Error ? error.message : String(error)
});
}
return metadata;
}
buildProjectStructure(files) {
const languages = new Set();
const frameworks = new Set();
const functions = {};
const classes = {};
for (const [relativePath, fileInfo] of files) {
languages.add(fileInfo.language);
// Track functions and classes
fileInfo.structure.functions.forEach(func => {
if (!functions[func])
functions[func] = [];
functions[func].push(relativePath);
});
fileInfo.structure.classes.forEach(cls => {
if (!classes[cls])
classes[cls] = [];
classes[cls].push(relativePath);
});
}
return {
totalFiles: files.size,
languages: Array.from(languages),
frameworks: Array.from(frameworks),
dependencies: new Map(),
exports: new Map(),
functions,
classes
};
}
findDependents(targetFile, context) {
const dependents = [];
for (const [filePath, fileInfo] of context.files) {
if (filePath === targetFile)
continue;
// Check if this file depends on the target file
const isDependent = fileInfo.dependencies.some(dep => {
// Handle relative imports
if (dep.startsWith('./') || dep.startsWith('../')) {
const resolvedDep = path.resolve(path.dirname(filePath), dep);
const resolvedTarget = path.resolve(targetFile);
return resolvedDep === resolvedTarget ||
resolvedDep === resolvedTarget.replace(/\.[^.]+$/, '');
}
// Handle module imports that might match file names
return dep.includes(path.basename(targetFile, path.extname(targetFile)));
});
if (isDependent) {
dependents.push(filePath);
}
}
return dependents;
}
}
// Export singleton instance
export const contextEngine = ContextEngine.getInstance();
//# sourceMappingURL=context-engine.js.map