UNPKG

@aaswe/codebase-ai

Version:

AI-Assisted Software Engineering (AASWE) - Rich codebase context for IDE LLMs

1,144 lines 43.3 kB
"use strict"; /** * MCP Server Implementation * * Model Context Protocol server that provides rich codebase context * to IDE LLMs using TTL files and knowledge graphs. */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.MCPServer = void 0; const events_1 = require("events"); const http_1 = require("http"); const ws_1 = require("ws"); const chokidar_1 = require("chokidar"); const glob_1 = require("glob"); const promises_1 = require("fs/promises"); const path_1 = require("path"); const crypto_1 = require("crypto"); const uuid_1 = require("uuid"); const logger_1 = __importDefault(require("../../utils/logger")); const types_1 = require("./types"); /** * MCP Server for IDE Integration */ class MCPServer extends events_1.EventEmitter { config; server; wsServer; layer3Service; // private _hybridStorage: HybridStorageManager; clients = new Map(); connections = new Map(); ttlFiles = new Map(); contextCache = new Map(); fileWatcher = null; status = 'stopped'; startTime = 0; metrics; cacheCleanupInterval; constructor(config, layer3Service, hybridStorage) { super(); this.config = config; this.layer3Service = layer3Service; // this._hybridStorage = hybridStorage; // Store for future use when needed hybridStorage; this.metrics = this.initializeMetrics(); // Create HTTP server with health endpoint this.server = (0, http_1.createServer)((req, res) => { if (req.url === '/health' && req.method === 'GET') { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ status: 'healthy', uptime: this.status === 'running' ? Date.now() - this.startTime : 0, connections: this.connections.size, ttlFiles: this.ttlFiles.size, timestamp: new Date().toISOString() })); } else { res.writeHead(404, { 'Content-Type': 'text/plain' }); res.end('Not Found'); } }); // Create WebSocket server this.wsServer = new ws_1.WebSocketServer({ server: this.server, maxPayload: 1024 * 1024 // 1MB max payload }); this.setupWebSocketHandlers(); logger_1.default.info('MCPServer initialized', { name: config.server.name, version: config.server.version, port: config.server.port }); } /** * Initialize metrics */ initializeMetrics() { return { server: { uptime: 0, connections: 0, totalRequests: 0, requestsPerSecond: 0, averageResponseTime: 0, errorRate: 0 }, context: { totalContextRequests: 0, averageContextSize: 0, cacheHitRate: 0, averageRelevanceScore: 0, ttlFilesWatched: 0, ttlFilesLoaded: 0 }, performance: { memoryUsage: 0, cpuUsage: 0, diskUsage: 0, networkBytesIn: 0, networkBytesOut: 0 } }; } /** * Setup WebSocket handlers */ setupWebSocketHandlers() { this.wsServer.on('connection', (ws, request) => { const clientId = (0, uuid_1.v4)(); const clientInfo = { id: clientId, name: 'Unknown', version: '1.0.0', capabilities: {}, connected: new Date(), lastActivity: new Date() }; this.clients.set(clientId, clientInfo); this.connections.set(clientId, ws); this.metrics.server.connections++; logger_1.default.info('MCP client connected', { clientId, ip: request.socket.remoteAddress }); ws.on('message', async (data) => { try { const message = JSON.parse(data.toString()); await this.handleMessage(clientId, message); // Update client activity const client = this.clients.get(clientId); if (client) { client.lastActivity = new Date(); } this.metrics.server.totalRequests++; } catch (error) { logger_1.default.error('Failed to handle MCP message', { clientId, error }); this.sendError(clientId, -32700, 'Parse error', { error: String(error) }); } }); ws.on('close', () => { this.clients.delete(clientId); this.connections.delete(clientId); this.metrics.server.connections--; logger_1.default.info('MCP client disconnected', { clientId }); }); ws.on('error', (error) => { logger_1.default.error('MCP WebSocket error', { clientId, error }); }); // Send initialization this.sendNotification(clientId, 'initialized', { protocolVersion: '2024-11-05', capabilities: { tools: { listChanged: true }, resources: { subscribe: true, listChanged: true }, prompts: { listChanged: true }, logging: {} }, serverInfo: { name: this.config.server.name, version: this.config.server.version } }); }); } /** * Handle incoming MCP message */ async handleMessage(clientId, message) { const startTime = Date.now(); try { let result; switch (message.method) { case 'initialize': result = await this.handleInitialize(clientId, message.params); break; case 'tools/list': result = await this.handleToolsList(); break; case 'tools/call': result = await this.handleToolCall(message.params); break; case 'resources/list': result = await this.handleResourcesList(); break; case 'resources/read': result = await this.handleResourceRead(message.params); break; case 'context/request': result = await this.handleContextRequest(message.params); break; case 'ping': result = { pong: true }; break; default: throw new types_1.MCPServerError('METHOD_NOT_FOUND', `Method not found: ${message.method}`); } this.sendResponse(clientId, message.id, result); // Update metrics const responseTime = Date.now() - startTime; this.updateResponseTimeMetrics(responseTime); } catch (error) { logger_1.default.error('MCP method error', { method: message.method, error }); if (error instanceof types_1.MCPServerError) { this.sendError(clientId, this.getErrorCode(error.code), error.message, error.data); } else { this.sendError(clientId, -32603, 'Internal error', { error: String(error) }); } } } /** * Handle initialize request */ async handleInitialize(clientId, params) { const client = this.clients.get(clientId); if (client) { client.name = params.clientInfo?.name || 'Unknown'; client.version = params.clientInfo?.version || '1.0.0'; client.capabilities = params.capabilities || {}; } return { protocolVersion: '2024-11-05', capabilities: { tools: { listChanged: true }, resources: { subscribe: true, listChanged: true }, prompts: { listChanged: true }, logging: {} }, serverInfo: { name: this.config.server.name, version: this.config.server.version } }; } /** * Handle tools list request */ async handleToolsList() { const tools = [ { name: 'get_context', description: 'Get rich codebase context for a specific file and cursor position', inputSchema: { type: 'object', properties: { filePath: { type: 'string', description: 'Path to the current file' }, cursorPosition: { type: 'object', properties: { line: { type: 'number' }, column: { type: 'number' } }, required: ['line', 'column'] }, selection: { type: 'object', properties: { start: { type: 'object', properties: { line: { type: 'number' }, column: { type: 'number' } } }, end: { type: 'object', properties: { line: { type: 'number' }, column: { type: 'number' } } }, text: { type: 'string' } } }, query: { type: 'string', description: 'Optional query or question' }, intent: { type: 'string', enum: ['explanation', 'completion', 'refactoring', 'debugging', 'documentation'], description: 'Intent of the context request' }, maxTokens: { type: 'number', description: 'Maximum tokens in response' }, includeRelated: { type: 'boolean', description: 'Include related files and context' } }, required: ['filePath', 'cursorPosition'] } }, { name: 'query_knowledge', description: 'Query the knowledge graph using natural language', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Natural language query' }, type: { type: 'string', enum: ['rag', 'cypher', 'sparql', 'auto'], description: 'Query type (auto-detected if not specified)' }, maxResults: { type: 'number', description: 'Maximum number of results' } }, required: ['query'] } }, { name: 'analyze_code', description: 'Analyze code structure and relationships', inputSchema: { type: 'object', properties: { filePath: { type: 'string', description: 'Path to file to analyze' }, includeMetrics: { type: 'boolean', description: 'Include complexity metrics' }, includeDependencies: { type: 'boolean', description: 'Include dependency analysis' } }, required: ['filePath'] } } ]; return { tools }; } /** * Handle tool call */ async handleToolCall(params) { switch (params.name) { case 'get_context': return await this.handleGetContext(params.arguments); case 'query_knowledge': return await this.handleQueryKnowledge(params.arguments); case 'analyze_code': return await this.handleAnalyzeCode(params.arguments); default: throw new types_1.MCPServerError('METHOD_NOT_FOUND', `Tool not found: ${params.name}`); } } /** * Handle get context tool call */ async handleGetContext(args) { try { const contextRequest = { filePath: args.filePath, cursorPosition: args.cursorPosition, selection: args.selection, query: args.query, intent: args.intent, maxTokens: args.maxTokens || this.config.context.maxTokens, includeRelated: args.includeRelated !== false }; const context = await this.generateContext(contextRequest); return { content: [{ type: 'text', text: JSON.stringify(context, null, 2) }] }; } catch (error) { logger_1.default.error('Get context failed', { error }); return { content: [{ type: 'text', text: `Error generating context: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } /** * Handle query knowledge tool call */ async handleQueryKnowledge(args) { try { const response = await this.layer3Service.query({ query: args.query, type: args.type || 'auto' }); return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] }; } catch (error) { logger_1.default.error('Query knowledge failed', { error }); return { content: [{ type: 'text', text: `Error querying knowledge: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } /** * Handle analyze code tool call */ async handleAnalyzeCode(args) { try { // This would integrate with Layer 1 services for code analysis const analysis = { filePath: args.filePath, structure: 'Code structure analysis would go here', metrics: args.includeMetrics ? 'Complexity metrics would go here' : undefined, dependencies: args.includeDependencies ? 'Dependency analysis would go here' : undefined }; return { content: [{ type: 'text', text: JSON.stringify(analysis, null, 2) }] }; } catch (error) { logger_1.default.error('Analyze code failed', { error }); return { content: [{ type: 'text', text: `Error analyzing code: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } /** * Handle resources list request */ async handleResourcesList() { const resources = []; // Add TTL files as resources for (const [path, ttlFile] of this.ttlFiles) { resources.push({ uri: `ttl://${path}`, name: `TTL: ${(0, path_1.relative)(process.cwd(), path)}`, description: `Module knowledge file for ${ttlFile.metadata.module}`, mimeType: 'text/turtle' }); } return { resources }; } /** * Handle resource read request */ async handleResourceRead(params) { if (params.uri.startsWith('ttl://')) { const path = params.uri.replace('ttl://', ''); const ttlFile = this.ttlFiles.get(path); if (!ttlFile) { throw new types_1.MCPServerError('FILE_NOT_FOUND', `TTL file not found: ${path}`); } return { uri: params.uri, mimeType: 'text/turtle', text: ttlFile.content }; } throw new types_1.MCPServerError('INVALID_PARAMS', `Unsupported resource URI: ${params.uri}`); } /** * Handle context request */ async handleContextRequest(params) { return await this.generateContext(params); } /** * Generate context for a request */ async generateContext(request) { const startTime = Date.now(); // Update metrics first this.metrics.context.totalContextRequests++; // Check cache first const cacheKey = this.generateCacheKey(request); const cached = this.getCachedContext(cacheKey); if (cached) { return cached; } try { // Select relevant TTL files and context const selection = await this.selectRelevantContext(request); // Load and process selected files const sources = []; let totalTokens = 0; let relevanceScore = 0; for (const filePath of selection.selectedFiles) { if (totalTokens >= request.maxTokens) break; const ttlFile = this.ttlFiles.get(filePath); if (ttlFile) { const source = { type: 'ttl', path: filePath, content: this.formatTTLForLLM(ttlFile), relevanceScore: selection.relevanceScores[filePath] || 0, metadata: { lastModified: ttlFile.lastModified, size: ttlFile.size, language: ttlFile.metadata.language, module: ttlFile.metadata.module } }; sources.push(source); totalTokens += this.estimateTokens(source.content); relevanceScore += source.relevanceScore; } } // Generate final context const context = this.buildContextString(sources, request); // Ensure processing time is > 0 const processingTime = Math.max(1, Date.now() - startTime); const response = { context, sources, metadata: { totalTokens, processingTime, relevanceScore: sources.length > 0 ? relevanceScore / sources.length : 0, cached: false }, suggestions: { relatedFiles: selection.alternatives[0]?.files || [], followUpQueries: this.generateFollowUpQueries(request, sources), improvements: this.generateImprovements(sources) } }; // Cache the response if (this.config.context.cacheEnabled) { this.cacheContext(cacheKey, response); } this.metrics.context.averageContextSize = (this.metrics.context.averageContextSize * (this.metrics.context.totalContextRequests - 1) + totalTokens) / this.metrics.context.totalContextRequests; return response; } catch (error) { logger_1.default.error('Context generation failed', { request, error }); throw new types_1.MCPServerError('CONTEXT_GENERATION_FAILED', 'Failed to generate context', { error }); } } /** * Select relevant context based on request */ async selectRelevantContext(request) { // Simple relevance scoring based on file proximity and content const relevanceScores = {}; const selectedFiles = []; const requestDir = (0, path_1.dirname)(request.filePath); for (const [filePath, ttlFile] of this.ttlFiles) { let score = 0; // File proximity scoring const fileDir = (0, path_1.dirname)(filePath); if (fileDir === requestDir) score += 0.5; else if (fileDir.startsWith(requestDir) || requestDir.startsWith(fileDir)) score += 0.3; // Language matching if (ttlFile.metadata.language && request.filePath.includes(ttlFile.metadata.language)) { score += 0.3; } // Query matching (if provided) if (request.query && ttlFile.content.toLowerCase().includes(request.query.toLowerCase())) { score += 0.4; } // Dependency matching if (ttlFile.metadata.dependencies.some(dep => request.filePath.includes(dep))) { score += 0.2; } relevanceScores[filePath] = score; if (score > this.config.context.relevanceThreshold) { selectedFiles.push(filePath); } } // Sort by relevance and limit selectedFiles.sort((a, b) => relevanceScores[b] - relevanceScores[a]); const limitedFiles = selectedFiles.slice(0, this.config.context.maxFiles); return { selectedFiles: limitedFiles, relevanceScores, reasoning: `Selected ${limitedFiles.length} files based on proximity, language, and content relevance`, alternatives: [{ files: selectedFiles.slice(this.config.context.maxFiles), score: 0.5, reason: 'Alternative files with lower relevance scores' }] }; } /** * Format TTL file content for LLM consumption */ formatTTLForLLM(ttlFile) { const lines = [ `# Module: ${ttlFile.metadata.module}`, `# Language: ${ttlFile.metadata.language}`, `# Dependencies: ${ttlFile.metadata.dependencies.join(', ')}`, `# Last Modified: ${ttlFile.lastModified.toISOString()}`, '', '# Knowledge Content:', ttlFile.content, '', '# Business Context:', ...ttlFile.metadata.businessContext.map(ctx => `# ${ctx}`) ]; return lines.join('\n'); } /** * Build context string from sources */ buildContextString(sources, request) { const lines = [ '# Codebase Context', `# Current File: ${request.filePath}`, `# Cursor Position: Line ${request.cursorPosition.line}, Column ${request.cursorPosition.column}`, '' ]; if (request.query) { lines.push(`# Query: ${request.query}`, ''); } if (request.intent) { lines.push(`# Intent: ${request.intent}`, ''); } lines.push('# Relevant Knowledge:'); for (const source of sources) { lines.push('', `## ${source.metadata.module} (${source.type.toUpperCase()})`, `Relevance: ${(source.relevanceScore * 100).toFixed(1)}%`, `Path: ${source.path}`, '', source.content); } return lines.join('\n'); } /** * Generate follow-up queries */ generateFollowUpQueries(request, _sources) { const queries = [ 'What are the main dependencies of this module?', 'How does this code relate to other parts of the system?', 'What are the key architectural patterns used here?' ]; if (request.intent === 'debugging') { queries.unshift('What are common issues in this type of code?'); } if (request.intent === 'refactoring') { queries.push('What refactoring opportunities exist here?'); } return queries.slice(0, 3); } /** * Generate improvement suggestions */ generateImprovements(sources) { const improvements = []; if (sources.length < 3) { improvements.push('Consider adding more detailed business context to TTL files'); } if (sources.some(s => s.metadata.size > 10000)) { improvements.push('Large TTL files detected - consider splitting for better performance'); } improvements.push('Enhance TTL files with more architectural insights'); return improvements; } /** * Start the MCP server */ async start() { try { this.status = 'starting'; this.startTime = Date.now(); // Initialize TTL file watching if (this.config.ttl.watchEnabled) { await this.initializeTTLWatcher(); } // Load existing TTL files await this.loadTTLFiles(); // Start cache cleanup if (this.config.context.cacheEnabled) { this.startCacheCleanup(); } // Start HTTP server await new Promise((resolve, reject) => { this.server.listen(this.config.server.port, this.config.server.host, (error) => { if (error) reject(error); else resolve(); }); }); this.status = 'running'; logger_1.default.info('MCP Server started', { host: this.config.server.host, port: this.config.server.port, ttlFiles: this.ttlFiles.size }); } catch (error) { this.status = 'error'; logger_1.default.error('Failed to start MCP Server', { error }); throw error; } } /** * Stop the MCP server */ async stop() { this.status = 'stopping'; // Close all connections for (const [_clientId, ws] of this.connections) { ws.close(); } // Stop file watcher if (this.fileWatcher) { await this.fileWatcher.close(); } // Stop cache cleanup if (this.cacheCleanupInterval) { clearInterval(this.cacheCleanupInterval); } // Close servers this.wsServer.close(); await new Promise((resolve) => { this.server.close(() => resolve()); }); this.status = 'stopped'; logger_1.default.info('MCP Server stopped'); } /** * Initialize TTL file watcher */ async initializeTTLWatcher() { const directories = this.config.ttl.directories || ['./']; const patterns = this.config.ttl.patterns || ['**/*.module-knowledge.ttl']; // Create watch patterns for all directories and patterns const watchPatterns = []; for (const directory of directories) { for (const pattern of patterns) { // Combine directory and pattern const watchPattern = directory === './' ? pattern : `${directory}/${pattern}`; watchPatterns.push(watchPattern); } } logger_1.default.info('Initializing TTL file watcher', { watchPatterns }); this.fileWatcher = (0, chokidar_1.watch)(watchPatterns, { ignored: /node_modules/, persistent: true, ignoreInitial: false }); this.fileWatcher.on('add', (path) => this.handleFileEvent('created', path)); this.fileWatcher.on('change', (path) => this.handleFileEvent('modified', path)); this.fileWatcher.on('unlink', (path) => this.handleFileEvent('deleted', path)); logger_1.default.info('TTL file watcher initialized', { patterns: watchPatterns }); } /** * Handle file system events */ async handleFileEvent(type, path) { try { switch (type) { case 'created': case 'modified': await this.loadTTLFile(path); this.clearRelatedCache(path); break; case 'deleted': this.ttlFiles.delete(path); this.clearRelatedCache(path); break; } this.emit('ttl_file_changed', { type, path }); // Notify clients this.broadcastNotification('resources/list_changed', {}); } catch (error) { logger_1.default.error('Failed to handle file event', { type, path, error }); } } /** * Load all TTL files */ async loadTTLFiles() { try { logger_1.default.info('Loading TTL files...'); const allTTLFiles = []; // Use custom directories and patterns from config const directories = this.config.ttl.directories || ['./']; const patterns = this.config.ttl.patterns || ['**/*.module-knowledge.ttl']; logger_1.default.info('Scanning TTL directories', { directories, patterns }); // Scan each directory with each pattern for (const directory of directories) { for (const pattern of patterns) { try { const ttlFiles = await (0, glob_1.glob)(pattern, { cwd: directory, ignore: ['node_modules/**', '.git/**', 'dist/**', 'build/**'], absolute: true }); allTTLFiles.push(...ttlFiles); logger_1.default.debug(`Found ${ttlFiles.length} TTL files in ${directory} with pattern ${pattern}`); } catch (error) { logger_1.default.warn(`Failed to scan directory ${directory} with pattern ${pattern}`, { error }); } } } // Remove duplicates const uniqueTTLFiles = [...new Set(allTTLFiles)]; logger_1.default.info(`Found ${uniqueTTLFiles.length} TTL files to load`); // Load each TTL file const loadPromises = uniqueTTLFiles.map(filePath => this.loadTTLFile(filePath)); await Promise.allSettled(loadPromises); this.metrics.context.ttlFilesLoaded = this.ttlFiles.size; this.metrics.context.ttlFilesWatched = this.ttlFiles.size; logger_1.default.info(`Successfully loaded ${this.ttlFiles.size} TTL files`); } catch (error) { logger_1.default.error('Failed to load TTL files', { error }); } } /** * Load a single TTL file */ async loadTTLFile(path) { try { const content = await (0, promises_1.readFile)(path, 'utf-8'); const stats = await (0, promises_1.stat)(path); const hash = (0, crypto_1.createHash)('md5').update(content).digest('hex'); const ttlFile = { path, content, lastModified: stats.mtime, size: stats.size, hash, parsed: this.parseTTLContent(content), metadata: this.extractTTLMetadata(content, path) }; this.ttlFiles.set(path, ttlFile); this.metrics.context.ttlFilesWatched = this.ttlFiles.size; logger_1.default.debug('TTL file loaded', { path, size: stats.size }); } catch (error) { logger_1.default.error('Failed to load TTL file', { path, error }); } } /** * Parse TTL content (simplified) */ parseTTLContent(_content) { // This is a simplified parser - in production, use a proper RDF library return { triples: [], prefixes: {}, classes: [], properties: [], individuals: [] }; } /** * Extract metadata from TTL content */ extractTTLMetadata(content, path) { // Extract metadata from comments and content const lines = content.split('\n'); const comments = lines.filter(line => line.trim().startsWith('#')); // Extract language from comments first, then fallback to path/content let language = this.extractLanguage(path, content); const langComment = comments.find(c => c.toLowerCase().includes('language:')); if (langComment) { const match = langComment.match(/language:\s*(\w+)/i); if (match) { language = match[1].toLowerCase(); } } // Extract dependencies from comments let dependencies = this.extractDependencies(content); const depComment = comments.find(c => c.toLowerCase().includes('dependencies:')); if (depComment) { const match = depComment.match(/dependencies:\s*(.+)/i); if (match) { dependencies = match[1].split(',').map(d => d.trim()).filter(Boolean); } } return { module: this.extractModuleName(path), language, dependencies, businessContext: comments.map(c => c.replace('#', '').trim()).filter(Boolean), architecturalPatterns: [], qualityMetrics: {}, extractedAt: new Date() }; } /** * Extract module name from path */ extractModuleName(path) { const parts = path.split('/'); return parts[parts.length - 2] || 'unknown'; } /** * Extract language from path/content */ extractLanguage(path, content) { // Check file extensions first if (path.endsWith('.java') || path.includes('/java/')) return 'java'; if (path.endsWith('.py') || path.includes('/python/')) return 'python'; if (path.endsWith('.js') || path.includes('/js/')) return 'javascript'; if (path.endsWith('.ts') || path.includes('/ts/')) return 'typescript'; // Check content for language hints const lowerContent = content.toLowerCase(); if (lowerContent.includes('typescript')) return 'typescript'; if (lowerContent.includes('javascript')) return 'javascript'; if (lowerContent.includes('python')) return 'python'; if (lowerContent.includes('java') && !lowerContent.includes('javascript')) return 'java'; return 'unknown'; } /** * Extract dependencies from content */ extractDependencies(content) { // Simple dependency extraction from TTL content const dependencies = []; const lines = content.split('\n'); for (const line of lines) { // Look for various dependency patterns if (line.includes('imports:') || line.includes('depends:') || line.includes('Dependencies:')) { // Extract quoted strings const quotedMatches = line.match(/["']([^"']+)["']/g); if (quotedMatches) { quotedMatches.forEach(match => { const dep = match.replace(/["']/g, ''); if (dep && !dependencies.includes(dep)) { dependencies.push(dep); } }); } // Extract comma-separated values after colon const colonMatch = line.match(/(?:imports|depends|Dependencies):\s*(.+)/i); if (colonMatch) { const deps = colonMatch[1].split(',').map(d => d.trim().replace(/["']/g, '')).filter(Boolean); deps.forEach(dep => { if (!dependencies.includes(dep)) { dependencies.push(dep); } }); } } } return dependencies; } /** * Send response to client */ sendResponse(clientId, id, result) { const response = { jsonrpc: '2.0', id, result }; this.sendMessage(clientId, response); } /** * Send error to client */ sendError(clientId, code, message, data) { const error = { code, message, data }; const response = { jsonrpc: '2.0', id: 0, // Error responses may not have an ID error }; this.sendMessage(clientId, response); } /** * Send notification to client */ sendNotification(clientId, method, params) { const notification = { jsonrpc: '2.0', method, params }; this.sendMessage(clientId, notification); } /** * Broadcast notification to all clients */ broadcastNotification(method, params) { for (const clientId of this.clients.keys()) { this.sendNotification(clientId, method, params); } } /** * Send message to client */ sendMessage(clientId, message) { const connection = this.connections.get(clientId); if (connection) { try { connection.send(JSON.stringify(message)); } catch (error) { logger_1.default.error('Failed to send message to client', { clientId, error }); } } } /** * Get error code for MCP error */ getErrorCode(code) { const errorCodes = { 'INVALID_REQUEST': -32600, 'METHOD_NOT_FOUND': -32601, 'INVALID_PARAMS': -32602, 'INTERNAL_ERROR': -32603, 'CONTEXT_GENERATION_FAILED': -32000, 'TTL_PARSING_FAILED': -32001, 'FILE_NOT_FOUND': -32002, 'PERMISSION_DENIED': -32003, 'RATE_LIMITED': -32004, 'SERVICE_UNAVAILABLE': -32005 }; return errorCodes[code] || -32603; } /** * Update response time metrics */ updateResponseTimeMetrics(responseTime) { const total = this.metrics.server.totalRequests; this.metrics.server.averageResponseTime = (this.metrics.server.averageResponseTime * (total - 1) + responseTime) / total; } /** * Generate cache key for context request */ generateCacheKey(request) { const key = JSON.stringify({ filePath: request.filePath, cursorPosition: request.cursorPosition, query: request.query, intent: request.intent, maxTokens: request.maxTokens }); return (0, crypto_1.createHash)('md5').update(key).digest('hex'); } /** * Get cached context */ getCachedContext(cacheKey) { const cached = this.contextCache.get(cacheKey); if (!cached) return null; const now = Date.now(); if (now - cached.timestamp > cached.ttl) { this.contextCache.delete(cacheKey); return null; } cached.hits++; cached.context.metadata.cached = true; return cached.context; } /** * Cache context response */ cacheContext(cacheKey, response) { const cached = { key: cacheKey, context: response, timestamp: Date.now(), ttl: this.config.context.cacheTtl, hits: 0 }; this.contextCache.set(cacheKey, cached); // Limit cache size if (this.contextCache.size > 1000) { const oldestKey = this.contextCache.keys().next().value; if (oldestKey) { this.contextCache.delete(oldestKey); } } } /** * Clear cache related to a file */ clearRelatedCache(filePath) { for (const [key, cached] of this.contextCache.entries()) { if (cached.context.sources.some(s => s.path === filePath)) { this.contextCache.delete(key); } } } /** * Estimate tokens in text */ estimateTokens(text) { // Simple token estimation (roughly 4 characters per token) return Math.ceil(text.length / 4); } /** * Start cache cleanup interval */ startCacheCleanup() { this.cacheCleanupInterval = setInterval(() => { const now = Date.now(); for (const [key, cached] of this.contextCache.entries()) { if (now - cached.timestamp > cached.ttl) { this.contextCache.delete(key); } } }, 60000); // Clean every minute } /** * Get server status */ getStatus() { return { status: this.status, uptime: this.status === 'running' ? Date.now() - this.startTime : 0, connections: this.connections.size, metrics: { totalRequests: this.metrics.server.totalRequests, successfulRequests: this.metrics.server.totalRequests - Math.floor(this.metrics.server.totalRequests * this.metrics.server.errorRate), failedRequests: Math.floor(this.metrics.server.totalRequests * this.metrics.server.errorRate), averageResponseTime: this.metrics.server.averageResponseTime, contextCacheHits: Array.from(this.contextCache.values()).reduce((sum, c) => sum + c.hits, 0), contextCacheMisses: this.metrics.context.totalContextRequests - Array.from(this.contextCache.values()).reduce((sum, c) => sum + c.hits, 0) } }; } /** * Get server metrics */ getMetrics() { this.metrics.server.uptime = this.status === 'running' ? Date.now() - this.startTime : 0; this.metrics.server.connections = this.connections.size; // Update cache hit rate const totalCacheRequests = this.metrics.context.totalContextRequests; const cacheHits = Array.from(this.contextCache.values()).reduce((sum, c) => sum + c.hits, 0); this.metrics.context.cacheHitRate = totalCacheRequests > 0 ? cacheHits / totalCacheRequests : 0; return { ...this.metrics }; } } exports.MCPServer = MCPServer; //# sourceMappingURL=MCPServer.js.map