UNPKG

mcp-codebase-search

Version:

MCP server for semantic codebase search using embeddings

348 lines (299 loc) 9.91 kB
#!/usr/bin/env node /** * 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;