mcp-repl
Version:
MCP REPL with code execution, semantic code search, and comprehensive ast-grep integration
486 lines (411 loc) • 14.4 kB
JavaScript
import { spawn } from 'child_process';
import * as path from 'node:path';
/**
* Advanced search capabilities for ast-grep
* Supports multiple patterns, constraints, and meta-variable validation
*/
const executeAstGrepCommand = async (args, workingDirectory, timeout = 30000) => {
const startTime = Date.now();
return new Promise((resolve) => {
const childProcess = spawn('ast-grep', args, {
cwd: workingDirectory,
timeout,
env: process.env,
stdio: ['pipe', 'pipe', 'pipe']
});
let stdout = '';
let stderr = '';
childProcess.stdout.on('data', (data) => {
stdout += data;
});
childProcess.stderr.on('data', (data) => {
stderr += data;
});
childProcess.on('close', (code) => {
const executionTimeMs = Date.now() - startTime;
let parsedOutput = null;
let jsonError = null;
try {
if (stdout.trim()) {
parsedOutput = JSON.parse(stdout);
}
} catch (e) {
jsonError = e.message;
}
resolve({
success: code === 0,
stdout: stdout.trim(),
stderr: stderr.trim(),
exitCode: code,
parsedOutput,
jsonError,
executionTimeMs,
command: `ast-grep ${args.join(' ')}`
});
});
childProcess.on('error', (err) => {
resolve({
success: false,
error: err.message,
executionTimeMs: Date.now() - startTime,
command: `ast-grep ${args.join(' ')}`
});
});
});
};
export const multiPatternSearch = async (patterns, options = {}) => {
const {
language,
paths = ['.'],
operator = 'any', // 'any' (OR), 'all' (AND), 'not' (NOT)
context,
strictness,
includeMetadata = true,
workingDirectory
} = options;
if (!Array.isArray(patterns) || patterns.length === 0) {
throw new Error('Patterns must be a non-empty array');
}
const allResults = [];
const searchMetadata = {
patterns: patterns.slice(),
operator,
startTime: new Date().toISOString()
};
// Execute searches for each pattern
for (const pattern of patterns) {
const args = ['run', '--json=compact'];
if (language) args.push('--lang', language);
args.push('--pattern', pattern);
if (context && context > 0) args.push('--context', context.toString());
if (strictness) args.push('--strictness', strictness);
args.push(...paths);
const result = await executeAstGrepCommand(args, workingDirectory);
if (result.success && result.parsedOutput) {
const transformedResults = Array.isArray(result.parsedOutput)
? result.parsedOutput.map(match => ({
...match,
pattern,
file: match.file,
startLine: match.range?.start?.line || 0,
endLine: match.range?.end?.line || 0,
text: match.text || match.lines,
metaVariables: match.metaVariables || {},
language: match.language,
range: match.range
}))
: [];
allResults.push({
pattern,
matches: transformedResults,
matchCount: transformedResults.length,
executionTimeMs: result.executionTimeMs
});
} else {
allResults.push({
pattern,
matches: [],
matchCount: 0,
error: result.error || result.stderr,
executionTimeMs: result.executionTimeMs
});
}
}
// Apply operator logic to combine results
const combinedResults = combinePatternResults(allResults, operator);
return {
success: true,
operator,
patternResults: allResults,
combinedMatches: combinedResults,
totalPatterns: patterns.length,
totalCombinedMatches: combinedResults.length,
...(includeMetadata && { searchMetadata })
};
};
export const constraintBasedSearch = async (pattern, constraints = {}, options = {}) => {
const {
language,
paths = ['.'],
context,
strictness,
workingDirectory
} = options;
const {
minMatches,
maxMatches,
filePathPattern,
metaVariableConstraints = {},
contextConstraints = {},
performanceThreshold
} = constraints;
const args = ['run', '--json=compact'];
if (language) args.push('--lang', language);
args.push('--pattern', pattern);
if (context && context > 0) args.push('--context', context.toString());
if (strictness) args.push('--strictness', strictness);
args.push(...paths);
const startTime = Date.now();
const result = await executeAstGrepCommand(args, workingDirectory);
const executionTimeMs = Date.now() - startTime;
if (!result.success) {
return {
success: false,
error: result.error || result.stderr,
executionTimeMs: result.executionTimeMs
};
}
let matches = Array.isArray(result.parsedOutput) ? result.parsedOutput : [];
// Apply file path constraints
if (filePathPattern) {
const pathRegex = new RegExp(filePathPattern);
matches = matches.filter(match => pathRegex.test(match.file));
}
// Apply meta-variable constraints
if (Object.keys(metaVariableConstraints).length > 0) {
matches = matches.filter(match =>
validateMetaVariables(match.metaVariables, metaVariableConstraints)
);
}
// Apply context constraints
if (Object.keys(contextConstraints).length > 0) {
matches = matches.filter(match =>
validateContextConstraints(match, contextConstraints)
);
}
// Apply count constraints
if (minMatches !== undefined && matches.length < minMatches) {
return {
success: false,
error: `Insufficient matches: found ${matches.length}, required minimum ${minMatches}`,
actualMatches: matches.length,
executionTimeMs
};
}
if (maxMatches !== undefined && matches.length > maxMatches) {
matches = matches.slice(0, maxMatches);
}
// Check performance threshold
if (performanceThreshold && executionTimeMs > performanceThreshold) {
return {
success: false,
error: `Performance threshold exceeded: ${executionTimeMs}ms > ${performanceThreshold}ms`,
executionTimeMs
};
}
const transformedMatches = matches.map(match => ({
file: match.file,
startLine: match.range?.start?.line || 0,
endLine: match.range?.end?.line || 0,
text: match.text || match.lines,
metaVariables: match.metaVariables || {},
language: match.language,
range: match.range,
constraintsApplied: {
filePathPattern: !!filePathPattern,
metaVariableConstraints: Object.keys(metaVariableConstraints).length > 0,
contextConstraints: Object.keys(contextConstraints).length > 0
}
}));
return {
success: true,
matches: transformedMatches,
totalMatches: transformedMatches.length,
constraintsApplied: constraints,
executionTimeMs
};
};
export const metaVariableValidation = async (pattern, validationRules = {}, options = {}) => {
const {
language,
paths = ['.'],
workingDirectory
} = options;
const args = ['run', '--json=compact'];
if (language) args.push('--lang', language);
args.push('--pattern', pattern);
args.push(...paths);
const result = await executeAstGrepCommand(args, workingDirectory);
if (!result.success) {
return {
success: false,
error: result.error || result.stderr,
executionTimeMs: result.executionTimeMs
};
}
const matches = Array.isArray(result.parsedOutput) ? result.parsedOutput : [];
const validationResults = [];
for (const match of matches) {
const metaVars = match.metaVariables || {};
const validationResult = {
file: match.file,
startLine: match.range?.start?.line || 0,
endLine: match.range?.end?.line || 0,
metaVariables: metaVars,
validationPassed: true,
validationErrors: [],
validationDetails: {}
};
// Validate each meta-variable according to rules
for (const [varName, rules] of Object.entries(validationRules)) {
const varValue = metaVars.single?.[varName]?.text || metaVars[varName];
const varValidation = validateMetaVariable(varName, varValue, rules);
validationResult.validationDetails[varName] = varValidation;
if (!varValidation.passed) {
validationResult.validationPassed = false;
validationResult.validationErrors.push(...varValidation.errors);
}
}
validationResults.push(validationResult);
}
const passedValidation = validationResults.filter(r => r.validationPassed);
const failedValidation = validationResults.filter(r => !r.validationPassed);
return {
success: true,
pattern,
totalMatches: matches.length,
validationResults,
passedValidation: passedValidation.length,
failedValidation: failedValidation.length,
validationRules,
executionTimeMs: result.executionTimeMs
};
};
// Helper functions
const combinePatternResults = (patternResults, operator) => {
if (operator === 'any') {
// Union of all matches (OR operation)
const allMatches = [];
const seenMatches = new Set();
for (const result of patternResults) {
for (const match of result.matches) {
const key = `${match.file}:${match.startLine}-${match.endLine}`;
if (!seenMatches.has(key)) {
seenMatches.add(key);
allMatches.push(match);
}
}
}
return allMatches;
}
if (operator === 'all') {
// Intersection of matches (AND operation)
if (patternResults.length === 0) return [];
let intersection = patternResults[0].matches.slice();
for (let i = 1; i < patternResults.length; i++) {
const currentMatches = new Set(
patternResults[i].matches.map(m => `${m.file}:${m.startLine}-${m.endLine}`)
);
intersection = intersection.filter(match => {
const key = `${match.file}:${match.startLine}-${match.endLine}`;
return currentMatches.has(key);
});
}
return intersection;
}
if (operator === 'not') {
// First pattern results minus subsequent patterns (NOT operation)
if (patternResults.length < 2) return patternResults[0]?.matches || [];
const baseMatches = patternResults[0].matches.slice();
const excludeKeys = new Set();
for (let i = 1; i < patternResults.length; i++) {
for (const match of patternResults[i].matches) {
const key = `${match.file}:${match.startLine}-${match.endLine}`;
excludeKeys.add(key);
}
}
return baseMatches.filter(match => {
const key = `${match.file}:${match.startLine}-${match.endLine}`;
return !excludeKeys.has(key);
});
}
return [];
};
const validateMetaVariables = (metaVariables, constraints) => {
const metaVars = metaVariables?.single || metaVariables || {};
for (const [varName, constraint] of Object.entries(constraints)) {
const varValue = metaVars[varName]?.text || metaVars[varName];
if (!varValue) {
if (constraint.required) return false;
continue;
}
// Apply regex constraint
if (constraint.regex && !new RegExp(constraint.regex).test(varValue)) {
return false;
}
// Apply type constraint (basic heuristics)
if (constraint.type) {
if (!validateVariableType(varValue, constraint.type)) {
return false;
}
}
// Apply length constraints
if (constraint.minLength && varValue.length < constraint.minLength) return false;
if (constraint.maxLength && varValue.length > constraint.maxLength) return false;
}
return true;
};
const validateContextConstraints = (match, constraints) => {
// File-based constraints
if (constraints.fileSize) {
// Would need file system access to check file size
}
// Line-based constraints
if (constraints.minLine && match.range?.start?.line < constraints.minLine) return false;
if (constraints.maxLine && match.range?.start?.line > constraints.maxLine) return false;
// Text-based constraints
if (constraints.containsText && !match.text?.includes(constraints.containsText)) return false;
if (constraints.excludesText && match.text?.includes(constraints.excludesText)) return false;
return true;
};
const validateMetaVariable = (varName, varValue, rules) => {
const result = {
passed: true,
errors: [],
varName,
varValue
};
if (!varValue) {
if (rules.required) {
result.passed = false;
result.errors.push(`Meta-variable ${varName} is required but not found`);
}
return result;
}
// Regex validation
if (rules.regex && !new RegExp(rules.regex).test(varValue)) {
result.passed = false;
result.errors.push(`${varName} does not match pattern: ${rules.regex}`);
}
// Type validation
if (rules.type && !validateVariableType(varValue, rules.type)) {
result.passed = false;
result.errors.push(`${varName} is not of expected type: ${rules.type}`);
}
// Length validation
if (rules.minLength && varValue.length < rules.minLength) {
result.passed = false;
result.errors.push(`${varName} is too short (${varValue.length} < ${rules.minLength})`);
}
if (rules.maxLength && varValue.length > rules.maxLength) {
result.passed = false;
result.errors.push(`${varName} is too long (${varValue.length} > ${rules.maxLength})`);
}
return result;
};
const validateVariableType = (value, expectedType) => {
switch (expectedType.toLowerCase()) {
case 'identifier':
return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(value);
case 'string':
return /^["'].*["']$/.test(value);
case 'number':
return /^\d+(\.\d+)?$/.test(value);
case 'boolean':
return /^(true|false)$/.test(value);
case 'function':
return value.includes('function') || /^\w+\s*\(.*\)\s*(\{|=>)/.test(value);
default:
return true; // Unknown types pass by default
}
};