Skip to main content
Glama

MCP-REPL

by AnEntrypoint
MIT License
3,295
104
  • Linux
  • Apple
ast-tool.js42.4 kB
import { existsSync, readFileSync, writeFileSync, readdirSync, statSync } from 'fs'; import path, { dirname } from 'path'; import { fileURLToPath } from 'url'; import { createMCPResponse } from '../core/mcp-pagination.js'; import { workingDirectoryContext, createToolContext } from '../core/working-directory-context.js'; import { createIgnoreFilter } from '../core/ignore-manager.js'; import { suppressConsoleOutput } from '../core/console-suppression.js'; import { addExecutionStatusToResponse } from '../core/execution-state.js'; import { ToolError, ToolErrorHandler } from '../core/error-handling.js'; import { withConnectionManagement, getGlobalConnectionManager } from '../core/connection-manager.js'; import { withCrossToolAwareness, addToolMetadata } from '../core/cross-tool-context.js'; import { parse, ensureAstGrepAvailable, isAstGrepAvailable } from './ast-grep-wrapper.js'; import { spawn } from 'child_process'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); function generatePatternGuidance(issues) { let guidance = '💡 Pattern Optimization Tips:\n\n'; issues.forEach((issue, index) => { guidance += `${index + 1}. ${issue.message}\n`; if (issue.examples && issue.examples.length > 0) { guidance += '\n Examples:\n'; issue.examples.forEach(example => { guidance += ` ${example}\n`; }); } guidance += '\n'; }); return guidance; } export class ASTPatternValidator { static validateAndFixPattern(pattern) { if (!pattern || typeof pattern !== 'string') { throw new ToolError( 'Pattern must be a non-empty string', 'INVALID_PATTERN', 'ast-tool', false, ['Provide a valid AST pattern string', 'Check pattern syntax documentation'] ); } // Validate pattern length if (pattern.length > 1000) { throw new ToolError( 'Pattern is too long (max 1000 characters)', 'PATTERN_TOO_LONG', 'ast-tool', false, ['Shorten the pattern', 'Break into multiple smaller patterns'] ); } // Provide helpful guidance for effective patterns instead of auto-fixing const patternIssues = []; let suggestedPattern = pattern; // Check for "has" patterns and provide guidance if (pattern.includes(' has ')) { patternIssues.push({ type: 'HAS_PATTERN', message: 'Pattern uses "has" syntax - for better results, search for the specific element directly', examples: [ `Instead of: "$VAR has useEffect"`, `Try: "useEffect($$$)" or "useEffect"`, '', `Instead of: "$COMPONENT has props"`, `Try: "const $PROP = props.$PROP" or "$COMPONENT($$$)"` ] }); // Extract the variable part as a better pattern suggestion const parts = pattern.split(' has '); if (parts[0].startsWith('$')) { suggestedPattern = parts[0]; } } // Check for overly complex patterns const complexPatternCount = (pattern.match(/\$\$\$/g) || []).length; if (complexPatternCount > 2) { patternIssues.push({ type: 'COMPLEX_PATTERN', message: 'Pattern contains multiple complex placeholders - simplify for better results', examples: [ `Instead of: "function $FUNC($$$) { $$$ }"`, `Try: "function $FUNC(" or just "$FUNC"`, '', `Break complex searches into multiple specific patterns` ] }); } // Check for overly broad patterns const overlyBroadPatterns = [ /^\w+$/, // Single words /^[A-Z][a-zA-Z0-9]*$/, // Just class/function names /^\$\w+$/ // Just metavariables ]; if (overlyBroadPatterns.some(regex => regex.test(pattern))) { patternIssues.push({ type: 'BROAD_PATTERN', message: 'Pattern is very broad - add more context for better results', examples: [ `Instead of: "Component"`, `Try: "class Component" or "function Component("`, '', `Instead of: "$VAR"`, `Try: "const $VAR =" or "let $VAR ="` ] }); } // Only validate for truly invalid patterns const invalidPatterns = [ // Empty patterns /^\s*$/, // Patterns with clearly invalid characters /[<>\\|]/g ]; for (const invalidRegex of invalidPatterns) { if (invalidRegex.test(pattern)) { throw new ToolError( `Pattern "${pattern}" contains invalid syntax`, 'INVALID_PATTERN', 'ast-tool', false, [ 'Use valid AST pattern syntax', 'Avoid special characters like < > \\ |', 'See tool description for effective examples' ] ); } } return { isValid: true, originalPattern: pattern, suggestedPattern, patternIssues, hasSuggestions: patternIssues.length > 0, guidance: patternIssues.length > 0 ? generatePatternGuidance(patternIssues) : null }; } // getSafePattern now returns the original pattern with validation info static getSafePattern(pattern) { const validation = ASTPatternValidator.validateAndFixPattern(pattern); return validation.originalPattern; } } class ASTHelper { constructor(language = 'javascript') { this.language = language; this.errorHandler = new ToolErrorHandler('ast-tool'); } detectLanguageFromExtension(filePath) { const ext = path.extname(filePath).toLowerCase(); const extensionMap = { '.js': 'javascript', '.jsx': 'javascript', '.ts': 'typescript', '.tsx': 'typescript', '.mjs': 'javascript', '.py': 'python', '.go': 'go', '.rs': 'rust', '.c': 'c', '.cpp': 'cpp', '.cc': 'cpp', '.cxx': 'cpp' }; return extensionMap[ext] || 'javascript'; } setLanguage(language) { this.language = language; } parseCode(code) { return this.safeASTOperation(() => { try { ensureAstGrepAvailable(); return parse(this.language, code); } catch (error) { return null; } }, { code, language: this.language, operation: 'parseCode' }); } safeASTOperation(operation, context = {}) { try { return operation(); } catch (error) { // Handle synchronous errors gracefully console.error(`AST Operation Error (${context.operation}):`, error.message); return this.createErrorResponse(error, context); } } createErrorResponse(error, context = {}) { const errorInfo = { error: true, message: error.message, operation: context.operation, text: `AST Operation Error: ${context.operation} failed - ${error.message}`, timestamp: new Date().toISOString() }; if (context.pattern) { errorInfo.pattern = context.pattern; errorInfo.text += ` (Pattern: ${context.pattern})`; } if (context.operation === 'searchPattern') { return [errorInfo]; } return errorInfo; } searchPattern(code, pattern) { return this.safeASTOperation(() => { // Try to fix the pattern first const originalPattern = pattern; const fixedPattern = this.fixPattern(pattern); if (fixedPattern === null) { return [{ error: true, message: `AST Pattern Error: Pattern "${originalPattern}" is too complex to automatically fix and cannot be processed.`, pattern: originalPattern, text: `AST Pattern Error: Pattern "${originalPattern}" contains multiple complex $$$ metavariables that cannot be safely converted. Please use simpler patterns with single metavariables ($VAR, $ARG) instead.` }]; } if (this.isInvalidPattern(fixedPattern)) { return [{ error: true, message: `AST Pattern Error: Pattern "${originalPattern}" is invalid and cannot be processed.`, pattern: originalPattern, text: `AST Pattern Error: Pattern "${originalPattern}" is invalid. Please use valid AST patterns.` }]; } const root = this.parseCode(code); if (!root) return []; const rootNode = root.root(); // Use ast-grep natively with additional safety try { const matches = rootNode.findAll(fixedPattern); const results = matches.map(match => ({ text: match.text(), start: match.range().start.index, end: match.range().end.index, line: match.range().start.line, column: match.range().start.column })); // If pattern was auto-fixed, include a warning if (fixedPattern !== originalPattern) { return [{ error: true, message: `Pattern automatically fixed: "${originalPattern}" → "${fixedPattern}"`, pattern: originalPattern, text: `⚠️ Pattern Warning: The original pattern "${originalPattern}" was automatically converted to "${fixedPattern}" to prevent server crashes. Results shown are for the fixed pattern.`, isWarning: true, results: results }]; } return results; } catch (astError) { // Report ast-grep errors to the agent return [{ error: true, message: astError.message, pattern: fixedPattern, text: `AST Pattern Error: ast-grep failed to process pattern "${fixedPattern}": ${astError.message}` }]; } }, { code, pattern, operation: 'searchPattern' }); } isInvalidPattern(pattern) { // Check for truly invalid patterns (empty, wrong type) if (!pattern || typeof pattern !== 'string' || pattern.trim().length === 0) { return true; } // Only block patterns that cannot be automatically fixed const trulyUnsafePatterns = [ '', // Empty pattern undefined, null ]; return trulyUnsafePatterns.includes(pattern); } fixPattern(pattern) { if (!pattern || typeof pattern !== 'string') { return pattern; } let fixedPattern = pattern; // Simple string replacements for common problematic $$$ patterns const patternFixes = [ // Function patterns { match: 'function $FUNC($$$) { $$$ }', replacement: 'function $FUNC($PARAM) { $STMT }' }, { match: 'function $FUNC($$$)', replacement: 'function $FUNC($PARAM)' }, { match: 'async function $FUNC($$$)', replacement: 'async function $FUNC($PARAM)' }, // Arrow function patterns - convert to safe simple patterns { match: 'onClick={() => $$$}', replacement: 'onClick={$_}' }, { match: '($$$) => { $$$ }', replacement: '$A => $B' }, { match: '() => { $$$ }', replacement: '() => $B' }, { match: '$HANDLER = () => { $$$ }', replacement: '$A = () => $B' }, // Object patterns - convert to empty object for safety { match: '{$$$}', replacement: '{}' }, { match: 'const $OBJ = {$$$}', replacement: 'const $OBJ = {}' }, { match: '{$KEY: $$$}', replacement: '{}' }, // Array patterns { match: '[$$$]', replacement: '[]' }, { match: 'const $ARR = [$$$]', replacement: 'const $ARR = []' }, // React hooks patterns { match: 'const [$STATE, $SETTER] = useState($$$)', replacement: 'const [$STATE, $SETTER] = useState($INITIAL)' }, { match: 'useState($$$)', replacement: 'useState($PROP)' }, { match: 'useEffect($$$)', replacement: 'useEffect($DEPENDENCY)' }, // Console and other patterns { match: 'console.log($$$)', replacement: 'console.log($ARG)' }, { match: 'console.error($$$)', replacement: 'console.error($ARG)' }, { match: 'console.warn($$$)', replacement: 'console.warn($ARG)' }, { match: 'console.info($$$)', replacement: 'console.info($ARG)' }, { match: 'const $VAR = $$$', replacement: 'const $VAR = $VALUE' }, { match: 'let $VAR = $$$', replacement: 'let $VAR = $VALUE' }, { match: 'var $VAR = $$$', replacement: 'var $VAR = $VALUE' }, // Multiple $$$ in function calls (fallback) { match: '$FUNC($$$)', replacement: '$FUNC($ARG)' } ]; // Apply all fixes using simple string matching for (const fix of patternFixes) { if (fix.match && fixedPattern.includes(fix.match)) { fixedPattern = fixedPattern.replace(fix.match, fix.replacement); } } // If there are still $$$ patterns that couldn't be automatically fixed, // try a more generic approach if (fixedPattern.includes('$$$')) { // Replace isolated $$$ with single metavariables based on context fixedPattern = fixedPattern .replace(/\b\s*\$\$\$\s*\b/g, '$CONTENT') // Isolated $$$ .replace(/\{\s*\$\$\$\s*\}/g, '{}') // Object with $$$ .replace(/\[\s*\$\$\$\s*\]/g, '[]') // Array with $$$ .replace(/\(\s*\$\$\$\s*\)/g, '($ARG)') // Function params with $$$ .replace(/=\s*\$\$\$\s*;/g, '= $VALUE;') // Assignments with $$$ .replace(/return\s+\$\$\$/g, 'return $VALUE'); // Return statements } // If the pattern is still unsafe after all fixes, return null to indicate it should be blocked if (this.isTrulyUnsafe(fixedPattern)) { return null; } return fixedPattern !== pattern ? fixedPattern : pattern; } isTrulyUnsafe(pattern) { // Patterns that are fundamentally unsafe and cannot be automatically fixed const tripleDollarCount = (pattern.match(/\$\$\$/g) || []).length; if (tripleDollarCount > 3) { return true; // Too many $$$ to safely fix } // Check for complex nested patterns that would be hard to fix automatically const complexPatterns = [ /\$\$\$.*\$\$\$/, // Multiple $$$ in complex arrangements /\{\s*\$\w+\s*:\s*\$\$\$\s*.*\$\$\$\s*\}/, // Objects with multiple $$$ /\[\s*\$\w+\s*,\s*\$\$\$\s*,.*\$\$\$\s*\]/, // Arrays with multiple $$$ ]; for (const complexRegex of complexPatterns) { if (complexRegex.test(pattern)) { return true; } } return false; } replacePattern(code, pattern, replacement) { try { if (this.isInvalidPattern(pattern)) { return code; // Return original code for invalid patterns } const rootNode = this.parseCodeWithPatternSafety(code, pattern); try { const matches = rootNode.findAll(pattern); let modifiedCode = code; let offset = 0; matches.forEach(match => { const range = match.range(); const before = modifiedCode.substring(0, range.start.index + offset); const after = modifiedCode.substring(range.end.index + offset); modifiedCode = before + replacement + after; offset += replacement.length - (range.end.index - range.start.index); }); return modifiedCode; } catch (astError) { // ast-grep failed to parse the pattern - return original code return code; } } catch (error) { // Return original code for any errors return code; } } searchPatternSync(code, pattern) { return this.searchPattern(code, pattern); } } function detectLanguageFromPattern(pattern, targetPath) { // Detect JSON files/patterns if (pattern.includes('.json') || pattern.includes('package.json') || targetPath.includes('.json') || pattern.includes('"') && pattern.includes(':') && pattern.includes('{')) { return 'json'; } // Detect YAML files if (pattern.includes('.yaml') || pattern.includes('.yml')) { return 'yaml'; } // Detect config files if (pattern.includes('toml') || pattern.includes('config')) { return 'toml'; } return null; // fallback to default } async function unifiedASTOperation(operation, options = {}) { // Extract options first to avoid destructuring scope issues const targetPathParam = options.path || '.'; const patternParam = options.pattern; const replacementParam = options.replacement; const language = options.language || 'javascript'; const recursive = options.recursive !== false; const maxResults = options.maxResults || 100; const workingDirectory = options.workingDirectory || process.cwd(); // Auto-detect language based on pattern if not specified const detectedLanguage = detectLanguageFromPattern(patternParam, targetPathParam); const finalLanguage = options.language || detectedLanguage || 'javascript'; return safeASTOperationWrapper(async () => { const helper = new ASTHelper(finalLanguage); // Check for invalid patterns before processing files if (operation === 'search' || operation === 'replace') { if (helper.isInvalidPattern(patternParam)) { return { success: false, results: [], errors: [{ message: `AST Pattern Error: Pattern "${patternParam}" is invalid and cannot be processed.`, pattern: patternParam, isPatternError: true }], patternErrors: [{ message: `AST Pattern Error: Pattern "${patternParam}" is invalid. Please use valid AST patterns.`, pattern: patternParam, isPatternError: true }], generalErrors: [], otherErrors: [], totalMatches: 0, totalErrors: 1, pattern: patternParam, error: `Pattern "${patternParam}" is invalid and cannot be processed` }; } } let targetPath; if (path.isAbsolute(targetPathParam)) { targetPath = targetPathParam; } else { const basePath = workingDirectory || process.cwd(); targetPath = path.resolve(basePath, targetPathParam); } if (!existsSync(targetPath)) { throw new Error(`Path not found: ${targetPath}`); } switch (operation) { case 'search': return await performSearch(helper, targetPath, patternParam, recursive, maxResults); case 'replace': return await performReplace(helper, targetPath, patternParam, replacementParam, recursive, true); default: throw new Error(`Unknown operation: ${operation}`); } }, { operation, pattern: patternParam, workingDirectory }); } async function safeASTOperationWrapper(operation, context = {}) { const connectionManager = getGlobalConnectionManager(); const errorHandler = new ToolErrorHandler('ast-tool'); return connectionManager.executeWithRetry(async () => { try { // Provide pattern guidance instead of auto-fixing const originalPattern = context.pattern; let patternGuidance = null; if (context.pattern) { const validation = ASTPatternValidator.validateAndFixPattern(context.pattern); // Store validation info for guidance if (validation.hasSuggestions) { patternGuidance = validation.guidance; context.pattern = validation.suggestedPattern; context.originalPattern = originalPattern; context.hasGuidance = true; } } const result = await operation(); // Add pattern guidance if suggestions were provided if (patternGuidance && result.results) { result.patternGuidance = { originalPattern, guidance: patternGuidance, message: 'Pattern optimization suggestions available' }; } return result; } catch (error) { console.error(`AST Operation Wrapper Error (${context.operation || 'unknown'}):`, error.message); // Handle different types of errors gracefully let errorResponse; if (connectionManager.isConnectionError(error)) { errorResponse = { success: false, results: [], errors: [{ message: `Connection Error: ${error.message}`, operation: context.operation || 'unknown', isConnectionError: true }], patternErrors: [], generalErrors: [{ message: `Connection failed: ${error.message}`, operation: context.operation || 'unknown' }], otherErrors: [], totalMatches: 0, totalErrors: 1, pattern: context.pattern || undefined, error: `Connection error: ${error.message}`, isConnectionError: true }; } else { errorResponse = { success: false, results: [], errors: [{ message: `AST Operation Error: ${context.operation || 'unknown'} failed - ${error.message}`, operation: context.operation || 'unknown', isWrapperError: true }], patternErrors: [], generalErrors: [{ message: error.message, operation: context.operation || 'unknown' }], otherErrors: [], totalMatches: 0, totalErrors: 1, pattern: context.pattern || undefined, error: `AST operation failed: ${error.message}`, isGracefulError: true }; } return errorResponse; } }, { toolName: 'ast-tool', maxRetries: 2, retryDelay: 1000 }); } async function performSearch(helper, targetPath, pattern, recursive, maxResults) { const results = []; const maxFileSize = 100 * 1024; // Reduced from 150KB for better performance const maxConcurrency = 8; // Process files in parallel const processFile = async (file) => { try { const stat = statSync(file); if (stat.size > maxFileSize) { return [{ file, error: `File too large for search (>${Math.round(maxFileSize/1024)}KB)` }]; } const content = readFileSync(file, 'utf8'); helper.setLanguage(helper.detectLanguageFromExtension(file)); const matches = await helper.searchPattern(content, pattern); return matches.map(match => { if (match.error) { if (match.isWarning && match.results) { // Pattern was auto-fixed, include both warning and results return { file, warning: match.message, pattern: match.pattern, isPatternWarning: true, content: match.text, line: match.line, column: match.column, start: match.start, end: match.end }; } return { file, error: match.message, pattern: match.pattern, isPatternError: true }; } return { file, content: match.text, line: match.line, column: match.column, start: match.start, end: match.end }; }); } catch (error) { return [{ file, error: error.message, isGeneralError: true }]; } }; if (statSync(targetPath).isDirectory()) { const files = await findFiles(targetPath, { recursive }); const limitedFiles = files.slice(0, maxResults); // Process files in batches for better performance for (let i = 0; i < limitedFiles.length; i += maxConcurrency) { const batch = limitedFiles.slice(i, i + maxConcurrency); const batchPromises = batch.map(file => processFile(file)); const batchResults = await Promise.all(batchPromises); results.push(...batchResults.flat()); // Early termination if we have enough results if (results.length >= maxResults * 2) break; } } else { const fileResults = await processFile(targetPath); results.push(...fileResults); } const validResults = results.filter(r => !r.error && !r.warning); const errorResults = results.filter(r => r.error); const warningResults = results.filter(r => r.warning); const patternErrors = errorResults.filter(r => r.isPatternError); const generalErrors = errorResults.filter(r => r.isGeneralError); const patternWarnings = warningResults.filter(r => r.isPatternWarning); const otherErrors = errorResults.filter(r => !r.isPatternError && !r.isGeneralError); return { success: patternErrors.length === 0 && generalErrors.length === 0, results: validResults, errors: errorResults, patternErrors: patternErrors, generalErrors: generalErrors, patternWarnings: patternWarnings, otherErrors: otherErrors, totalMatches: validResults.length, totalErrors: errorResults.length, totalWarnings: warningResults.length, pattern: pattern, path: targetPath, warning: patternErrors.length > 0 ? `Pattern errors found: ${patternErrors.length} files had invalid AST patterns` : patternWarnings.length > 0 ? `Pattern auto-fixed: ${patternWarnings.length} files had patterns automatically converted` : undefined }; } async function performReplace(helper, targetPath, pattern, replacement, recursive, autoLint = true) { const results = []; const processFile = async (file) => { try { const content = readFileSync(file, 'utf8'); helper.setLanguage(helper.detectLanguageFromExtension(file)); const newContent = await helper.replacePattern(content, pattern, replacement); if (newContent !== content) { writeFileSync(file, newContent); return { file, status: 'modified', changes: true }; } else { return { file, status: 'unchanged', changes: false }; } } catch (error) { return { file, error: error.message, status: 'failed' }; } }; if (statSync(targetPath).isDirectory()) { const files = await findFiles(targetPath, { recursive }); for (const file of files) { const result = await processFile(file); results.push(result); } } else { const result = await processFile(targetPath); results.push(result); } return { success: true, results, modifiedFiles: results.filter(r => r.changes).length, totalFiles: results.length, pattern, replacement, path: targetPath }; } async function findFiles(dir, options = {}) { const { recursive = true, extensions = ['.js', '.ts', '.jsx', '.tsx', '.py', '.go', '.rs', '.c', '.cpp', '.json'], ignorePatterns = [], useGitignore = true } = options; const results = []; const customPatterns = [...ignorePatterns]; const ignoreFilter = createIgnoreFilter(dir, customPatterns, { useGitignore, useDefaults: true, caseSensitive: false }); const scan = async (currentDir) => { const entries = readdirSync(currentDir, { withFileTypes: true }); const filePromises = entries.map(async (entry) => { const fullPath = path.join(currentDir, entry.name); if (ignoreFilter.ignores(fullPath)) { return null; } if (entry.isDirectory() && recursive) { return scan(fullPath); } else if (entry.isFile()) { if (extensions.some(ext => fullPath.endsWith(ext))) { results.push(fullPath); } } return null; }); await Promise.all(filePromises); }; await scan(dir); return results; } function generateASTInsights(results, operation, pattern, workingDirectory, result = null) { const insights = []; if (operation === 'search') { insights.push(`AST search found ${results.length} matches for pattern: "${pattern}"`); const uniqueFiles = new Set(results.map(r => r.file)); if (uniqueFiles.size > 1) { insights.push(`Pattern found in ${uniqueFiles.size} different files`); } if (pattern.includes('$') || pattern.includes('has')) { insights.push('Complex pattern search - results show structural code relationships'); } const fileTypes = new Set(results.map(r => r.file.split('.').pop())); if (fileTypes.size > 1) { insights.push(`Pattern spans ${fileTypes.size} file types: ${Array.from(fileTypes).join(', ')}`); } } else if (operation === 'replace') { if (result && result.modifiedFiles > 0) { insights.push(`Pattern replacement completed: ${result.modifiedFiles} files modified`); insights.push(`Replaced "${pattern}" with "${result.replacement}"`); if (result.modifiedFiles > 5) { insights.push('Large-scale change - consider testing and verification'); } } else { insights.push(`No changes made - pattern "${pattern}" not found`); } } if (pattern.includes('console.')) { insights.push('Console operation detected - consider removing for production'); } if (pattern.includes('debugger')) { insights.push('Debugger statement found - should be removed for production'); } if (pattern.includes('var ')) { insights.push('Var declaration found - consider using const/let'); } if (pattern.includes('TODO') || pattern.includes('FIXME')) { insights.push('Task comment found - track for resolution'); } if (results.length === 0) { insights.push('No matches found - pattern may be too specific or not present'); } else if (results.length > 50) { insights.push('Many matches found - consider more specific pattern or review scope'); } if (operation === 'replace' && result && result.modifiedFiles > 0) { insights.push('Verification recommended - run tests to ensure changes work correctly'); } return insights; } export const UNIFIED_AST_TOOL = { name: 'ast_tool', description: "AST pattern matching for precise code structure searches. Patterns: $VAR (variables), $FUNC (functions), $$$ (any code). Examples: 'useState($INIT)', 'function $F($P)', 'console.log($A)', 'try {$$$} catch($E) {$$$}'. Relations: '$F has debugger' (contains), '$V inside function_declaration' (context), 'kind: function_declaration' (node type). Composites: 'all:[p1,p2]', 'any:[p1,p2]', 'not:p'. Use concrete syntax patterns, not abstract concepts. Auto-simplifies complex patterns.", examples: [ 'operation="search", pattern="console.log($ARG)"', 'operation="replace", pattern="var $NAME", replacement="let $NAME"', 'operation="search", pattern="$FUNC debugger"', 'operation="search", pattern="kind: function_declaration"', 'operation="search", pattern="$OBJ"' ], inputSchema: { type: 'object', properties: { operation: { type: 'string', enum: ['search', 'replace'], description: 'search: find patterns, replace: transform code' }, pattern: { type: 'string', description: 'AST pattern: $VAR (variable), $FUNC (function), $$$ (any code). Ex: "useState($INIT)", "function $F($P)", "$F has debugger", "kind: function_declaration"' }, replacement: { type: 'string', description: 'Replacement text for AST patterns' }, language: { type: 'string', enum: ['javascript', 'typescript', 'jsx', 'tsx', 'python', 'go', 'rust', 'c', 'cpp'], default: 'javascript' }, workingDirectory: { type: 'string', description: 'Absolute path to existing directory (no relative paths). Ex: "/home/user/project"' }, cursor: { type: 'string', description: 'Pagination cursor for large result sets' }, pageSize: { type: 'number', default: 50, description: 'Results per page' } }, required: ['operation', 'workingDirectory', 'pattern'] }, handler: withCrossToolAwareness(withConnectionManagement(async (args) => { const consoleRestore = suppressConsoleOutput(); const workingDirectory = args.workingDirectory; const query = args.pattern || args.operation || ''; try { const context = await workingDirectoryContext.getToolContext(workingDirectory, 'ast_tool', query); // Validate input parameters if (!args.operation) { throw new ToolError( 'Operation parameter is required', 'MISSING_PARAMETER', 'ast-tool', false, ['Provide "operation" parameter (search or replace)', 'Check tool documentation'] ); } if (!args.workingDirectory) { throw new ToolError( 'Working directory parameter is required', 'MISSING_PARAMETER', 'ast-tool', false, ['Provide "workingDirectory" parameter', 'Use absolute path like "/Users/username/project"'] ); } if (args.operation === 'search' && (args.cursor || args.pageSize !== 50)) { const result = await unifiedASTOperation(args.operation, args); // Handle validation errors if (result.isValidationError) { const response = { content: [{ type: "text", text: result.error }], isError: true }; return addExecutionStatusToResponse(response, 'ast_tool'); } // Handle connection errors if (result.isConnectionError) { const response = { content: [{ type: "text", text: `Connection Error: ${result.error}` }], isError: true }; return addExecutionStatusToResponse(response, 'ast_tool'); } // Add pattern guidance if available if (result.patternGuidance) { const response = { content: [{ type: "text", text: result.patternGuidance.guidance + '\n\nSearching with suggested pattern...' }] }; return addExecutionStatusToResponse(response, 'ast_tool'); } // Check for pattern errors and return error response instead of pagination if (result.patternErrors && result.patternErrors.length > 0) { let output = `Pattern Error${result.patternErrors.length > 1 ? 's' : ''} encountered:\n\n`; result.patternErrors.forEach(err => { output += `• ${err.message}\n`; }); if (result.totalMatches > 0) { output += `\n${result.totalMatches} matches found for pattern: "${args.pattern}":\n\n`; const results = Array.isArray(result) ? result : (result.results || []); results.slice(0, 15).forEach((match, i) => { output += `${match.file}:${match.line}\n${match.content.trim()}\n\n`; }); } const response = { content: [{ type: "text", text: output.trim() }], isError: true }; return addExecutionStatusToResponse(response, 'ast_tool'); } const results = Array.isArray(result) ? result : (result.results || []); const insights = generateASTInsights(results, args.operation, args.pattern, workingDirectory); const toolContext = createToolContext('ast_tool', workingDirectory, query, { filesAccessed: Array.isArray(results) ? results.map(r => r?.file || r.path || 'unknown').filter(Boolean) : [], patterns: [args.pattern], insights: insights }); await workingDirectoryContext.updateContext(workingDirectory, 'ast_tool', toolContext); const response = createMCPResponse(results, { cursor: args.cursor, pageSize: args.pageSize, metadata: { operation: args.operation, path: workingDirectory, pattern: args.pattern, timestamp: new Date().toISOString() } }); return addExecutionStatusToResponse(response, 'ast_tool'); } let result; try { result = await unifiedASTOperation(args.operation, args); } catch (error) { // Handle catastrophic errors gracefully const response = { content: [{ type: "text", text: `AST Operation Error: ${error.message}\n\nOperation: ${args.operation}\nPattern: ${args.pattern || 'N/A'}\nPath: ${workingDirectory || 'N/A'}\n\nThe operation encountered an unexpected error but the connection remains stable.` }], isError: true }; return addExecutionStatusToResponse(response, 'ast_tool'); } // Handle validation and connection errors if (result.isValidationError) { const response = { content: [{ type: "text", text: result.error }], isError: true }; return addExecutionStatusToResponse(response, 'ast_tool'); } if (result.isConnectionError) { const response = { content: [{ type: "text", text: `Connection Error: ${result.error}` }], isError: true }; return addExecutionStatusToResponse(response, 'ast_tool'); } let finalResult; if (args.operation === 'search') { finalResult = formatSearchResult(result, args); } else if (args.operation === 'replace') { finalResult = formatReplaceResult(result, args); } else { finalResult = result; } const insights = generateASTInsights(result.results || [], args.operation, args.pattern, workingDirectory, result); const toolContext = createToolContext('ast_tool', workingDirectory, query, { filesAccessed: result.filesAccessed || result.modifiedFiles || [], patterns: [args.pattern], insights: insights }); await workingDirectoryContext.updateContext(workingDirectory, 'ast_tool', toolContext); // Check for pattern errors and include them in the response if (result.patternErrors && result.patternErrors.length > 0) { const patternErrorOutput = finalResult.content && finalResult.content[0] && finalResult.content[0].type === 'text' ? finalResult.content[0].text : ''; const errorMessages = result.patternErrors.map(err => `Pattern Error: ${err.message} in file ${err.file}` ).join('\n'); const response = { content: [{ type: "text", text: patternErrorOutput + '\n\n' + errorMessages }], isError: true }; return addExecutionStatusToResponse(response, 'ast_tool'); } return addExecutionStatusToResponse(finalResult, 'ast_tool'); } catch (error) { const errorContext = createToolContext('ast_tool', workingDirectory, query, { error: error.message }); await workingDirectoryContext.updateContext(workingDirectory, 'ast_tool', errorContext); const response = { success: false, error: error.message, operation: args.operation }; return addExecutionStatusToResponse(response, 'ast_tool'); } finally { consoleRestore.restore(); } }, 'ast-tool', { maxRetries: 2, retryDelay: 1000 }), 'ast_tool') }; function formatSearchResult(result, args) { if (!result.success) { let errorMessage = `Search failed: ${result.error}`; // Add pattern errors if they exist if (result.patternErrors && result.patternErrors.length > 0) { errorMessage += '\n\nPattern Errors:\n'; result.patternErrors.forEach(err => { errorMessage += `• ${err.message}\n`; }); } const response = { content: [{ type: "text", text: errorMessage }], isError: true }; return addExecutionStatusToResponse(response, 'ast_tool'); } // Add pattern guidance if available if (result.patternGuidance) { const response = { content: [{ type: "text", text: result.patternGuidance.guidance + '\n\nSearching with suggested pattern...' }] }; return addExecutionStatusToResponse(response, 'ast_tool'); } // Check for pattern errors even if search was successful if (result.patternErrors && result.patternErrors.length > 0) { let output = `Pattern Error${result.patternErrors.length > 1 ? 's' : ''} encountered:\n\n`; result.patternErrors.forEach(err => { output += `• ${err.message}\n`; }); if (result.totalMatches > 0) { output += `\n${result.totalMatches} matches found for pattern: "${args.pattern}":\n\n`; result.results.slice(0, 15).forEach((match, i) => { output += `${match.file}:${match.line}\n${match.content.trim()}\n\n`; }); } const response = { content: [{ type: "text", text: output.trim() }], isError: true }; return addExecutionStatusToResponse(response, 'ast_tool'); } if (result.totalMatches === 0) { const response = { content: [{ type: "text", text: `No matches found for pattern: "${args.pattern}"` }] }; return addExecutionStatusToResponse(response, 'ast_tool'); } let output = `${result.totalMatches} matches for "${args.pattern}":\n\n`; result.results.slice(0, 15).forEach((match, i) => { output += `${match.file}:${match.line}\n${match.content.trim()}\n\n`; }); if (result.totalMatches > 15) { output += `... ${result.totalMatches - 15} more matches\n`; } const response = { content: [{ type: "text", text: output.trim() }] }; return addExecutionStatusToResponse(response, 'ast_tool'); } function formatReplaceResult(result, args) { if (!result.success) { const response = { content: [{ type: "text", text: `Replace failed: ${result.error}` }], isError: true }; return addExecutionStatusToResponse(response, 'ast_tool'); } if (result.modifiedFiles === 0) { const response = { content: [{ type: "text", text: `No changes made - pattern "${args.pattern}" found no matches` }] }; return addExecutionStatusToResponse(response, 'ast_tool'); } let response = `Replaced pattern in ${result.modifiedFiles} files\n`; response += `Pattern: "${args.pattern}"\n`; response += `Replacement: "${args.replacement}"\n`; response += `Files modified: ${result.modifiedFiles}/${result.totalFiles}\n`; const responseObj = { content: [{ type: "text", text: response.trim() }] }; return addExecutionStatusToResponse(responseObj, 'ast_tool'); } export { ASTHelper, unifiedASTOperation }; export default UNIFIED_AST_TOOL;

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/AnEntrypoint/mcp-repl'

If you have feedback or need assistance with the MCP directory API, please join our Discord server