UNPKG

ts-project-indexer-mcp

Version:

A powerful MCP (Model Context Protocol) server that indexes and analyzes TypeScript/JavaScript projects, providing intelligent code exploration, dependency tracking, method discovery, and project structure analysis for AI assistants and development tools.

293 lines 11.4 kB
#!/usr/bin/env node import { ProjectIndexer } from './core/indexer.js'; import { CacheManager } from './cache/cache-manager.js'; import { logger } from './mcp-server.js'; class SimpleProjectIndexer { indexer; cache; constructor() { this.cache = new CacheManager(); this.indexer = new ProjectIndexer(this.cache); } async initialize() { await this.indexer.initialize(); } async handleRequest(request) { try { switch (request.tool) { case 'analyze_project': return await this.analyzeProject(request.args); case 'search_methods': return await this.searchMethods(request.args); case 'find_dependencies': return await this.findDependencies(request.args); case 'generate_graph': return await this.generateGraph(request.args); case 'get_files': return await this.getFiles(); case 'get_methods': return await this.getMethods(); case 'get_paths': return await this.getPaths(); case 'get_stats': return await this.getStats(); default: return { success: false, error: `Unknown tool: ${request.tool}`, message: 'Tool not found', }; } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { success: false, error: errorMessage, message: 'Tool execution failed', }; } } async analyzeProject(args) { const { projectPath = '../', includePatterns = ['**/*.ts', '**/*.js', '**/*.json'], excludePatterns = ['node_modules/**', 'dist/**', '**/*.d.ts'], forceReindex = false, } = args; const result = await this.indexer.analyzeProject({ projectPath, includePatterns, excludePatterns, forceReindex, }); return { success: true, result, message: `Project analysis complete! Found ${result.totalFiles} files, ${result.totalMethods} methods, ${result.totalPaths} paths, and ${result.totalDependencies} dependencies in ${result.duration}ms.`, }; } async searchMethods(args) { const { query, type = 'all', includeUsages = false } = args; if (!query) { return { success: false, error: 'Query parameter is required', message: 'Missing required parameter', }; } const result = await this.indexer.searchMethods(query, type, includeUsages); return { success: true, result: { query: result.query, totalCount: result.totalCount, searchTime: result.searchTime, items: result.items.slice(0, 50), }, message: `Found ${result.totalCount} matching items for query "${query}" in ${result.searchTime}ms`, }; } async findDependencies(args) { const { entityName, direction = 'both', depth = 2 } = args; if (!entityName) { return { success: false, error: 'entityName parameter is required', message: 'Missing required parameter', }; } const result = await this.indexer.findDependencies(entityName, direction, depth); const graphObj = {}; for (const [key, value] of result.graph) { graphObj[key] = value; } return { success: true, result: { entity: result.entity, incoming: result.incoming, outgoing: result.outgoing, graph: graphObj, summary: { incomingCount: result.incoming.length, outgoingCount: result.outgoing.length, totalNodes: result.graph.size, }, }, message: `Found ${result.incoming.length} incoming and ${result.outgoing.length} outgoing dependencies for "${entityName}"`, }; } async generateGraph(args) { const { format = 'json', scope = 'services', maxNodes = 50 } = args; const allMethods = this.indexer.getAllMethods(); const allDependencies = this.indexer.getAllDependencies(); const filteredMethods = allMethods.filter(method => { switch (scope) { case 'services': return method.file.includes('/services/') || method.file.includes('\\services\\'); case 'controllers': return method.file.includes('/controllers/') || method.file.includes('\\controllers\\') || method.file.includes('/routes/') || method.file.includes('\\routes\\'); case 'types': return method.type === 'interface' || method.type === 'type' || method.type === 'enum'; case 'full': default: return true; } }).slice(0, maxNodes); let graphContent = JSON.stringify({ nodes: filteredMethods.map(m => ({ id: m.name, type: m.type, file: m.file })), edges: allDependencies.map(d => ({ from: d.from, to: d.to, type: d.type })), }, null, 2); if (format === 'mermaid') { const lines = ['graph TD']; for (const method of filteredMethods) { const nodeId = method.name.replace(/[^a-zA-Z0-9_]/g, '_'); lines.push(` ${nodeId}["${method.name}[${method.type}]"]`); } graphContent = lines.join('\n'); } return { success: true, result: { format, scope, nodeCount: filteredMethods.length, edgeCount: allDependencies.length, graph: graphContent, }, message: `Generated ${format} graph with ${filteredMethods.length} nodes and ${allDependencies.length} edges`, }; } async getFiles() { const files = this.indexer.getAllFiles(); const stats = this.indexer.getProjectStats(); return { success: true, result: { summary: { totalFiles: files.length, lastIndexed: stats.lastIndexed, }, files: files.map(file => ({ path: file.path, relativePath: file.relativePath, name: file.name, extension: file.extension, size: file.size, lastModified: file.lastModified, })), }, message: `Retrieved ${files.length} files`, }; } async getMethods() { const methods = this.indexer.getAllMethods(); const stats = this.indexer.getProjectStats(); const methodsByType = methods.reduce((acc, method) => { if (!acc[method.type]) acc[method.type] = []; acc[method.type].push(method); return acc; }, {}); return { success: true, result: { summary: { totalMethods: methods.length, methodsByType: Object.fromEntries(Object.entries(methodsByType).map(([type, items]) => [type, items.length])), lastIndexed: stats.lastIndexed, }, methodsByType, allMethods: methods, }, message: `Retrieved ${methods.length} methods`, }; } async getPaths() { const paths = this.indexer.getAllPaths(); const stats = this.indexer.getProjectStats(); const pathsByMethod = paths.reduce((acc, path) => { if (!acc[path.method]) acc[path.method] = []; acc[path.method].push(path); return acc; }, {}); return { success: true, result: { summary: { totalPaths: paths.length, pathsByMethod: Object.fromEntries(Object.entries(pathsByMethod).map(([method, items]) => [method, items.length])), lastIndexed: stats.lastIndexed, }, pathsByMethod, allPaths: paths, }, message: `Retrieved ${paths.length} API paths`, }; } async getStats() { const stats = this.indexer.getProjectStats(); return { success: true, result: stats, message: 'Project statistics retrieved', }; } } // CLI interface async function main() { const indexer = new SimpleProjectIndexer(); await indexer.initialize(); logger.info('🚀 Simple Project Indexer ready!'); logger.info('Available tools: analyze_project, search_methods, find_dependencies, generate_graph, get_files, get_methods, get_paths, get_stats'); // Handle command line arguments if (process.argv.length > 2) { const tool = process.argv[2]; const argsString = process.argv[3] || '{}'; try { const args = JSON.parse(argsString); const result = await indexer.handleRequest({ tool, args }); console.log(JSON.stringify(result, null, 2)); } catch (error) { logger.error(`Error: ${error}`); process.exit(1); } } else { logger.info('Usage: node simple-server.js <tool> <args>'); logger.info('Example: node simple-server.js analyze_project \'{"projectPath": "../"}\''); } } // Handle stdin for MCP-like communication if (process.stdin.isTTY) { main().catch((error) => logger.error(`Main execution error: ${error}`)); } else { const indexer = new SimpleProjectIndexer(); indexer.initialize().then(() => { logger.info('🚀 Simple Project Indexer ready for stdin input!'); process.stdin.setEncoding('utf8'); let buffer = ''; process.stdin.on('data', async (chunk) => { buffer += chunk; const lines = buffer.split('\n'); buffer = lines.pop() || ''; for (const line of lines) { if (line.trim()) { try { const request = JSON.parse(line); const result = await indexer.handleRequest(request); console.log(JSON.stringify(result)); } catch (error) { console.log(JSON.stringify({ success: false, error: error instanceof Error ? error.message : String(error), message: 'Invalid request format', })); } } } }); }); } //# sourceMappingURL=simple-server.js.map