UNPKG

claude-code-conversation-search-mcp

Version:

Never lose your Claude Code conversations again. Search across all projects, find old chats, and resume where you left off.

959 lines (844 loc) 32.3 kB
#!/usr/bin/env node import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ListToolsRequestSchema, Tool, } from '@modelcontextprotocol/sdk/types.js'; import { ConversationIndexer } from './indexer/indexer.js'; import { QueryParser } from './search/query.js'; import { ResultFormatter } from './search/result-formatter.js'; import { SearchOptions, SearchResult } from './types/index.js'; import { createUserFriendlyError, getErrorResponse, ConfigurationError, SearchError, IndexingError } from './utils/errors.js'; import * as path from 'path'; import * as os from 'os'; import * as fs from 'fs'; interface ServerConfig { // Database configuration dbPath?: string; dbBackupEnabled?: boolean; dbBackupInterval?: number; // Indexing configuration projectsDir?: string; indexInterval?: number; autoIndexing?: boolean; fullTextMinLength?: number; indexBatchSize?: number; // Search configuration maxResults?: number; defaultContextSize?: number; searchTimeout?: number; // Performance configuration maxMemoryMB?: number; cacheSize?: number; // Logging and monitoring debug?: boolean; logLevel?: string; logToFile?: boolean; logFilePath?: string; // Security and validation allowedFileExtensions?: string[]; maxFileSize?: number; indexThreads?: number; } export class ConversationSearchServer { private server: Server; private indexer: ConversationIndexer; private queryParser: QueryParser; private resultFormatter: ResultFormatter; private config: ServerConfig; private indexingTimer?: NodeJS.Timeout; constructor(testConfig?: Partial<ServerConfig>) { try { this.config = testConfig ? { ...this.loadConfig(), ...testConfig } : this.loadConfig(); this.setupLogging(); const projectsPath = this.resolveHome(this.config.projectsDir || '~/.claude/projects'); const dbPath = this.resolveHome(this.config.dbPath || '~/.claude/conversation-search.db'); this.indexer = new ConversationIndexer(projectsPath, dbPath); this.queryParser = new QueryParser(); this.resultFormatter = new ResultFormatter(); this.server = new Server( { name: 'claude-code-conversation-search', version: '1.0.0', }, { capabilities: { tools: {}, }, } ); this.setupHandlers(); this.log('Server initialized successfully'); } catch (error) { this.logError('Failed to initialize server', error); throw createUserFriendlyError(error, 'Failed to initialize conversation search server. Check configuration and file permissions.'); } } private loadConfig(): ServerConfig { const config: ServerConfig = { // Database configuration dbPath: process.env.CONVERSATION_DB_PATH, dbBackupEnabled: process.env.DB_BACKUP_ENABLED === 'true', dbBackupInterval: process.env.DB_BACKUP_INTERVAL ? parseInt(process.env.DB_BACKUP_INTERVAL) : 86400000, // 24 hours // Indexing configuration projectsDir: process.env.CLAUDE_PROJECTS_DIR, indexInterval: process.env.INDEX_INTERVAL ? parseInt(process.env.INDEX_INTERVAL) : 300000, // 5 minutes autoIndexing: process.env.AUTO_INDEXING !== 'false', // Default true fullTextMinLength: process.env.FULL_TEXT_MIN_LENGTH ? parseInt(process.env.FULL_TEXT_MIN_LENGTH) : 3, indexBatchSize: process.env.INDEX_BATCH_SIZE ? parseInt(process.env.INDEX_BATCH_SIZE) : 100, // Search configuration maxResults: process.env.MAX_RESULTS ? parseInt(process.env.MAX_RESULTS) : 20, defaultContextSize: process.env.DEFAULT_CONTEXT_SIZE ? parseInt(process.env.DEFAULT_CONTEXT_SIZE) : 2, searchTimeout: process.env.SEARCH_TIMEOUT ? parseInt(process.env.SEARCH_TIMEOUT) : 30000, // 30 seconds // Performance configuration maxMemoryMB: process.env.MAX_MEMORY_MB ? parseInt(process.env.MAX_MEMORY_MB) : 512, cacheSize: process.env.CACHE_SIZE ? parseInt(process.env.CACHE_SIZE) : 1000, // Logging and monitoring debug: process.env.DEBUG === 'true', logLevel: process.env.LOG_LEVEL || 'info', logToFile: process.env.LOG_TO_FILE === 'true', logFilePath: process.env.LOG_FILE_PATH, // Security and validation allowedFileExtensions: process.env.ALLOWED_FILE_EXTENSIONS ? process.env.ALLOWED_FILE_EXTENSIONS.split(',').map(ext => ext.trim()) : ['.jsonl'], maxFileSize: process.env.MAX_FILE_SIZE ? parseInt(process.env.MAX_FILE_SIZE) : 104857600, // 100MB indexThreads: process.env.INDEX_THREADS ? parseInt(process.env.INDEX_THREADS) : 1, }; this.validateConfig(config); return config; } private validateConfig(config: ServerConfig): void { // Validate numeric configurations if (config.indexInterval !== undefined && (isNaN(config.indexInterval) || config.indexInterval < 0)) { throw new ConfigurationError('INDEX_INTERVAL must be a positive number'); } if (config.maxResults !== undefined && (isNaN(config.maxResults) || config.maxResults < 1 || config.maxResults > 1000)) { throw new ConfigurationError('MAX_RESULTS must be between 1 and 1000'); } if (config.defaultContextSize !== undefined && (isNaN(config.defaultContextSize) || config.defaultContextSize < 0 || config.defaultContextSize > 50)) { throw new ConfigurationError('DEFAULT_CONTEXT_SIZE must be between 0 and 50'); } if (config.searchTimeout !== undefined && (isNaN(config.searchTimeout) || config.searchTimeout < 1000 || config.searchTimeout > 300000)) { throw new ConfigurationError('SEARCH_TIMEOUT must be between 1000ms and 300000ms (5 minutes)'); } if (config.maxMemoryMB !== undefined && (isNaN(config.maxMemoryMB) || config.maxMemoryMB < 64 || config.maxMemoryMB > 8192)) { throw new ConfigurationError('MAX_MEMORY_MB must be between 64MB and 8GB'); } if (config.indexBatchSize !== undefined && (isNaN(config.indexBatchSize) || config.indexBatchSize < 1 || config.indexBatchSize > 10000)) { throw new ConfigurationError('INDEX_BATCH_SIZE must be between 1 and 10000'); } if (config.fullTextMinLength !== undefined && (isNaN(config.fullTextMinLength) || config.fullTextMinLength < 1 || config.fullTextMinLength > 50)) { throw new ConfigurationError('FULL_TEXT_MIN_LENGTH must be between 1 and 50'); } if (config.maxFileSize !== undefined && (isNaN(config.maxFileSize) || config.maxFileSize < 1024 || config.maxFileSize > 1073741824)) { throw new ConfigurationError('MAX_FILE_SIZE must be between 1KB and 1GB'); } if (config.indexThreads !== undefined && (isNaN(config.indexThreads) || config.indexThreads < 1 || config.indexThreads > 16)) { throw new ConfigurationError('INDEX_THREADS must be between 1 and 16'); } if (config.dbBackupInterval !== undefined && (isNaN(config.dbBackupInterval) || config.dbBackupInterval < 3600000)) { throw new ConfigurationError('DB_BACKUP_INTERVAL must be at least 1 hour (3600000ms)'); } // Validate log level const validLogLevels = ['error', 'warn', 'info', 'debug']; if (config.logLevel && !validLogLevels.includes(config.logLevel)) { throw new ConfigurationError(`LOG_LEVEL must be one of: ${validLogLevels.join(', ')}`); } // Validate file extensions if (config.allowedFileExtensions) { const invalidExts = config.allowedFileExtensions.filter(ext => !ext.startsWith('.') || ext.length < 2); if (invalidExts.length > 0) { throw new ConfigurationError(`Invalid file extensions: ${invalidExts.join(', ')}. Extensions must start with '.' and be at least 2 characters`); } } } private resolveHome(filePath: string): string { if (filePath.startsWith('~')) { return path.join(os.homedir(), filePath.slice(1)); } return filePath; } private setupLogging() { if (!this.config.debug) { console.error = () => {}; } } private log(message: string) { if (this.config.debug) { console.error(`[INFO] ${new Date().toISOString()} - ${message}`); } } private logError(message: string, error?: any) { console.error(`[ERROR] ${new Date().toISOString()} - ${message}`); if (error && this.config.debug) { console.error(error); } } private async startBackgroundIndexing() { try { this.log('Starting background indexing...'); await this.indexer.indexAll((message) => { this.log(message); }); if (this.config.indexInterval && this.config.indexInterval > 0) { this.indexingTimer = setInterval(() => { this.performIncrementalIndex(); }, this.config.indexInterval); this.log(`Scheduled incremental indexing every ${this.config.indexInterval}ms`); } } catch (error) { this.logError('Error during background indexing', error); } } private async performIncrementalIndex() { try { this.log('Performing incremental index update...'); const result = await this.indexer.indexAll((message) => { this.log(message); }); this.log(`Incremental indexing complete: ${result.messagesIndexed} new messages`); } catch (error) { this.logError('Error during incremental indexing', error); } } private setupHandlers() { // List available tools this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: this.getTools(), })); // Handle tool calls this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; switch (name) { case 'search_conversations': return await this.searchConversations(args); case 'list_projects': return await this.listProjects(); case 'get_message_context': return await this.getMessageContext(args); case 'get_conversation_messages': return await this.getConversationMessages(args); case 'refresh_index': return await this.refreshIndex(); case 'get_config_info': return await this.getConfigInfo(); case 'get_server_info': return await this.getServerInfo(); case 'list_tools': return await this.listTools(); default: throw new Error(`Unknown tool: ${name}`); } }); } public getTools(): Tool[] { return [ { name: 'search_conversations', description: 'Search through Claude Code conversation history', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Search query (e.g., "where did we create auth.js", "discuss React hooks", "fix CORS error")', }, limit: { type: 'number', description: 'Maximum number of results to return (default: 10)', default: 10, }, includeContext: { type: 'boolean', description: 'Include surrounding messages for context (default: true)', default: true, }, }, required: ['query'], }, }, { name: 'list_projects', description: 'List all indexed Claude Code projects', inputSchema: { type: 'object', properties: {}, }, }, { name: 'get_message_context', description: 'Get full context around a specific message', inputSchema: { type: 'object', properties: { messageId: { type: 'string', description: 'The message ID to get context for', }, contextSize: { type: 'number', description: 'Number of messages before and after to include (default: 5)', default: 5, }, }, required: ['messageId'], }, }, { name: 'get_conversation_messages', description: 'Get messages from a specific conversation', inputSchema: { type: 'object', properties: { conversationId: { type: 'string', description: 'The conversation ID to get messages from', }, limit: { type: 'number', description: 'Number of messages to return (default: 50)', default: 50, }, startFrom: { type: 'number', description: 'Starting position: 0=first, -1=last, -10=10th from end, etc. (default: 0)', default: 0, }, }, required: ['conversationId'], }, }, { name: 'refresh_index', description: 'Manually refresh the conversation index', inputSchema: { type: 'object', properties: {}, }, }, { name: 'get_config_info', description: 'Get current configuration settings and validation status', inputSchema: { type: 'object', properties: {}, }, }, { name: 'get_server_info', description: 'Get server version, changelog, and system information', inputSchema: { type: 'object', properties: {}, }, }, { name: 'list_tools', description: 'List all available tools with their signatures and descriptions', inputSchema: { type: 'object', properties: {}, }, }, ]; } public async callTool(name: string, args: any) { switch (name) { case 'search_conversations': return await this.searchConversations(args); case 'list_projects': return await this.listProjects(); case 'get_message_context': return await this.getMessageContext(args); case 'get_conversation_messages': return await this.getConversationMessages(args); case 'refresh_index': return await this.refreshIndex(); case 'get_config_info': return await this.getConfigInfo(); case 'get_server_info': return await this.getServerInfo(); case 'list_tools': return await this.listTools(); default: throw new Error(`Unknown tool: ${name}`); } } public close() { this.shutdown(); } private async searchConversations(args: any) { try { const { query, limit = 10, includeContext = true } = args; if (!query || typeof query !== 'string') { throw new SearchError('empty query', new Error('Query parameter is required and must be a string')); } if (query.trim().length === 0) { throw new SearchError('empty query', new Error('Search query cannot be empty')); } const effectiveLimit = Math.min(limit, this.config.maxResults || 20); // Parse the natural language query const { searchQuery, filters } = this.queryParser.parseQuery(query); const ftsQuery = this.queryParser.buildFTSQuery(searchQuery); // Perform search const searchOptions: SearchOptions = { query: ftsQuery, limit: effectiveLimit * 5, // Get more results for better grouping includeContext, contextSize: this.config.defaultContextSize || 2, ...filters, }; const results = this.indexer.getDatabase().search(searchOptions); // Use ResultFormatter to process and group results const { conversations, totalMatches, totalConversations } = await this.resultFormatter.formatSearchResults(results, effectiveLimit); // Build the JSON output structure const output = { query: query, totalMatches: totalMatches, totalConversations: totalConversations, conversations: conversations }; // Return structured JSON data instead of formatted text const searchResults = { query: query, totalMatches: totalMatches, totalConversations: totalConversations, conversations: conversations.map(conv => ({ project: conv.projectName, date: conv.messages[0]?.timestamp.split('T')[0] || 'Unknown', summary: this.generateSummary(conv.messages), resumeCommand: conv.resumeCommand, conversationId: conv.conversationId })) }; return { content: [ { type: 'text', text: JSON.stringify(searchResults, null, 2), }, ], }; } catch (error) { this.logError(`Error searching conversations: ${(error as Error).message}`, error); return getErrorResponse(error, 'Search'); } } private generateSummary(messages: any[]): string { if (messages.length === 0) return 'No messages'; // Take a sample of message content to generate summary const sampleContent = messages .slice(0, 3) .map(msg => msg.content) .join(' ') .substring(0, 100) .replace(/[{}"]/g, '') .trim(); if (sampleContent.length > 50) { return sampleContent.substring(0, 50) + '...'; } return sampleContent || 'Conversation content'; } private async listProjects() { try { const projects = this.indexer.getDatabase().getProjects(); if (projects.length === 0) { return { content: [ { type: 'text', text: '📂 No projects indexed yet. Run refresh_index() to start indexing.', }, ], }; } return { content: [ { type: 'text', text: `Found ${projects.length} indexed project(s):`, }, { type: 'text', text: projects .map(p => `• ${p.name} (${p.messageCount} messages)`) .join('\n'), }, ], }; } catch (error) { this.logError(`Error listing projects: ${(error as Error).message}`, error); return getErrorResponse(error, 'List projects'); } } private async getMessageContext(args: any) { try { const { messageId, contextSize = this.config.defaultContextSize || 5 } = args; if (!messageId || typeof messageId !== 'string') { throw new SearchError('invalid message ID', new Error('Message ID is required and must be a string')); } // This would need to be implemented in the database class // For now, return a placeholder return { content: [ { type: 'text', text: `Context for message ${messageId} with ${contextSize} messages before/after`, }, ], }; } catch (error) { this.logError(`Error getting conversation context: ${(error as Error).message}`, error); return getErrorResponse(error, 'Get message context'); } } private async getConversationMessages(args: any) { try { const { conversationId, limit = 50, startFrom = 0 } = args; if (!conversationId || typeof conversationId !== 'string') { throw new SearchError('invalid conversation ID', new Error('Conversation ID is required and must be a string')); } const messages = this.indexer.getDatabase().getConversationMessages(conversationId, limit, startFrom); if (messages.length === 0) { return { content: [ { type: 'text', text: `No messages found for conversation ID: ${conversationId}`, }, ], }; } const output = { conversationId, messageCount: messages.length, startFrom, limit, messages: messages.map(msg => ({ id: msg.id, timestamp: msg.timestamp, type: msg.type, content: msg.content, messageUuid: msg.messageUuid, parentUuid: msg.parentUuid, projectPath: msg.projectPath, projectName: msg.projectName })) }; return { content: [ { type: 'text', text: `Found ${messages.length} messages in conversation ${conversationId}`, }, { type: 'text', text: JSON.stringify(output, null, 2), }, ], }; } catch (error) { this.logError(`Error getting conversation messages: ${(error as Error).message}`, error); return getErrorResponse(error, 'Get conversation messages'); } } private async listTools() { try { const tools = this.getTools(); const toolSignatures = tools.map(tool => { const params = tool.inputSchema?.properties || {}; const required = (tool.inputSchema as any)?.required || []; // Build parameter signature string const paramStrings = Object.entries(params).map(([name, schema]: [string, any]) => { const isRequired = required.includes(name); const type = schema.type || 'any'; const defaultValue = schema.default !== undefined ? ` = ${JSON.stringify(schema.default)}` : ''; const optional = isRequired ? '' : '?'; return `${name}${optional}: ${type}${defaultValue}`; }); const signature = `${tool.name}(${paramStrings.length > 0 ? `{ ${paramStrings.join(', ')} }` : ''})`; return { name: tool.name, signature, description: tool.description, parameters: Object.entries(params).map(([name, schema]: [string, any]) => ({ name, type: schema.type || 'any', required: required.includes(name), default: schema.default, description: schema.description })) }; }); const output = { totalTools: tools.length, tools: toolSignatures }; return { content: [ { type: 'text', text: `Available tools (${tools.length} total):`, }, { type: 'text', text: toolSignatures.map(tool => `• ${tool.signature}\n ${tool.description}` ).join('\n\n'), }, { type: 'text', text: '\nDetailed tool information:', }, { type: 'text', text: JSON.stringify(output, null, 2), }, ], }; } catch (error) { this.logError(`Error listing tools: ${(error as Error).message}`, error); return getErrorResponse(error, 'List tools'); } } private async getConfigInfo() { try { const configInfo = { database: { path: this.config.dbPath || '~/.claude/conversation-search.db', backupEnabled: this.config.dbBackupEnabled || false, backupInterval: this.config.dbBackupInterval || 86400000, }, indexing: { projectsDir: this.config.projectsDir || '~/.claude/projects', interval: this.config.indexInterval || 300000, autoIndexing: this.config.autoIndexing !== false, batchSize: this.config.indexBatchSize || 100, fullTextMinLength: this.config.fullTextMinLength || 3, threads: this.config.indexThreads || 1, }, search: { maxResults: this.config.maxResults || 20, defaultContextSize: this.config.defaultContextSize || 2, timeout: this.config.searchTimeout || 30000, }, performance: { maxMemoryMB: this.config.maxMemoryMB || 512, cacheSize: this.config.cacheSize || 1000, }, logging: { debug: this.config.debug || false, logLevel: this.config.logLevel || 'info', logToFile: this.config.logToFile || false, logFilePath: this.config.logFilePath || 'default', }, security: { allowedExtensions: this.config.allowedFileExtensions || ['.jsonl'], maxFileSize: this.config.maxFileSize || 104857600, }, }; const configSummary = [ '⚙️ **Current Configuration**', '', '**Database:**', `• Path: ${configInfo.database.path}`, `• Backup enabled: ${configInfo.database.backupEnabled}`, '', '**Indexing:**', `• Projects directory: ${configInfo.indexing.projectsDir}`, `• Auto-indexing: ${configInfo.indexing.autoIndexing}`, `• Interval: ${(configInfo.indexing.interval / 1000 / 60).toFixed(1)} minutes`, `• Batch size: ${configInfo.indexing.batchSize}`, '', '**Search:**', `• Max results: ${configInfo.search.maxResults}`, `• Default context: ${configInfo.search.defaultContextSize} messages`, `• Timeout: ${(configInfo.search.timeout / 1000).toFixed(1)} seconds`, '', '**Performance:**', `• Max memory: ${configInfo.performance.maxMemoryMB} MB`, `• Cache size: ${configInfo.performance.cacheSize} results`, '', '**Logging:**', `• Level: ${configInfo.logging.logLevel}`, `• Debug: ${configInfo.logging.debug}`, '', '**Security:**', `• Allowed extensions: ${configInfo.security.allowedExtensions.join(', ')}`, `• Max file size: ${(configInfo.security.maxFileSize / 1024 / 1024).toFixed(1)} MB`, '', '📖 **For configuration help, see [Configuration Guide](docs/configuration.md)**' ]; return { content: [ { type: 'text', text: configSummary.join('\n'), }, { type: 'text', text: `\n**Raw Configuration Object:**\n\`\`\`json\n${JSON.stringify(configInfo, null, 2)}\n\`\`\``, }, ], }; } catch (error) { this.logError(`Error getting configuration info: ${(error as Error).message}`, error); return getErrorResponse(error, 'Get configuration info'); } } private async getServerInfo() { try { // Default package info as fallback let packageInfo: any = { name: 'claude-code-conversation-search-mcp', version: '1.0.0', description: 'MCP server for searching Claude Code conversation history' }; // Try to read package.json for version info try { // Use process.cwd() as fallback that works in both environments const packageJsonPath = path.resolve(process.cwd(), 'package.json'); const packageContent = fs.readFileSync(packageJsonPath, 'utf8'); packageInfo = JSON.parse(packageContent); } catch (error) { // Use fallback package info } // Read recent changelog entries const changelogPath = path.resolve(process.cwd(), 'CHANGELOG.md'); let latestChanges = 'No changelog available'; try { const changelogContent = fs.readFileSync(changelogPath, 'utf8'); // Extract the unreleased section and latest version const unreleasedMatch = changelogContent.match(/## \[Unreleased\](.*?)(?=\n## \[|$)/s); const latestVersionMatch = changelogContent.match(/## \[(\d+\.\d+\.\d+)\] - (\d{4}-\d{2}-\d{2})(.*?)(?=\n## \[|$)/s); if (unreleasedMatch) { latestChanges = '## [Unreleased]' + unreleasedMatch[1].trim(); } else if (latestVersionMatch) { latestChanges = `## [${latestVersionMatch[1]}] - ${latestVersionMatch[2]}` + latestVersionMatch[3].trim(); } } catch (error) { // Fallback if we can't read changelog latestChanges = 'Changelog not accessible'; } // Get system information const systemInfo = { nodeVersion: process.version, platform: process.platform, architecture: process.arch, memoryUsage: process.memoryUsage(), uptime: Math.floor(process.uptime()), }; const serverInfo = [ '🚀 **Claude Conversation Search MCP Server**', '', '**Version Information:**', `• Package: ${packageInfo.name}`, `• Version: ${packageInfo.version}`, `• Description: ${packageInfo.description}`, '', '**System Information:**', `• Node.js: ${systemInfo.nodeVersion}`, `• Platform: ${systemInfo.platform} (${systemInfo.architecture})`, `• Memory Usage: ${Math.round(systemInfo.memoryUsage.rss / 1024 / 1024)} MB RSS`, `• Uptime: ${Math.floor(systemInfo.uptime / 60)} minutes`, '', '**Latest Changes:**', '```markdown', latestChanges, '```', '', '**Available Tools:**', `• ${this.getTools().length} tools available`, `• Use \`list_tools()\` to see all available functionality`, '', '**Links:**', `• Repository: ${packageInfo.repository?.url || 'https://github.com/TonySimonovsky/claude-code-conversation-search-mcp'}`, `• Issues: ${packageInfo.bugs?.url || 'https://github.com/TonySimonovsky/claude-code-conversation-search-mcp/issues'}`, `• Documentation: README.md and docs/`, '', '📖 **For help:** Use `get_config_info()` to check configuration or `list_tools()` to see available commands' ]; return { content: [ { type: 'text', text: serverInfo.join('\n'), }, { type: 'text', text: `\n**Raw Server Metadata:**\n\`\`\`json\n${JSON.stringify({ package: packageInfo, system: systemInfo, tools: this.getTools().map(tool => ({ name: tool.name, description: tool.description })) }, null, 2)}\n\`\`\``, }, ], }; } catch (error) { this.logError(`Error getting server info: ${(error as Error).message}`, error); return getErrorResponse(error, 'Get server info'); } } private async refreshIndex() { try { this.log('Manual index refresh requested'); const result = await this.indexer.indexAll((message) => { this.log(message); }); return { content: [ { type: 'text', text: `✅ Indexing complete! Indexed ${result.messagesIndexed} messages from ${result.filesIndexed} files.`, }, ], }; } catch (error) { this.logError(`Error refreshing index: ${(error as Error).message}`, error); return getErrorResponse(error, 'Index refresh'); } } async run() { try { const transport = new StdioServerTransport(); await this.server.connect(transport); this.log('Claude Conversation Search MCP server running'); console.error('Claude Conversation Search MCP server started successfully'); // Start indexing after server is connected and ready this.startBackgroundIndexing(); } catch (error) { this.logError('Failed to start server', error); throw error; } } shutdown() { if (this.indexingTimer) { clearInterval(this.indexingTimer); } this.indexer.close(); this.log('Server shutdown complete'); } } // Main entry point const server = new ConversationSearchServer(); // Handle graceful shutdown process.on('SIGINT', () => { console.error('\nShutting down server...'); server.shutdown(); process.exit(0); }); process.on('SIGTERM', () => { console.error('\nShutting down server...'); server.shutdown(); process.exit(0); }); server.run().catch((error) => { console.error('Fatal server error:', error.message); if (process.env.DEBUG === 'true') { console.error(error.stack); } process.exit(1); });