mcp-codebase-search
Version:
MCP server for semantic codebase search using embeddings
348 lines (299 loc) • 9.91 kB
JavaScript
/**
* MCP Codebase Indexer - Node.js Version (Sem FAISS)
*
* Este script indexa um codebase para consultas semânticas usando:
* - @xenova/transformers para embeddings semânticos
* - Armazenamento JSON simples para compatibilidade
* - Sistema de cache local para performance
*
* Uso: node index-codebase.js [caminho-do-projeto]
*/
const fs = require('fs-extra');
const path = require('path');
const { glob } = require('glob');
const { pipeline } = require('@xenova/transformers');
const winston = require('winston');
const mime = require('mime-types');
// __dirname já está disponível em CommonJS
// Configuração do logger
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.simple()
),
transports: [
new winston.transports.Console(),
new winston.transports.File({ filename: 'indexer.log' })
]
});
class CodebaseIndexer {
constructor(projectPath = '.') {
this.projectPath = path.resolve(projectPath);
this.indexPath = path.join(this.projectPath, 'code_index'); // Índice no diretório do projeto
this.embedder = null;
this.documents = [];
this.embeddings = [];
// Extensões de arquivo suportadas (com foco em Flutter/Dart)
this.supportedExtensions = [
// Flutter/Dart (prioridade)
'.dart',
// Web e Mobile
'.js', '.jsx', '.ts', '.tsx', '.vue', '.svelte',
'.html', '.htm', '.xml', '.css', '.scss', '.sass', '.less',
// Outras linguagens
'.py', '.pyx', '.pyi',
'.java', '.kt', '.scala',
'.cpp', '.cc', '.cxx', '.c', '.h', '.hpp',
'.cs', '.vb',
'.php', '.rb', '.go', '.rs', '.swift',
'.sql', '.graphql', '.gql',
// Configuração e dados (incluindo Flutter)
'.json', '.yaml', '.yml', '.toml', '.ini', '.cfg',
'.pubspec', '.gradle', '.plist', '.arb',
// Documentação
'.md', '.mdx', '.rst', '.txt',
// Scripts
'.sh', '.bash', '.zsh', '.fish', '.ps1', '.bat', '.cmd',
'.dockerfile', '.dockerignore',
'.gitignore', '.gitattributes',
'.env', '.env.example', '.env.local'
];
// Diretórios e arquivos a ignorar (incluindo Flutter/Dart específicos)
this.ignorePatterns = [
'node_modules/**',
'.git/**',
'.svn/**',
'.hg/**',
'dist/**',
'build/**',
'out/**',
'target/**',
'bin/**',
'obj/**',
'.next/**',
'.nuxt/**',
'.vscode/**',
'.idea/**',
'__pycache__/**',
'*.pyc',
'*.pyo',
'*.pyd',
'.pytest_cache/**',
'coverage/**',
'.coverage',
'.nyc_output/**',
'logs/**',
'*.log',
'.DS_Store',
'Thumbs.db',
'*.tmp',
'*.temp',
'*.swp',
'*.swo',
'*~',
'.env.production',
'.env.staging',
// Flutter/Dart específicos
'.dart_tool/**',
'.flutter-plugins',
'.flutter-plugins-dependencies',
'.packages',
'pubspec.lock',
'android/app/build/**',
'ios/Runner.xcworkspace/**',
'.metadata',
'.fvm/**'
];
}
/**
* Inicializa o modelo de embeddings
*/
async initializeEmbedder() {
try {
logger.info('Inicializando modelo de embeddings...');
// Usando sentence-transformers via @xenova/transformers
this.embedder = await pipeline(
'feature-extraction',
'Xenova/all-MiniLM-L6-v2',
{
quantized: true,
progress_callback: (progress) => {
if (progress.status === 'downloading') {
logger.info(`Baixando modelo: ${Math.round(progress.progress || 0)}%`);
}
}
}
);
logger.info('Modelo de embeddings carregado com sucesso!');
} catch (error) {
logger.error('Erro ao carregar modelo de embeddings:', error);
throw error;
}
}
/**
* Coleta todos os arquivos relevantes do projeto
*/
async collectFiles() {
logger.info(`Coletando arquivos do projeto: ${this.projectPath}`);
try {
const allFiles = await glob('**/*', {
cwd: this.projectPath,
ignore: this.ignorePatterns,
nodir: true,
dot: false
});
// Filtrar por extensões suportadas
const relevantFiles = allFiles.filter(file => {
const ext = path.extname(file).toLowerCase();
const basename = path.basename(file);
// Incluir arquivos com extensões suportadas ou arquivos especiais
return this.supportedExtensions.includes(ext) ||
['Dockerfile', 'Makefile', 'Rakefile', 'Gemfile', 'Pipfile'].includes(basename);
});
// Arquivos coletados
return relevantFiles;
} catch (error) {
logger.error('Erro ao coletar arquivos:', error);
throw error;
}
}
/**
* Processa um arquivo e extrai seu conteúdo
*/
async processFile(filePath) {
const fullPath = path.join(this.projectPath, filePath);
try {
const stats = await fs.stat(fullPath);
// Pular arquivos muito grandes (>1MB)
if (stats.size > 1024 * 1024) {
// Arquivo muito grande, pulando
return null;
}
const content = await fs.readFile(fullPath, 'utf-8');
// Pular arquivos vazios ou muito pequenos
if (!content.trim() || content.length < 10) {
// Arquivo vazio ou muito pequeno, pulando
return null;
}
// Criar documento
const document = {
id: this.documents.length,
path: filePath,
content: content,
size: stats.size,
extension: path.extname(filePath).toLowerCase(),
lastModified: stats.mtime.toISOString(),
// Metadados para busca
summary: this.createSummary(content, filePath)
};
return document;
} catch (error) {
// Erro ao processar arquivo
return null;
}
}
/**
* Cria um resumo do arquivo para busca
*/
createSummary(content, filePath) {
const lines = content.split('\n');
const ext = path.extname(filePath).toLowerCase();
// Extrair informações relevantes baseadas no tipo de arquivo
let summary = `Arquivo: ${filePath}\n`;
if (ext === '.dart') {
// Para arquivos Dart/Flutter
const classes = content.match(/class\s+(\w+)/g) || [];
const functions = content.match(/\w+\s+\w+\s*\([^)]*\)\s*{/g) || [];
const widgets = content.match(/class\s+(\w+)\s+extends\s+StatelessWidget|class\s+(\w+)\s+extends\s+StatefulWidget/g) || [];
if (classes.length > 0) summary += `Classes: ${classes.join(', ')}\n`;
if (widgets.length > 0) summary += `Widgets: ${widgets.join(', ')}\n`;
if (functions.length > 0) summary += `Funções: ${functions.slice(0, 5).join(', ')}\n`;
}
// Adicionar primeiras linhas como contexto
const firstLines = lines.slice(0, 10).join('\n');
summary += `Conteúdo:\n${firstLines}`;
return summary.substring(0, 1000); // Limitar tamanho
}
/**
* Gera embedding para um texto
*/
async generateEmbedding(text) {
try {
const output = await this.embedder(text, { pooling: 'mean', normalize: true });
return Array.from(output.data);
} catch (error) {
logger.error('Erro ao gerar embedding:', error);
throw error;
}
}
/**
* Salva o índice em formato JSON
*/
async saveIndex() {
try {
await fs.ensureDir(this.indexPath);
const indexData = {
metadata: {
version: '1.0.0',
created: new Date().toISOString(),
projectPath: this.projectPath,
totalDocuments: this.documents.length,
supportedExtensions: this.supportedExtensions
},
documents: this.documents,
embeddings: this.embeddings
};
const indexFile = path.join(this.indexPath, 'index.json');
await fs.writeJson(indexFile, indexData, { spaces: 2 });
// Índice salvo
} catch (error) {
logger.error('Erro ao salvar índice:', error);
throw error;
}
}
/**
* Executa o processo completo de indexação
*/
async run() {
try {
// Iniciando indexação
// Inicializar embedder
await this.initializeEmbedder();
// Coletar arquivos
const files = await this.collectFiles();
if (files.length === 0) {
// Nenhum arquivo encontrado
return;
}
for (let i = 0; i < files.length; i++) {
const file = files[i];
// Processando arquivo
const document = await this.processFile(file);
if (document) {
this.documents.push(document);
// Gerar embedding
const embedding = await this.generateEmbedding(document.summary);
this.embeddings.push(embedding);
}
}
// Salvar índice
await this.saveIndex();
// Indexação concluída
} catch (error) {
logger.error('Erro durante a indexação:', error);
throw error;
}
}
}
// Executar se chamado diretamente
if (require.main === module) {
const projectPath = process.argv[2] || '.';
const indexer = new CodebaseIndexer(projectPath);
indexer.run().catch(error => {
console.error('Erro fatal:', error);
process.exit(1);
});
}
module.exports = CodebaseIndexer;