#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
import path from 'path';
import { GitignoreHandler } from './utils/gitignoreHandler.js';
import { FileScanner } from './utils/fileScanner.js';
import { SymbolExtractor } from './parsers/symbolExtractor.js';
import { MarkdownGenerator, FileSymbols } from './formatters/markdownGenerator.js';
import { FullCodeGenerator } from './formatters/fullCodeGenerator.js';
/**
* MCP Server that analyzes codebases and extracts symbols
*/
class CodebaseSymbolsServer {
private server: Server;
private gitignoreHandler: GitignoreHandler;
private symbolExtractor: SymbolExtractor;
private markdownGenerator: MarkdownGenerator;
private fullCodeGenerator: FullCodeGenerator;
constructor() {
this.server = new Server(
{
name: 'codebase-symbols',
version: '1.0.0',
},
{
capabilities: {
tools: {},
},
}
);
this.gitignoreHandler = new GitignoreHandler();
this.symbolExtractor = new SymbolExtractor();
this.markdownGenerator = new MarkdownGenerator();
this.fullCodeGenerator = new FullCodeGenerator();
this.setupHandlers();
}
private setupHandlers(): void {
// List available tools
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: 'analyze_codebase',
description:
'Analyzes a codebase and returns a comprehensive LLM-optimized markdown with all symbols (functions, classes, methods, interfaces, types, etc.) and their file paths. ' +
'Respects .gitignore rules. Perfect for giving LLMs complete understanding of code structure in a single request.',
inputSchema: {
type: 'object',
properties: {
path: {
type: 'string',
description: 'Absolute path to the codebase directory to analyze',
},
},
required: ['path'],
},
},
{
name: 'read_codebase',
description:
'Reads the full content of all code files in a directory respecting .gitignore rules and returns in a structured format. ' +
'WARNING: This tool can generate very large responses, use on smaller directories or specific subdirectories.',
inputSchema: {
type: 'object',
properties: {
path: {
type: 'string',
description: 'Absolute path to the codebase directory to read',
},
},
required: ['path'],
},
},
],
}));
// Handle tool calls
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name === 'analyze_codebase') {
return await this.handleAnalyzeCodebase(request.params.arguments);
}
if (request.params.name === 'read_codebase') {
return await this.handleReadCodebase(request.params.arguments);
}
throw new Error(`Unknown tool: ${request.params.name}`);
});
}
private async handleAnalyzeCodebase(args: any): Promise<any> {
const basePath = args.path as string;
if (!basePath) {
throw new Error('path parameter is required');
}
try {
// Load .gitignore
await this.gitignoreHandler.load(basePath);
// Scan for files
const fileScanner = new FileScanner(this.gitignoreHandler);
const files = await fileScanner.scan(basePath);
console.error(`Found ${files.length} code files`);
// Extract symbols from each file
const fileSymbols: FileSymbols[] = [];
for (const file of files) {
const symbols = await this.symbolExtractor.extractFromFile(
file.absolutePath,
file.extension
);
fileSymbols.push({
relativePath: file.relativePath,
symbols,
});
}
// Generate markdown
const markdown = this.markdownGenerator.generate(fileSymbols, basePath);
return {
content: [
{
type: 'text',
text: markdown,
},
],
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to analyze codebase: ${errorMessage}`);
}
}
async run(): Promise<void> {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error('Codebase Symbols MCP Server running on stdio');
}
private async handleReadCodebase(args: any): Promise<any> {
const basePath = args.path as string;
if (!basePath) {
throw new Error('path parameter is required');
}
try {
// Load .gitignore
await this.gitignoreHandler.load(basePath);
// Scan for files
const fileScanner = new FileScanner(this.gitignoreHandler);
const files = await fileScanner.scan(basePath);
console.error(`Found ${files.length} code files`);
// Generate full code content with line numbers
const content = await this.fullCodeGenerator.generate(files, basePath);
return {
content: [
{
type: 'text',
text: content,
},
],
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to read codebase: ${errorMessage}`);
}
}
}
// Start server
const server = new CodebaseSymbolsServer();
server.run().catch((error) => {
console.error('Server error:', error);
process.exit(1);
});