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.
566 lines • 24.7 kB
JavaScript
#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { CallToolRequestSchema, ErrorCode, ListResourcesRequestSchema, ListToolsRequestSchema, McpError, ReadResourceRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
import { ProjectIndexer } from './core/indexer.js';
import { CacheManager } from './cache/cache-manager.js';
import { writeFileSync, appendFileSync } from 'fs';
import { join, resolve } from 'path';
// Parse command line arguments - only for logging flag
const args = process.argv.slice(2);
const logEnabled = args.includes('--log');
// Determine log file path - debug.log in the directory where MCP server is located
const serverDir = resolve(process.argv[1], '..');
const logFilePath = join(serverDir, 'debug.log');
// Enhanced logger with file logging support
export const logger = {
info: (message) => {
const logMessage = `[INFO] ${new Date().toISOString()} - ${message}`;
if (logEnabled) {
appendFileSync(logFilePath, logMessage + '\n', 'utf8');
}
},
error: (message) => {
const logMessage = `[ERROR] ${new Date().toISOString()} - ${message}`;
console.error(logMessage);
if (logEnabled) {
appendFileSync(logFilePath, logMessage + '\n', 'utf8');
}
},
debug: (message) => {
const logMessage = `[DEBUG] ${new Date().toISOString()} - ${message}`;
if (logEnabled) {
appendFileSync(logFilePath, logMessage + '\n', 'utf8');
}
},
operation: (operation, details) => {
const logMessage = `[OPERATION] ${new Date().toISOString()} - ${operation}${details ? ` - ${details}` : ''}`;
if (logEnabled) {
appendFileSync(logFilePath, logMessage + '\n', 'utf8');
}
}
};
// Export logger enabled status
export const isLogEnabled = () => logEnabled;
// Initialize log file
if (logEnabled) {
writeFileSync(logFilePath, `=== TypeScript Project Indexer MCP Log Started ===\n${new Date().toISOString()}\n\n`, 'utf8');
logger.info(`Log system active - Debug log file: ${logFilePath}`);
logger.info(`Root directory: ${process.cwd()}`);
logger.info(`Startup arguments: ${JSON.stringify(process.argv)}`);
}
logger.info(`TypeScript Project Indexer MCP - No default project path (argument-based usage removed)`);
logger.info(`Current working directory: ${process.cwd()}`);
// Global instance to persist data across tool calls
let globalIndexer = null;
let globalCache = null;
let isInitialized = false;
async function getOrCreateIndexer() {
if (!globalIndexer) {
globalCache = new CacheManager();
await globalCache.init();
globalIndexer = new ProjectIndexer(globalCache);
await globalIndexer.initialize();
isInitialized = true;
}
return globalIndexer;
}
class ProjectIndexerMCPServer {
server;
constructor() {
this.server = new Server({
name: 'mcp-project-indexer',
version: '1.0.0',
}, {
capabilities: {
tools: {},
resources: {},
},
});
this.setupHandlers();
}
setupHandlers() {
// List available tools
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: 'analyze_project',
description: 'Analyze and index the entire project structure',
inputSchema: {
type: 'object',
properties: {
projectPath: {
type: 'string',
description: 'Path to the project root directory. Must be an absolute path (e.g., "/home/user/project" or "C:\\Users\\user\\project")',
},
includePatterns: {
type: 'array',
items: { type: 'string' },
description: 'File patterns to include (glob patterns)',
default: ['**/*.ts', '**/*.js', '**/*.json'],
},
excludePatterns: {
type: 'array',
items: { type: 'string' },
description: 'File patterns to exclude (glob patterns)',
default: [
'node_modules/**',
'dist/**',
'**/*.d.ts',
'.git/**',
'plugged/**',
'.local/**',
'cache/**',
'tmp/**',
'temp/**',
'build/**',
'coverage/**',
'test/**',
'tests/**',
'__tests__/**',
'.pytest_cache/**',
'.vscode/**',
'.idea/**'
],
},
forceReindex: {
type: 'boolean',
description: 'Force full reindexing even if cache exists',
default: false,
},
},
required: ['projectPath'],
},
},
{
name: 'search_methods',
description: 'Search for methods, functions, and classes in the project',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'Search query (method/function/class name or pattern)',
},
type: {
type: 'string',
enum: ['method', 'function', 'class', 'interface', 'type', 'all'],
description: 'Type of definition to search for',
default: 'all',
},
includeUsages: {
type: 'boolean',
description: 'Include usage locations in the results',
default: false,
},
},
required: ['query'],
},
},
{
name: 'get_project_files',
description: 'Get list of all indexed project files',
inputSchema: {
type: 'object',
properties: {},
required: [],
},
},
{
name: 'get_project_stats',
description: 'Get project statistics',
inputSchema: {
type: 'object',
properties: {},
required: [],
},
},
{
name: 'find_usages',
description: 'Find where a file, method, or class is imported or used',
inputSchema: {
type: 'object',
properties: {
filePath: {
type: 'string',
description: 'Path to the file to find imports/usages for (e.g., "src/config.ts")',
},
methodName: {
type: 'string',
description: 'Name of the method/function to find usages for',
},
className: {
type: 'string',
description: 'Name of the class to find usages for',
},
searchType: {
type: 'string',
enum: ['imports', 'usages', 'both'],
description: 'Type of search: imports (where file is imported), usages (where method/class is used), or both',
default: 'imports',
},
includeDetails: {
type: 'boolean',
description: 'Include detailed context information',
default: true,
},
},
required: [],
},
},
{
name: 'debug_dependencies',
description: 'Debug dependencies to see what is being indexed',
inputSchema: {
type: 'object',
properties: {
limit: {
type: 'number',
description: 'Maximum number of dependencies to show',
default: 20,
},
filterBy: {
type: 'string',
description: 'Filter dependencies by string match',
},
},
required: [],
},
},
],
};
});
// Handle tool calls
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
const indexer = await getOrCreateIndexer();
switch (name) {
case 'analyze_project':
return await this.analyzeProject(indexer, args || {});
case 'search_methods':
return await this.searchMethods(indexer, args);
case 'get_project_files':
return await this.getProjectFiles(indexer);
case 'get_project_stats':
return await this.getProjectStats(indexer);
case 'find_usages':
return await this.findUsages(indexer, args);
case 'debug_dependencies':
return await this.debugDependencies(indexer, args);
default:
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
}
}
catch (error) {
const message = error instanceof Error ? error.message : String(error);
throw new McpError(ErrorCode.InternalError, `Tool execution failed: ${message}`);
}
});
// List available resources
this.server.setRequestHandler(ListResourcesRequestSchema, async () => {
return {
resources: [
{
uri: 'project://files',
name: 'Project Files',
description: 'List of all indexed project files with metadata',
mimeType: 'application/json',
},
{
uri: 'project://methods',
name: 'Project Methods',
description: 'Index of all methods, functions, and classes',
mimeType: 'application/json',
},
],
};
});
// Handle resource reads
this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const { uri } = request.params;
try {
const indexer = await getOrCreateIndexer();
switch (uri) {
case 'project://files':
return await this.getProjectFilesResource(indexer);
case 'project://methods':
return await this.getProjectMethodsResource(indexer);
default:
throw new McpError(ErrorCode.InvalidRequest, `Unknown resource: ${uri}`);
}
}
catch (error) {
const message = error instanceof Error ? error.message : String(error);
throw new McpError(ErrorCode.InternalError, `Resource read failed: ${message}`);
}
});
}
async analyzeProject(indexer, args) {
const { projectPath, includePatterns = ['**/*.ts', '**/*.js', '**/*.json'], excludePatterns = ['node_modules/**', 'dist/**', '**/*.d.ts'], forceReindex = false, } = args;
logger.operation('MCP Tool: analyze_project called', `Path: ${projectPath}, ForceReindex: ${forceReindex}`);
logger.info(`🔍 Starting project analysis with path: ${projectPath}`);
logger.info(`💼 Process working directory: ${process.cwd()}`);
logger.debug(`Include patterns: ${includePatterns.join(', ')}`);
logger.debug(`Exclude patterns: ${excludePatterns.join(', ')}`);
const toolStartTime = Date.now();
const result = await indexer.analyzeProject({
projectPath,
includePatterns,
excludePatterns,
forceReindex,
});
const toolDuration = Date.now() - toolStartTime;
logger.operation('MCP Tool: analyze_project completed', `Total duration: ${toolDuration}ms, IndexingTime: ${result.duration}ms, Results: ${result.totalFiles} files, ${result.totalMethods} methods, ${result.totalPaths} paths, ${result.totalDependencies} dependencies`);
return {
content: [
{
type: 'text',
text: JSON.stringify({
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.`,
}, null, 2),
},
],
};
}
async searchMethods(indexer, args) {
const { query, type = 'all', includeUsages = false } = args;
if (!query) {
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: false,
error: 'Query parameter is required',
message: 'Missing required parameter',
}, null, 2),
},
],
};
}
const result = await indexer.searchMethods(query, type, includeUsages);
return {
content: [
{
type: 'text',
text: JSON.stringify({
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`,
}, null, 2),
},
],
};
}
async getProjectFiles(indexer) {
const files = indexer.getAllFiles();
const stats = indexer.getProjectStats();
return {
content: [
{
type: 'text',
text: JSON.stringify({
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`,
}, null, 2),
},
],
};
}
async getProjectStats(indexer) {
const stats = indexer.getProjectStats();
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: true,
result: stats,
message: 'Project statistics retrieved',
}, null, 2),
},
],
};
}
async findUsages(indexer, args) {
const { filePath, methodName, className, searchType = 'imports', includeDetails = true } = args;
if (!filePath && !methodName && !className) {
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: false,
error: 'Either filePath, methodName, or className is required',
message: 'Missing required parameter',
}, null, 2),
},
],
};
}
const startTime = Date.now();
const result = await indexer.findUsages({
filePath,
methodName,
className,
searchType,
includeDetails,
});
const searchTime = Date.now() - startTime;
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: true,
result: {
searchType,
query: filePath || methodName || className,
totalCount: result.length,
searchTime,
usages: result.slice(0, 100), // Limit to 100 results
},
message: `Found ${result.length} usages for "${filePath || methodName || className}" in ${searchTime}ms`,
}, null, 2),
},
],
};
}
async debugDependencies(indexer, args) {
const { limit = 20, filterBy } = args;
const allDependencies = indexer.getAllDependencies();
let filteredDeps = allDependencies;
if (filterBy) {
filteredDeps = allDependencies.filter(dep => dep.to.includes(filterBy) ||
dep.from.includes(filterBy) ||
(dep.resolvedTo && dep.resolvedTo.includes(filterBy)));
}
const limitedDeps = filteredDeps.slice(0, limit);
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: true,
result: {
totalDependencies: allDependencies.length,
filteredCount: filteredDeps.length,
showing: limitedDeps.length,
dependencies: limitedDeps.map(dep => ({
from: dep.from,
to: dep.to,
resolvedTo: dep.resolvedTo,
type: dep.type,
line: dep.line,
file: dep.file
}))
},
message: `Showing ${limitedDeps.length} dependencies ${filterBy ? `filtered by "${filterBy}"` : ''}`,
}, null, 2),
},
],
};
}
async getProjectFilesResource(indexer) {
const files = indexer.getAllFiles();
const stats = indexer.getProjectStats();
return {
contents: [
{
uri: 'project://files',
mimeType: 'application/json',
text: JSON.stringify({
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,
})),
}, null, 2),
},
],
};
}
async getProjectMethodsResource(indexer) {
const methods = indexer.getAllMethods();
const stats = indexer.getProjectStats();
const methodsByType = methods.reduce((acc, method) => {
if (!acc[method.type])
acc[method.type] = [];
acc[method.type].push(method);
return acc;
}, {});
return {
contents: [
{
uri: 'project://methods',
mimeType: 'application/json',
text: JSON.stringify({
summary: {
totalMethods: methods.length,
methodsByType: Object.fromEntries(Object.entries(methodsByType).map(([type, items]) => [type, items.length])),
lastIndexed: stats.lastIndexed,
},
methodsByType,
allMethods: methods,
}, null, 2),
},
],
};
}
async start() {
const transport = new StdioServerTransport();
await this.server.connect(transport);
await getOrCreateIndexer(); // Initialize global indexer
logger.info('🚀 TypeScript Project Indexer MCP server started!');
}
async stop() {
await this.server.close();
}
}
// Start the server
const server = new ProjectIndexerMCPServer();
process.on('SIGINT', async () => {
logger.info('🛑 Shutting down TypeScript Project Indexer MCP server...');
await server.stop();
process.exit(0);
});
process.on('SIGTERM', async () => {
logger.info('🛑 Shutting down TypeScript Project Indexer MCP server...');
await server.stop();
process.exit(0);
});
server.start().catch((error) => {
logger.error(`❌ Failed to start TypeScript Project Indexer MCP server: ${error}`);
process.exit(1);
});
//# sourceMappingURL=mcp-server.js.map