@opichi/smartcode
Version:
Universal code intelligence MCP server - analyze any codebase with TypeScript excellence and multi-language support
1,139 lines • 68.2 kB
JavaScript
/**
* IMPORTANT: Before modifying this file, please update CHANGELOG.md with a summary of your changes.
* Also, make clear comments about every change in this file and what it was replacing so that we don't end up trying the same fixes repeatedly.
*/
import * as fs from 'fs';
import * as path from 'path';
import { TypeScriptAnalyzer } from '../analyzer/typescript.js';
import { CodeIndexer } from '../analyzer/indexer.js';
// Debug logging function
function debugLog(message, data) {
if (!process.env.MCP_DEBUG)
return;
const timestamp = new Date().toISOString();
const logEntry = `[${timestamp}] TOOLS: ${message}${data ? ': ' + JSON.stringify(data, null, 2) : ''}\n`;
try {
fs.appendFileSync(path.join(process.cwd(), '.mcp-debug.log'), logEntry);
}
catch (error) {
// Fallback to stderr if file logging fails
console.error(`[${timestamp}] TOOLS: ${message}`, data || '');
}
}
/**
* Get the project root directory
*/
function getProjectRoot(projectPath = '.') {
const fullPath = path.resolve(projectPath);
// Look for common project indicators
const indicators = ['package.json', 'tsconfig.json', '.git', 'src'];
let current = fullPath;
while (current !== path.dirname(current)) {
if (indicators.some(indicator => fs.existsSync(path.join(current, indicator)))) {
return current;
}
current = path.dirname(current);
}
return fullPath;
}
/**
* Find symbols by name - locate functions, classes, variables, types
*/
export const findSymbol = {
name: 'find_symbol',
description: 'Find functions, classes, variables, or types by exact name across the entire codebase. Returns precise locations, signatures, and documentation. Use this when you need to locate existing code before creating new implementations.',
inputSchema: {
type: 'object',
properties: {
name: {
type: 'string',
description: 'Name of the symbol to find'
},
scope: {
type: 'string',
description: 'Optional file path or directory to limit search scope'
},
project_path: {
type: 'string',
description: 'Path to the project root',
default: '.'
}
},
required: ['name']
}
};
export async function handleFindSymbol(args) {
const { name, scope, project_path = '.' } = args;
try {
const projectRoot = getProjectRoot(project_path);
const analyzer = new TypeScriptAnalyzer(projectRoot);
const symbols = await analyzer.findSymbols(name, scope);
if (symbols.length === 0) {
return {
content: [{
type: 'text',
text: `No symbols found with name "${name}"${scope ? ` in scope "${scope}"` : ''}`
}]
};
}
const results = symbols.map(symbol => `**${symbol.name}** (${symbol.kind})\n` +
`📄 ${symbol.file}:${symbol.line}:${symbol.column}\n` +
(symbol.signature ? `🔧 ${symbol.signature}\n` : '') +
(symbol.documentation ? `📝 ${symbol.documentation}\n` : '') +
(symbol.modifiers?.length ? `🏷️ ${symbol.modifiers.join(', ')}\n` : '')).join('\n---\n');
return {
content: [{
type: 'text',
text: `Found ${symbols.length} symbol(s) named "${name}":\n\n${results}`
}]
};
}
catch (error) {
return {
content: [{
type: 'text',
text: `Error finding symbol: ${error instanceof Error ? error.message : String(error)}`
}]
};
}
}
/**
* Search for code patterns across the codebase
*/
export const searchCode = {
name: 'search_code',
description: 'Search for code patterns across any language codebase. IMPACT GUIDE: Language constructs (operators, keywords, common method patterns) generate high volume - start with max_results=5-10 and scope to source directories. Domain-specific terms generate low volume - safe for broader searches. PATTERN CLASSIFICATION: High-volume (single characters, operators, reserved keywords), Medium-volume (common patterns like function/class/import), Low-volume (CamelCase, snake_case, domain-specific terms). STRATEGY: Unknown pattern impact? Start conservatively with max_results=5-10 and narrow scope, then expand based on results. Focus source directories (src/, lib/, app/) before exploring build artifacts.',
inputSchema: {
type: 'object',
properties: {
pattern: {
type: 'string',
description: 'The pattern to search for'
},
project_path: {
type: 'string',
description: 'Path to the project root',
default: '.'
},
file_types: {
type: 'array',
items: { type: 'string' },
description: 'File extensions to search (e.g., [".ts", ".js"])'
},
case_sensitive: {
type: 'boolean',
description: 'Whether search should be case sensitive',
default: false
},
whole_word: {
type: 'boolean',
description: 'Whether to match whole words only',
default: false
},
regex: {
type: 'boolean',
description: 'Whether pattern is a regular expression',
default: false
},
max_results: {
type: 'number',
description: 'Maximum number of results to return (default: 25). For language constructs or broad patterns, start with 5-10. For specific identifiers, 25-50 is safe. Large result count suggests narrowing scope or refining pattern rather than increasing this limit.',
default: 25
}
},
required: ['pattern']
}
};
// Helper functions for smart guidance
/**
* Highlight pattern matches in content
*/
function highlightMatch(content, pattern, isRegex) {
if (isRegex) {
try {
const regex = new RegExp(pattern, 'gi');
return content.replace(regex, '**$&**');
}
catch {
return content;
}
}
function escapeRegex(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
return content.replace(new RegExp(escapeRegex(pattern), 'gi'), '**$&**');
}
function scoreFileImportance(file, matches, query) {
let score = 0;
// High-density files (many matches per file)
const fileMatches = matches.filter(m => m.file === file);
score += fileMatches.length * 10;
// Key architectural files
const lowerFile = file.toLowerCase();
if (lowerFile.includes('/routes/') || lowerFile.includes('/api/'))
score += 50;
if (lowerFile.includes('/controllers/') || lowerFile.includes('/handlers/'))
score += 40;
if (lowerFile.includes('/models/') || lowerFile.includes('/schemas/'))
score += 30;
if (lowerFile.includes('/services/') || lowerFile.includes('/utils/'))
score += 20;
// Main entry points
if (file.endsWith('index.js') || file.endsWith('app.js') || file.endsWith('index.ts') || file.endsWith('app.ts'))
score += 25;
if (file.endsWith('main.ts') || file.endsWith('server.js') || file.endsWith('main.py'))
score += 25;
// Query-specific relevance
if (lowerFile.includes(query.toLowerCase()))
score += 30;
return score;
}
function generateNextSteps(matches, query, fileGroups) {
const suggestions = [];
// Get high-importance matches for better recommendations
const highImportanceMatches = matches.filter(m => (m.importance || 0) > 2);
const hasHighValueResults = highImportanceMatches.length > 0;
// Get top files by enhanced importance scoring
const files = Object.keys(fileGroups);
const topFiles = files
.map(file => {
const fileMatches = fileGroups[file];
const avgImportance = fileMatches.reduce((sum, m) => sum + (m.importance || 0), 0) / fileMatches.length;
const baseScore = scoreFileImportance(file, matches, query);
return { file, score: baseScore + (avgImportance * 10) };
})
.sort((a, b) => b.score - a.score)
.slice(0, 3)
.map(item => item.file);
// Suggest analyzing high-value files first
if (hasHighValueResults && topFiles.length > 0) {
suggestions.push(`Analyze the most relevant files: analyze_file on "${topFiles[0]}"`);
}
// Function-context-based suggestions
const functionsFound = new Set(matches.filter(m => m.functionContext).map(m => m.functionContext));
if (functionsFound.size > 0) {
const topFunction = Array.from(functionsFound)[0];
suggestions.push(`Explore function context: search_code pattern "${topFunction}" for complete implementation`);
}
// Query-specific enhanced suggestions
const lowerQuery = query.toLowerCase();
if (lowerQuery.includes('route') || lowerQuery.includes('endpoint') || lowerQuery.includes('api')) {
suggestions.push('Get comprehensive API view: list_api_routes');
suggestions.push(`Find route handlers: search_code pattern "app\\.(get|post|put|delete)" regex true`);
}
if (lowerQuery.includes('component') || lowerQuery.includes('jsx') || lowerQuery.includes('tsx')) {
suggestions.push('Find component usage: search_code with file_types [".tsx", ".jsx"]');
}
if (lowerQuery.includes('type') || lowerQuery.includes('interface')) {
suggestions.push('Explore type definitions: search_code pattern "interface|type" regex true');
}
// Smart refinement suggestions based on results
if (matches.length > 50) {
const commonWords = extractCommonWords(matches);
if (commonWords.length > 0) {
suggestions.push(`Narrow down results: search_code pattern "${commonWords[0]}" whole_word true`);
}
// Suggest directory-focused searches for large result sets
const directories = getTopDirectories(fileGroups);
if (directories.length > 0) {
suggestions.push(`Focus on key directory: search in project_path "./${directories[0]}"`);
}
}
// Pattern enhancement suggestions
if (!query.includes('*') && !query.includes('|') && matches.length < 5) {
suggestions.push(`Broaden search: try pattern "*${query}*" or "${query}*"`);
}
// File type filtering suggestions
const fileTypes = getCommonFileTypes(files);
if (fileTypes.length > 1 && matches.length > 20) {
suggestions.push(`Filter by specific types: file_types ${JSON.stringify(fileTypes.slice(0, 2))}`);
}
return suggestions.slice(0, 4); // Limit to most useful suggestions
}
function extractCommonWords(matches) {
const words = {};
matches.forEach(match => {
const content = match.content.toLowerCase();
const foundWords = content.match(/\b[a-z]+\b/g) || [];
foundWords.forEach((word) => {
if (word.length > 3) {
words[word] = (words[word] || 0) + 1;
}
});
});
return Object.entries(words)
.sort(([, a], [, b]) => b - a)
.slice(0, 3)
.map(([word]) => word);
}
function getTopDirectories(fileGroups) {
const dirCounts = {};
Object.keys(fileGroups).forEach(file => {
const dir = path.dirname(file);
if (dir !== '.') {
dirCounts[dir] = (dirCounts[dir] || 0) + fileGroups[file].length;
}
});
return Object.entries(dirCounts)
.sort(([, a], [, b]) => b - a)
.slice(0, 2)
.map(([dir]) => dir);
}
function getCommonFileTypes(files) {
const extensions = {};
files.forEach(file => {
const ext = path.extname(file);
if (ext) {
extensions[ext] = (extensions[ext] || 0) + 1;
}
});
return Object.entries(extensions)
.sort(([, a], [, b]) => b - a)
.map(([ext]) => ext);
}
export async function handleSearchCode(args) {
const { pattern, project_path = '.', file_types, case_sensitive = false, whole_word = false, regex = false, max_results = 25 } = args;
try {
const projectRoot = getProjectRoot(project_path);
const analyzer = new TypeScriptAnalyzer(projectRoot);
const options = {
fileTypes: file_types,
caseSensitive: case_sensitive,
wholeWord: whole_word,
regex: regex
};
const matches = await analyzer.searchCode(pattern, options);
if (matches.length === 0) {
return {
content: [{
type: 'text',
text: `No matches found for pattern "${pattern}"`
}]
};
}
// Group matches by file
const fileGroups = matches.reduce((acc, match) => {
if (!acc[match.file])
acc[match.file] = [];
acc[match.file].push(match);
return acc;
}, {});
// Smart result limiting and guidance
const totalMatches = matches.length;
const totalFiles = Object.keys(fileGroups).length;
const limitedMatches = matches.slice(0, max_results);
const hasMore = totalMatches > max_results;
// Create limited file groups for display
const limitedFileGroups = limitedMatches.reduce((acc, match) => {
if (!acc[match.file])
acc[match.file] = [];
acc[match.file].push(match);
return acc;
}, {});
// Enhanced result formatting with function context and highlighting
const results = Object.entries(limitedFileGroups).map(([file, fileMatches]) => {
// Sort matches within file by importance
const sortedMatches = fileMatches.sort((a, b) => (b.importance || 0) - (a.importance || 0));
const matchTexts = sortedMatches.map(match => {
let line = ` 📍 **Line ${match.line}**`;
// Add function context if available
if (match.functionContext) {
line += ` (in \`${match.functionContext}\`)`;
}
// Add importance indicator for high-value matches
if (match.importance && match.importance > 3) {
line += ` ⭐`;
}
line += `:\n ${highlightMatch(match.content, match.matchedText || pattern, pattern)}`;
return line;
}).join('\n\n');
// Calculate file relevance score
const avgImportance = fileMatches.reduce((sum, m) => sum + (m.importance || 0), 0) / fileMatches.length;
const fileIcon = avgImportance > 2 ? '🔥' : '📄';
return `${fileIcon} **${file}** (${fileMatches.length} match${fileMatches.length === 1 ? '' : 'es'})\n${matchTexts}`;
}).join('\n\n');
// Enhanced header with more context
let responseText = `🔍 Found ${totalMatches} match(es) in ${totalFiles} file(s)`;
// Add pattern type information
const patternInfo = regex ? ' (regex pattern)' : whole_word ? ' (whole word)' : '';
if (patternInfo) {
responseText += patternInfo;
}
if (hasMore) {
responseText += ` (showing top ${max_results} by relevance)`;
// Enhanced guidance with importance-based recommendations
const nextSteps = generateNextSteps(matches, pattern, fileGroups);
const topFiles = Object.entries(fileGroups)
.map(([file, fileMatches]) => ({
file,
count: fileMatches.length,
avgImportance: fileMatches.reduce((sum, m) => sum + (m.importance || 0), 0) / fileMatches.length
}))
.sort((a, b) => b.avgImportance - a.avgImportance)
.slice(0, 5)
.map(item => `${item.file}: ${item.count} (score: ${item.avgImportance.toFixed(1)})`)
.join('\n ');
responseText += `\n\n## 🎯 **Recommended Actions:**\n${nextSteps.map((step) => `- ${step}`).join('\n')}`;
responseText += `\n\n## 📊 **Top Files by Relevance:**\n ${topFiles}`;
}
responseText += `\n\n${results}`;
return {
content: [{
type: 'text',
text: responseText
}]
};
}
catch (error) {
return {
content: [{
type: 'text',
text: `Error searching code: ${error instanceof Error ? error.message : String(error)}`
}]
};
}
}
/**
* Analyze complete structure of a file
*/
export const analyzeFile = {
name: 'analyze_file',
description: 'Analyze complete file structure across any language. SIZE GUIDE: Large files (>1000 lines) generate detailed output - ensure you need full analysis. Binary/generated files may cause errors - focus on source files. STRATEGY: Target specific files identified through search_code or project overview tools. Use after broader exploration to deep-dive on key files.',
inputSchema: {
type: 'object',
properties: {
file_path: {
type: 'string',
description: 'Path to the file to analyze (relative to project root)'
},
project_path: {
type: 'string',
description: 'Path to the project root',
default: '.'
}
},
required: ['file_path']
}
};
export async function handleAnalyzeFile(args) {
const { file_path, project_path = '.' } = args;
try {
const projectRoot = getProjectRoot(project_path);
const analyzer = new TypeScriptAnalyzer(projectRoot);
const structure = await analyzer.analyzeFile(file_path);
if (!structure) {
return {
content: [{
type: 'text',
text: `Could not analyze file "${file_path}". This may indicate a TypeScript compiler initialization issue.`
}]
};
}
let result = `# Analysis of ${structure.file}\n\n`;
// Imports
if (structure.imports && structure.imports.length > 0) {
result += `## Imports\n`;
structure.imports.forEach(imp => {
const type = imp.isDefault ? 'default' : imp.isNamespace ? 'namespace' : 'named';
result += `- **${imp.name}** from "${imp.from}" (${type})\n`;
});
result += '\n';
}
// Functions
if (structure.functions && structure.functions.length > 0) {
result += `## Functions\n`;
structure.functions.forEach(func => {
const params = func.parameters.map(p => `${p.name}${p.isOptional ? '?' : ''}${p.type ? `: ${p.type}` : ''}`).join(', ');
result += `- **${func.name}**(${params})${func.returnType ? `: ${func.returnType}` : ''}\n`;
result += ` 📍 Line ${func.line}${func.isAsync ? ' (async)' : ''}${func.isExported ? ' (exported)' : ''}\n`;
if (func.documentation) {
result += ` 📝 ${func.documentation}\n`;
}
});
result += '\n';
}
// Classes
if (structure.classes && structure.classes.length > 0) {
result += `## Classes\n`;
structure.classes.forEach(cls => {
result += `- **${cls.name}**\n`;
result += ` 📍 Line ${cls.line}${cls.isExported ? ' (exported)' : ''}\n`;
if (cls.extends)
result += ` 🔗 Extends: ${cls.extends}\n`;
if (cls.implements && cls.implements.length > 0)
result += ` 🔗 Implements: ${cls.implements.join(', ')}\n`;
if (cls.documentation)
result += ` 📝 ${cls.documentation}\n`;
});
result += '\n';
}
// Interfaces
if (structure.interfaces && structure.interfaces.length > 0) {
result += `## Interfaces\n`;
structure.interfaces.forEach(int => {
result += `- **${int.name}**\n`;
result += ` 📍 Line ${int.line}${int.isExported ? ' (exported)' : ''}\n`;
if (int.extends && int.extends.length > 0)
result += ` 🔗 Extends: ${int.extends.join(', ')}\n`;
if (int.documentation)
result += ` 📝 ${int.documentation}\n`;
});
result += '\n';
}
// Types
if (structure.types && structure.types.length > 0) {
result += `## Type Aliases\n`;
structure.types.forEach(type => {
result += `- **${type.name}** = ${type.definition}\n`;
result += ` 📍 Line ${type.line}${type.isExported ? ' (exported)' : ''}\n`;
if (type.documentation)
result += ` 📝 ${type.documentation}\n`;
});
result += '\n';
}
// Variables
if (structure.variables && structure.variables.length > 0) {
result += `## Variables\n`;
structure.variables.forEach(variable => {
result += `- **${variable.name}**${variable.type ? `: ${variable.type}` : ''}`;
if (variable.value)
result += ` = ${variable.value}`;
result += '\n';
result += ` 📍 Line ${variable.line}${variable.isConst ? ' (const)' : ''}${variable.isExported ? ' (exported)' : ''}\n`;
});
result += '\n';
}
// Exports
if (structure.exports && structure.exports.length > 0) {
result += `## Exports\n`;
structure.exports.forEach(exp => {
result += `- **${exp.name}** (${exp.kind})${exp.isDefault ? ' (default)' : ''}\n`;
});
}
return {
content: [{
type: 'text',
text: result
}]
};
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
// Provide specific, helpful error messages
if (errorMessage.includes('File does not exist')) {
return {
content: [{
type: 'text',
text: `File not found: "${file_path}". Please check the file path and ensure the file exists.`
}]
};
}
if (errorMessage.includes('EACCES') || errorMessage.includes('permission denied')) {
return {
content: [{
type: 'text',
text: `Permission denied: Cannot read file "${file_path}". Please check file permissions.`
}]
};
}
if (errorMessage.includes('EISDIR')) {
return {
content: [{
type: 'text',
text: `"${file_path}" is a directory, not a file. Please specify a file path.`
}]
};
}
// Generic error for other cases
return {
content: [{
type: 'text',
text: `Error analyzing file "${file_path}": ${errorMessage}`
}]
};
}
}
/**
* Get high-level project structure and overview
*/
export const getProjectStructure = {
name: 'get_project_structure',
description: 'Get comprehensive project architecture overview across any language/framework. DEPTH GUIDE: Large codebases (>1000 files) should start with max_depth=2 to avoid overwhelming output. Deep nesting (>5 levels) indicates complex projects - increase depth incrementally. STRATEGY: Unknown project size? Start with max_depth=2, then expand based on complexity. Focus on understanding core structure before exploring deep hierarchies.',
inputSchema: {
type: 'object',
properties: {
project_path: {
type: 'string',
description: 'Path to the project root',
default: '.'
},
max_depth: {
type: 'number',
description: 'Directory depth to analyze (default: 3). Large codebases should start with 2. Complex projects (>10 subdirectories) benefit from incremental depth increases. Deeper analysis = more detailed but potentially overwhelming output.',
default: 3
}
}
}
};
export async function handleGetProjectStructure(args) {
const { project_path = '.', max_depth = 3 } = args;
try {
const projectRoot = getProjectRoot(project_path);
// Read key configuration files
const configFiles = {};
const configFilePaths = ['package.json', 'tsconfig.json', '.gitignore'];
for (const configFile of configFilePaths) {
const configPath = path.join(projectRoot, configFile);
if (fs.existsSync(configPath)) {
try {
if (configFile.endsWith('.json')) {
configFiles[configFile] = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
}
else {
configFiles[configFile] = fs.readFileSync(configPath, 'utf-8');
}
}
catch (error) {
// Ignore parse errors
}
}
}
// Analyze directory structure
const analyzeDirectory = (dir, depth = 0) => {
if (depth > max_depth)
return null;
const items = [];
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
if (entry.name.startsWith('.') && !['src', '.github'].includes(entry.name))
continue;
if (entry.name === 'node_modules')
continue;
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
const subItems = analyzeDirectory(fullPath, depth + 1);
items.push({
name: entry.name,
type: 'directory',
children: subItems || []
});
}
else {
items.push({
name: entry.name,
type: 'file',
size: fs.statSync(fullPath).size
});
}
}
return items;
};
const structure = analyzeDirectory(projectRoot);
let result = `# Project Structure: ${path.basename(projectRoot)}\n\n`;
// Project info from package.json
if (configFiles['package.json']) {
const pkg = configFiles['package.json'];
result += `## Project Info\n`;
result += `- **Name**: ${pkg.name || 'Unknown'}\n`;
result += `- **Version**: ${pkg.version || 'Unknown'}\n`;
if (pkg.description)
result += `- **Description**: ${pkg.description}\n`;
if (pkg.main)
result += `- **Entry Point**: ${pkg.main}\n`;
if (pkg.scripts && Object.keys(pkg.scripts).length > 0) {
result += `- **Scripts**: ${Object.keys(pkg.scripts).join(', ')}\n`;
}
if (pkg.dependencies && Object.keys(pkg.dependencies).length > 0) {
result += `- **Dependencies**: ${Object.keys(pkg.dependencies).length} packages\n`;
}
result += '\n';
}
// TypeScript configuration
if (configFiles['tsconfig.json']) {
const tsConfig = configFiles['tsconfig.json'];
result += `## TypeScript Configuration\n`;
if (tsConfig.compilerOptions) {
const opts = tsConfig.compilerOptions;
if (opts.target)
result += `- **Target**: ${opts.target}\n`;
if (opts.module)
result += `- **Module**: ${opts.module}\n`;
if (opts.strict !== undefined)
result += `- **Strict Mode**: ${opts.strict}\n`;
}
result += '\n';
}
// Directory structure
result += `## Directory Structure\n`;
const formatStructure = (items, indent = '') => {
return items.map(item => {
const icon = item.type === 'directory' ? '📁' : '📄';
const line = `${indent}${icon} ${item.name}`;
if (item.type === 'directory' && item.children && item.children.length > 0) {
return line + '\n' + formatStructure(item.children, indent + ' ');
}
return line;
}).join('\n');
};
result += formatStructure(structure || []);
// Identify key patterns
result += '\n\n## Architecture Patterns Detected\n';
const patterns = [];
// Check for common patterns
const hasDir = (name) => structure?.some((item) => item.name === name && item.type === 'directory');
const hasFile = (name) => structure?.some((item) => item.name === name && item.type === 'file');
if (hasDir('src'))
patterns.push('Source directory structure');
if (hasDir('components') || hasDir('src/components'))
patterns.push('Component-based architecture');
if (hasDir('pages') || hasDir('src/pages'))
patterns.push('Page-based routing');
if (hasDir('api') || hasDir('src/api'))
patterns.push('API layer separation');
if (hasDir('types') || hasDir('src/types'))
patterns.push('TypeScript definitions');
if (hasDir('utils') || hasDir('src/utils'))
patterns.push('Utility functions');
if (hasFile('next.config.js'))
patterns.push('Next.js framework');
if (hasFile('vite.config.ts') || hasFile('vite.config.js'))
patterns.push('Vite build tool');
if (configFiles['package.json']?.dependencies?.react)
patterns.push('React framework');
if (configFiles['package.json']?.dependencies?.express)
patterns.push('Express.js server');
result += patterns.length > 0 ? patterns.map(p => `- ${p}`).join('\n') : 'No specific patterns detected';
return {
content: [{
type: 'text',
text: result
}]
};
}
catch (error) {
return {
content: [{
type: 'text',
text: `Error analyzing project structure: ${error instanceof Error ? error.message : String(error)}`
}]
};
}
}
/**
* List API routes and endpoints in the project
*/
export const listApiRoutes = {
name: 'list_api_routes',
description: 'Discover all API endpoints across any web framework. IMPACT GUIDE: Large applications (>50 routes) can generate overwhelming output - use include_globs to focus on specific directories first. Framework detection works for Express, Next.js, FastAPI, Django, Spring, etc. STRATEGY: Unknown API size? Start with mode="strict" and specific include_globs=["**/api/**", "**/routes/**"]. Large result count suggests narrowing scope rather than switching to loose mode.',
inputSchema: {
type: 'object',
properties: {
project_path: {
type: 'string',
description: 'Path to the project root',
default: '.'
},
// NEW: Minimal, strategic params for precision and cross-language utility
include_globs: {
type: 'array',
items: { type: 'string' },
description: 'Path patterns to include (e.g., ["**/api/**"]). For large projects, start specific to avoid overwhelming output. Unknown API structure? Try ["**/routes/**", "**/api/**", "**/handlers/**"] first.'
},
exclude_globs: {
type: 'array',
items: { type: 'string' },
description: 'Optional path globs to exclude (e.g., ["**/client/**", "**/frontend/**"])'
},
file_types: {
type: 'array',
items: { type: 'string' },
description: 'File extensions to search (e.g., [".ts", ".js", ".py"])'
},
mode: {
type: 'string',
enum: ['strict', 'loose'],
description: 'strict = applies light validation and server-first scoping; loose = broad discovery',
default: 'strict'
}
}
}
};
export async function handleListApiRoutes(args) {
const { project_path = '.', include_globs, exclude_globs, file_types, mode = 'strict' } = args;
try {
const projectRoot = getProjectRoot(project_path);
const analyzer = new TypeScriptAnalyzer(projectRoot);
// NEW: Minimal glob matcher (supports ** and *). Keeps footprint tiny, no deps.
const globToRegex = (glob) => {
const escaped = glob
.replace(/[.+^${}()|\\]/g, r => `\\${r}`)
.replace(/\*\*/g, '::DOUBLE_STAR::')
.replace(/\*/g, '[^/]*')
.replace(/::DOUBLE_STAR::/g, '.*');
return new RegExp('^' + escaped + '$');
};
const normalizePath = (p) => ('/' + p.replace(/\\/g, '/')).replace(/\/+/g, '/');
const pathMatchesAny = (p, globs) => globs.some(g => globToRegex(g).test(normalizePath(p)));
// Defaults: server-first includes and common frontend excludes
const defaultIncludeGlobs = [
'**/server/**',
'**/api/**',
'**/backend/**',
'**/functions/**',
'**/cloudfunctions/**'
];
const defaultExcludeGlobs = [
'**/client/**',
'**/frontend/**',
'**/src/components/**',
'**/app/**/components/**'
];
const includes = include_globs && include_globs.length ? include_globs : defaultIncludeGlobs;
const excludes = exclude_globs && exclude_globs.length ? exclude_globs : defaultExcludeGlobs;
// Search for common API patterns
const detectProjectFileTypes = () => {
const commonExtensions = ['.ts', '.js', '.tsx', '.jsx', '.py', '.go', '.php', '.rb', '.java', '.cs'];
const detectedTypes = [];
// Check for common project indicators
try {
const projectFiles = fs.readdirSync(projectRoot, { recursive: true, withFileTypes: true })
.filter(entry => entry.isFile())
.map(entry => entry.name);
commonExtensions.forEach(ext => {
if (projectFiles.some(file => file.endsWith(ext))) {
detectedTypes.push(ext);
}
});
// Default to common server-side files if nothing detected
return detectedTypes.length > 0 ? detectedTypes : ['.ts', '.js', '.py', '.go', '.rb', '.php'];
}
catch {
return ['.ts', '.js', '.py', '.go', '.php', '.rb'];
}
};
// NEW: Tightened, cross-language regex patterns (still lightweight)
const apiPatterns = [
// JavaScript/TypeScript (Express/Router) — simplified, engine-friendly
'(?:app|router)\\.(?:get|post|put|delete|patch)\\s*\\(',
// Next.js route handlers (gated by path later)
'export\\s+(?:async\\s+)?function\\s+(GET|POST|PUT|DELETE|PATCH)\\s*\\(',
// NestJS/Decorators
'@(?:Get|Post|Put|Delete|Patch)\\s*\\(',
// Python Flask
'@(?:app|bp)\\.route\\s*\\(|app\\.add_url_rule\\s*\\(',
// Python FastAPI
'app\\.(get|post|put|delete|patch)\\s*\\(',
// Go (chi, mux, net/http)
'r\\.(Get|Post|Put|Delete|Patch)\\s*\\(|(?:mux|http)\\.HandleFunc\\s*\\(',
// PHP (Laravel, Slim)
'Route::(get|post|put|delete|patch)\\s*\\(|\\$app->(get|post|put|delete|patch)\\s*\\(',
// Ruby on Rails
'(^|\\s)(get|post|put|delete)\\s+["\']|resources\\s+:',
// Java Spring
'@(?:Get|Post|Put|Delete|Patch)Mapping\\s*\\(|@RequestMapping\\s*\\(',
// .NET (attributes or minimal APIs)
'\\[Http(?:Get|Post|Put|Delete|Patch)\\]|app\\.Map(?:Get|Post|Put|Delete|Patch)\\s*\\('
];
const projectFileTypes = file_types && file_types.length ? file_types : detectProjectFileTypes();
const allRoutes = [];
for (const pattern of apiPatterns) {
try {
const matches = await analyzer.searchCode(pattern, {
regex: true,
fileTypes: projectFileTypes
});
if (matches.length > 0) {
// Optional path filtering based on includes/excludes (skip in loose mode)
const filtered = (mode === 'loose') ? matches : matches.filter(m => {
const filePath = m.file.replace(/\\/g, '/');
const includePass = includes.length ? pathMatchesAny(filePath, includes) : true;
const excludePass = excludes.length ? !pathMatchesAny(filePath, excludes) : true;
return includePass && excludePass;
});
if (filtered.length > 0) {
allRoutes.push({ pattern, matches: filtered });
}
}
}
catch (error) {
// Ignore errors
}
}
if (allRoutes.length === 0) {
if (mode === 'loose') {
// Best-effort: re-scan without include/exclude filters to avoid false zeroes
const bestEffortRoutes = [];
for (const pattern of apiPatterns) {
try {
const matches = await analyzer.searchCode(pattern, { regex: true, fileTypes: projectFileTypes });
if (matches.length > 0)
bestEffortRoutes.push({ pattern, matches });
}
catch { }
}
if (bestEffortRoutes.length > 0) {
allRoutes.push(...bestEffortRoutes);
}
}
if (allRoutes.length === 0) {
return {
content: [{
type: 'text',
text: 'No API routes found. Tips: try mode="loose", broaden file_types (e.g., [".ts", ".js", ".py"]), or adjust include_globs (e.g., ["**/server/**", "**/api/**"]).'
}]
};
}
}
// Minimal validation in strict mode to reduce false positives
const isLikelyRouteMatch = (match) => {
if (mode !== 'strict')
return true;
const f = match.file.toLowerCase();
const line = (match.content || '').toLowerCase();
// Next.js: only accept within pages/api or app/api
if (/export\s+(async\s+)?function\s+(get|post|put|delete|patch)\s*\(/i.test(match.content || '')) {
return f.includes('/pages/api/') || f.includes('/app/api/');
}
// Rails routes should be in config/routes.rb
if (/resources\s+:|\b(get|post|put|delete)\s+["']/.test(line)) {
return f.endsWith('config/routes.rb') || f.includes('/config/routes');
}
// General server-first heuristic
return includes.length ? pathMatchesAny(match.file.replace(/\\/g, '/'), includes) : true;
};
let result = `# API Routes Found\n\n`;
// Group by file
const fileGroups = allRoutes
.flatMap(route => route.matches)
.filter(isLikelyRouteMatch)
// Deduplicate by file:line to reduce noise
.filter((m, idx, arr) => arr.findIndex(x => x.file === m.file && x.line === m.line) === idx)
.reduce((acc, match) => {
if (!acc[match.file])
acc[match.file] = [];
acc[match.file].push(match);
return acc;
}, {});
Object.entries(fileGroups).forEach(([file, matches]) => {
result += `## 📄 ${file}\n\n`;
matches.forEach((match) => {
result += `**Line ${match.line}**: \`${match.content.trim()}\`\n\n`;
// Show context for better understanding
if (match.context && match.context.length > 1) {
result += '```typescript\n';
match.context.forEach((line, index) => {
const lineNum = match.line - 2 + index;
const marker = lineNum === match.line ? '→ ' : ' ';
result += `${marker}${line}\n`;
});
result += '```\n\n';
}
});
});
return {
content: [{
type: 'text',
text: result
}]
};
}
catch (error) {
return {
content: [{
type: 'text',
text: `Error finding API routes: ${error instanceof Error ? error.message : String(error)}`
}]
};
}
}
/**
* Get code index - the complete "table of contents" for the project
*/
export const getProjectOverview = {
name: 'get_project_overview',
description: 'Get lightweight project overview with summary, structure, and key files. Use this first to understand project organization without overwhelming context.',
inputSchema: {
type: 'object',
properties: {
project_path: {
type: 'string',
description: 'Path to the project root',
default: '.'
}
}
}
};
export const getFunctionsByFile = {
name: 'get_functions_by_file',
description: 'Get all functions in a specific file with detailed signatures and documentation.',
inputSchema: {
type: 'object',
properties: {
file_path: {
type: 'string',
description: 'Path to the file (relative to project root)'
},
project_path: {
type: 'string',
description: 'Path to the project root',
default: '.'
}
},
required: ['file_path']
}
};
export const searchFunctions = {
name: 'search_functions',
description: 'Search for functions by name, documentation, or signature content.',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'Search query for function names, documentation, or signatures'
},
project_path: {
type: 'string',
description: 'Path to the project root',
default: '.'
}
},
required: ['query']
}
};
export const getTypesByPattern = {
name: 'get_types_by_pattern',
description: 'Get types (interfaces, classes, type aliases) matching a name pattern.',
inputSchema: {
type: 'object',
properties: {
pattern: {
type: 'string',
description: 'Pattern to match type names (supports regex)'
},
project_path: {
type: 'string',
description: 'Path to the project root',
default: '.'
}
},
required: ['pattern']
}
};
export const getComponentsByPattern = {
name: 'get_components_by_pattern',
description: 'Get React components matching a name or file pattern.',
inputSchema: {
type: 'object',
properties: {
pattern: {
type: 'string',
description: 'Pattern to match component names or file paths (supports regex)'
},
project_path: {
type: 'string',
description: 'Path to the project root',
default: '.'
}
},
required: ['pattern']
}
};
export const getRoutesByPattern = {
name: 'get_routes_by_pattern',
description: 'Get API routes matching a method, path, or handler pattern.',
inputSchema: {
type: 'object',
properties: {
pattern: {
type: 'string',
description: 'Pattern to match route method, path, or handler (supports regex)'
},
project_path: {
type: 'string',
description: 'Path to the project root',
default: '.'
},
// NEW: Optional scoping and precision parameters
include_globs: {
type: 'array',
items: { type: 'string' },
description: 'Optional path globs to include (e.g., ["**/server/**", "**/api/**"])'
},
exclude_globs: {
type: 'array',
items: { type: 'string' },
description: 'Optional path globs to exclude (e.g., ["**/client/**", "**/frontend/**"])'
},
file_types: {
type: 'array',
items: { type: 'string' },
description: 'File extensions to search (e.g., [".ts", ".js", ".py"])'
},
mode: {
type: 'string',
enum: ['strict', 'loose'],
description: 'strict = applies light validation and server-first scoping; loose = broad discovery',
default: 'strict'
}
},
required: ['pattern']
}
};
export const getCodeIndex = {
name: 'get_code_index',
description: 'Get complete indexed overview of all code elements. RESOURCE GUIDE: Large codebases (>500 files) can generate massive output and long processing times. CACHING: Index cached in .mcp-cache/code-index.json with lastUpdated timestamp. Cache automatically invalidated when source files are newer than cache timestamp. Tool compares file modification times vs cached timestamp - no manual checking needed. Use rebuild=false (default) for automatic cache management. Use rebuild=true only when: forcing complete rebuild, troubleshooting cache issues, or when automatic invalidation seems insufficient. STRATEGY: Check tool output for lastUpdated timestamp - if recent, subsequent identical calls will be nearly instant due to intelligent file-timestamp-based cache invalidation.',
inputSchema: {
type: 'object',
properties: {
project_path: {
type: 'string',
description: 'Path to the project root',
default: '.'
},
rebuild: {
type: 'boolean',
description: 'Force rebuild the index even if cache exists (default: false). Cache automatically invalidates when source files are newer than cached timestamp (file mtime > cache lastUpdated). Set to true only for: forcing complete rebuild, troubleshooting cache issues, or when automatic invalidation seems insufficient. Check .mcp-cache/code-index.json for lastUpdated vs your recent changes.',
default: false
}
}
}
};
export async function handleGetProjectOverview(args) {
const { project_path = '.' } = args;
debugLog('handleGetProjectOverview called', { project_path });
try {
debugLog('Getting project root');
const projectRoot = getProjectRoot(project_path);
debugLog('Project root found', { projectRoot });
debugLog('Creating CodeIndexer');
const indexer = new CodeIndexer(projectRoot);
debugLog('CodeIndexer created');
debugLog('Getting project overview - this may take time');
const overview = await indexer.getProjectOverview();
debugLog('Project overview completed', { overviewKeys: Object.keys(overview) });
let result = `# 📋 Project Overview: ${path.basename(projectRoot)}\n\n`;
result += `${overview.summary}\n\n`;
if (overview.structure.length > 0) {
result += `## 📁 Directory Structure\n`;
overview.structure.forEach(dir => {
result += `- ${dir}/\n`;
});
result += '\n';
}
if (overview.keyFiles.length > 0) {
result += `## 🔑 Key Files\n`;
overview.keyFiles.forEach(file => {
result += `- ${file}\n`;
});
result += '\n';
}
if (overview.entryPoints.length > 0) {
result += `## 🚀 Entry Points\n`;
overview.entryPoints.forEach(entry => {
result += `- ${entry}\n`;
});
result += '\n';
}
result += `## 📊 Quick Stats\n`;
const exportedFunc