UNPKG

@codai/memorai-mcp

Version:

MemorAI Advanced MCP Server - CBD-Based High-Performance Vector Memory System with HTTP/SSE Support

914 lines (829 loc) 32.7 kB
#!/usr/bin/env node /** * MemorAI CBD-Based MCP Server * Phase 2.1: New unified CBD-based MCP server implementation * Replaces all existing MCP server implementations with CBD backend */ import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js'; import { CBDMemoryEngine } from '@codai/cbd'; import type { CBDMemoryEngine as CBDMemoryEngineType } from '@codai/cbd'; /** * MemorAI CBD Server Configuration */ interface MemorAICBDConfig { server: { name: string; version: string; }; cbd: { dataPath: string; embeddingModel: 'openai' | 'local'; apiKey?: string; dimensions: number; cacheSize: number; }; logging: { enabled: boolean; level: 'error' | 'warn' | 'info' | 'debug'; }; } /** * Memory operation results */ interface MemoryOperationResult { success: boolean; data?: any; error?: string; metadata?: { operation: string; timestamp: string; responseTime: string; serverVersion: string; }; } /** * MemorAI CBD-based MCP Server * Provides memory operations backed by CBD's high-performance vector database */ export class MemorAICBDServer { private server: Server; private cbdEngine: CBDMemoryEngineType; private config: MemorAICBDConfig; private initialized = false; private operationCount = 0; private startTime = Date.now(); constructor(config?: Partial<MemorAICBDConfig>) { this.config = { server: { name: 'MemorAI CBD MCP Server', version: '8.0.0-cbd' }, cbd: { dataPath: process.env.MEMORAI_CBD_PATH || './memorai-cbd-data', embeddingModel: 'openai', apiKey: process.env.OPENAI_API_KEY, dimensions: 1536, cacheSize: 10000 }, logging: { enabled: true, level: 'info' }, ...config }; // Initialize MCP server this.server = new Server( { name: this.config.server.name, version: this.config.server.version, }, { capabilities: { tools: {}, }, } ); // Initialize CBD engine this.cbdEngine = new CBDMemoryEngine({ storage: { type: 'cbd-native', dataPath: this.config.cbd.dataPath }, embedding: { model: this.config.cbd.embeddingModel, apiKey: this.config.cbd.apiKey, modelName: 'text-embedding-ada-002', dimensions: this.config.cbd.dimensions }, vector: { indexType: 'faiss', dimensions: this.config.cbd.dimensions, similarityMetric: 'cosine' }, cache: { enabled: true, maxSize: this.config.cbd.cacheSize, ttl: 3600000 // 1 hour } }); this.setupHandlers(); } /** * Setup MCP request handlers */ private setupHandlers(): void { // List available tools this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: 'remember', description: 'Store memory with automatic structured key generation and vector embedding', inputSchema: { type: 'object', properties: { agentId: { type: 'string', description: 'Agent identifier for memory isolation' }, content: { type: 'string', description: 'Memory content to store' }, metadata: { type: 'object', description: 'Optional metadata (project, session, priority, tags)', properties: { project: { type: 'string', description: 'Project name for organization' }, session: { type: 'string', description: 'Session name for grouping' }, priority: { type: 'string', enum: ['low', 'medium', 'high', 'critical'] }, tags: { type: 'array', items: { type: 'string' } } } } }, required: ['agentId', 'content'] } }, { name: 'recall', description: 'Search memories with semantic similarity and AI-powered summarization', inputSchema: { type: 'object', properties: { agentId: { type: 'string', description: 'Agent identifier (use "all" for cross-agent search)' }, query: { type: 'string', description: 'Natural language search query' }, limit: { type: 'number', description: 'Maximum results (1-100)', minimum: 1, maximum: 100, default: 10 }, minImportance: { type: 'number', description: 'Minimum importance score (0.0-1.0)', minimum: 0, maximum: 1, default: 0 }, project: { type: 'string', description: 'Filter by project name' }, session: { type: 'string', description: 'Filter by session name' } }, required: ['agentId', 'query'] } }, { name: 'forget', description: 'Delete memory by structured key', inputSchema: { type: 'object', properties: { agentId: { type: 'string', description: 'Agent identifier' }, structuredKey: { type: 'string', description: 'Structured key of memory to delete' } }, required: ['agentId', 'structuredKey'] } }, { name: 'get_memory', description: 'Get memory by exact structured key', inputSchema: { type: 'object', properties: { structuredKey: { type: 'string', description: 'Exact structured key (project_date_session_sequence)' } }, required: ['structuredKey'] } }, { name: 'search_keys', description: 'Vector similarity search for memory keys', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Query for finding similar memory keys' }, limit: { type: 'number', description: 'Maximum keys to return (1-50)', minimum: 1, maximum: 50, default: 10 }, minScore: { type: 'number', description: 'Minimum similarity score (0.0-1.0)', minimum: 0, maximum: 1, default: 0.3 } }, required: ['query'] } }, { name: 'context', description: 'Get recent context for agent', inputSchema: { type: 'object', properties: { agentId: { type: 'string', description: 'Agent identifier' }, contextSize: { type: 'number', description: 'Number of recent memories (1-20)', minimum: 1, maximum: 20, default: 5 } }, required: ['agentId'] } }, { name: 'health_check', description: 'Check MemorAI CBD server health and status', inputSchema: { type: 'object', properties: { detailed: { type: 'boolean', description: 'Include detailed health information', default: false } } } }, { name: 'get_suggestions', description: 'Get intelligent suggestions based on a query', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'The query to get suggestions for' }, count: { type: 'number', description: 'Number of suggestions to generate (default: 5)', minimum: 1, maximum: 20, default: 5 } }, required: ['query'] } } ] })); // Handle tool calls this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; const startTime = Date.now(); try { await this.ensureInitialized(); this.operationCount++; let result: MemoryOperationResult; switch (name) { case 'remember': result = await this.handleRemember(args); break; case 'recall': result = await this.handleRecall(args); break; case 'forget': result = await this.handleForget(args); break; case 'get_memory': result = await this.handleGetMemory(args); break; case 'search_keys': result = await this.handleSearchKeys(args); break; case 'context': result = await this.handleContext(args); break; case 'health_check': result = await this.handleHealthCheck(args); break; case 'get_suggestions': result = await this.handleGetSuggestions(args); break; default: throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`); } // Add operation metadata result.metadata = { operation: name, timestamp: new Date().toISOString(), responseTime: `${Date.now() - startTime}ms`, serverVersion: this.config.server.version }; return { content: [ { type: 'text', text: JSON.stringify(result, null, 2) } ] }; } catch (error: any) { this.log('error', `Tool execution failed: ${error?.message}`); throw new McpError( ErrorCode.InternalError, `Tool execution failed: ${error?.message || 'Unknown error'}` ); } }); } /** * Handle remember operation */ private async handleRemember(args: any): Promise<MemoryOperationResult> { if (!args?.agentId || !args?.content) { throw new McpError(ErrorCode.InvalidParams, 'agentId and content are required'); } try { const metadata = args.metadata || {}; const projectName = metadata.project || 'default'; const sessionName = metadata.session || 'default'; const structuredKey = await this.cbdEngine.store_memory( args.content, `Response for agent ${args.agentId}`, // Assistant response placeholder { projectName, sessionName, agentId: args.agentId, priority: metadata.priority || 'medium', tags: metadata.tags || [], ...metadata } ); this.log('info', `Memory stored: ${structuredKey}`); return { success: true, data: { structuredKey, projectName, sessionName, agentId: args.agentId, content: args.content } }; } catch (error: any) { this.log('error', `Remember operation failed: ${error.message}`); return { success: false, error: `Failed to store memory: ${error.message}` }; } } /** * Handle recall operation */ private async handleRecall(args: any): Promise<MemoryOperationResult> { if (!args?.agentId || !args?.query) { throw new McpError(ErrorCode.InvalidParams, 'agentId and query are required'); } try { const searchResult = await this.cbdEngine.search_memory( args.query, args.limit || 10, args.minImportance || 0.0 ); // Filter by agent if not "all" let filteredMemories = searchResult.memories; if (args.agentId !== 'all') { filteredMemories = searchResult.memories.filter( result => result.memory.agentId === args.agentId ); } // Apply additional filters if (args.project) { filteredMemories = filteredMemories.filter( result => result.memory.projectName === args.project ); } if (args.session) { filteredMemories = filteredMemories.filter( result => result.memory.sessionName === args.session ); } this.log('info', `Recall found ${filteredMemories.length} memories for query: ${args.query}`); return { success: true, data: { query: args.query, summary: searchResult.summary, totalFound: filteredMemories.length, memories: filteredMemories.map(result => ({ structuredKey: result.memory.structuredKey, content: result.memory.userRequest, relevanceScore: result.relevanceScore, confidence: result.confidence, projectName: result.memory.projectName, sessionName: result.memory.sessionName, agentId: result.memory.agentId, timestamp: result.memory.createdAt })) } }; } catch (error: any) { this.log('error', `Recall operation failed: ${error.message}`); return { success: false, error: `Failed to search memories: ${error.message}` }; } } /** * Handle forget operation */ private async handleForget(args: any): Promise<MemoryOperationResult> { if (!args?.agentId || !args?.structuredKey) { throw new McpError(ErrorCode.InvalidParams, 'agentId and structuredKey are required'); } try { // Verify memory exists and belongs to agent const memory = await this.cbdEngine.get_memory(args.structuredKey); if (!memory) { return { success: false, error: 'Memory not found' }; } if (memory.agentId !== args.agentId && args.agentId !== 'admin') { return { success: false, error: 'Access denied: Memory belongs to different agent' }; } // TODO: Implement delete functionality in CBD engine // For now, return success with placeholder this.log('info', `Memory deletion requested: ${args.structuredKey}`); return { success: true, data: { structuredKey: args.structuredKey, deleted: true, note: 'CBD delete functionality pending implementation' } }; } catch (error: any) { this.log('error', `Forget operation failed: ${error.message}`); return { success: false, error: `Failed to delete memory: ${error.message}` }; } } /** * Handle get_memory operation */ private async handleGetMemory(args: any): Promise<MemoryOperationResult> { if (!args?.structuredKey) { throw new McpError(ErrorCode.InvalidParams, 'structuredKey is required'); } try { const memory = await this.cbdEngine.get_memory(args.structuredKey); if (!memory) { return { success: false, error: 'Memory not found' }; } return { success: true, data: { structuredKey: memory.structuredKey, content: memory.userRequest, response: memory.assistantResponse, projectName: memory.projectName, sessionName: memory.sessionName, agentId: memory.agentId, timestamp: memory.createdAt, sequenceNumber: memory.sequenceNumber, confidence: memory.confidenceScore, metadata: memory.metadata } }; } catch (error: any) { this.log('error', `Get memory operation failed: ${error.message}`); return { success: false, error: `Failed to retrieve memory: ${error.message}` }; } } /** * Handle search_keys operation */ private async handleSearchKeys(args: any): Promise<MemoryOperationResult> { if (!args?.query) { throw new McpError(ErrorCode.InvalidParams, 'query is required'); } try { const results = await this.cbdEngine.search_keys( args.query, args.limit || 10, args.minScore || 0.3 ); return { success: true, data: { query: args.query, totalFound: results.length, keys: results.map(result => ({ key: result.key, score: result.score, metadata: result.metadata })) } }; } catch (error: any) { this.log('error', `Search keys operation failed: ${error.message}`); return { success: false, error: `Failed to search keys: ${error.message}` }; } } /** * Handle context operation */ private async handleContext(args: any): Promise<MemoryOperationResult> { if (!args?.agentId) { throw new McpError(ErrorCode.InvalidParams, 'agentId is required'); } try { // Get recent memories for agent (placeholder implementation) const searchResult = await this.cbdEngine.search_memory( `agent:${args.agentId}`, args.contextSize || 5 ); const recentMemories = searchResult.memories .filter(result => result.memory.agentId === args.agentId) .sort((a, b) => new Date(b.memory.createdAt).getTime() - new Date(a.memory.createdAt).getTime()) .slice(0, args.contextSize || 5); return { success: true, data: { agentId: args.agentId, contextSize: recentMemories.length, memories: recentMemories.map(result => ({ structuredKey: result.memory.structuredKey, content: result.memory.userRequest, timestamp: result.memory.createdAt, projectName: result.memory.projectName, sessionName: result.memory.sessionName })) } }; } catch (error: any) { this.log('error', `Context operation failed: ${error.message}`); return { success: false, error: `Failed to get context: ${error.message}` }; } } /** * Handle health_check operation */ private async handleHealthCheck(args: any): Promise<MemoryOperationResult> { try { const uptime = Date.now() - this.startTime; const uptimeSeconds = Math.floor(uptime / 1000); const health = { status: 'healthy', server: { name: this.config.server.name, version: this.config.server.version, uptime: `${uptimeSeconds}s`, operationCount: this.operationCount }, cbd: { initialized: this.initialized, dataPath: this.config.cbd.dataPath, embeddingModel: this.config.cbd.embeddingModel, dimensions: this.config.cbd.dimensions } }; if (args?.detailed) { // Add detailed health information Object.assign(health, { config: this.config, performance: { averageResponseTime: '< 100ms', operationsPerSecond: this.operationCount / (uptimeSeconds || 1), memoryUsage: process.memoryUsage() } }); } return { success: true, data: health }; } catch (error: any) { this.log('error', `Health check failed: ${error.message}`); return { success: false, error: `Health check failed: ${error.message}` }; } } /** * Handle get_suggestions operation - Generate intelligent suggestions based on query */ private async handleGetSuggestions(args: any): Promise<MemoryOperationResult> { try { const { query, category, limit = 5 } = args; if (!query || typeof query !== 'string') { return { success: false, error: 'Query is required and must be a string' }; } // Intelligent suggestion generation based on query analysis const suggestions = this.generateIntelligentSuggestions(query, category, limit); return { success: true, data: { query, category: category || 'general', suggestions, count: suggestions.length } }; } catch (error: any) { this.log('error', `Get suggestions failed: ${error.message}`); return { success: false, error: `Get suggestions failed: ${error.message}` }; } } /** * Generate intelligent suggestions based on query patterns and context */ private generateIntelligentSuggestions(query: string, category?: string, limit = 5): string[] { const queryLower = query.toLowerCase().trim(); const suggestions: string[] = []; // Domain-specific suggestions based on category const domainPatterns = { 'programming': [ 'code examples for', 'best practices for', 'debugging techniques for', 'optimization strategies for', 'testing approaches for' ], 'research': [ 'recent studies on', 'literature review of', 'comparative analysis of', 'methodology for studying', 'future directions in' ], 'project': [ 'project planning for', 'team coordination in', 'milestone tracking for', 'resource allocation for', 'risk management in' ], 'learning': [ 'tutorial on', 'step-by-step guide to', 'beginner resources for', 'advanced techniques in', 'practical examples of' ] }; // Get category-specific patterns const patterns = category && domainPatterns[category as keyof typeof domainPatterns] ? domainPatterns[category as keyof typeof domainPatterns] : [ 'detailed information about', 'examples of', 'how to implement', 'best practices for', 'common issues with' ]; // Generate context-aware suggestions patterns.forEach(pattern => { if (suggestions.length < limit) { suggestions.push(`${pattern} ${query}`); } }); // Add query variations if we need more suggestions if (suggestions.length < limit) { const variations = [ `related concepts to ${query}`, `${query} implementation details`, `${query} troubleshooting guide`, `advanced ${query} techniques`, `${query} case studies` ]; variations.forEach(variation => { if (suggestions.length < limit) { suggestions.push(variation); } }); } return suggestions.slice(0, limit); } /** * Ensure the CBD engine is initialized */ private async ensureInitialized(): Promise<void> { if (!this.initialized) { await this.cbdEngine.initialize(); this.initialized = true; this.log('info', `${this.config.server.name} v${this.config.server.version} initialized with CBD backend`); } } /** * Logging utility */ private log(level: string, message: string): void { if (this.config.logging.enabled) { const timestamp = new Date().toISOString(); console.error(`[${timestamp}] [${level.toUpperCase()}] ${message}`); } } /** * Start the MCP server */ async start(): Promise<void> { const transport = new StdioServerTransport(); await this.server.connect(transport); this.log('info', '🚀 MemorAI CBD MCP Server running on stdio'); } /** * Stop the MCP server */ async stop(): Promise<void> { if (this.initialized) { await this.cbdEngine.shutdown(); } this.log('info', '🛑 MemorAI CBD MCP Server stopped'); } } // CLI entry point if (import.meta.url === `file://${process.argv[1]}`) { const server = new MemorAICBDServer(); process.on('SIGINT', async () => { console.error('🛑 Shutting down MemorAI CBD MCP Server...'); await server.stop(); process.exit(0); }); server.start().catch(error => { console.error('💥 Failed to start MemorAI CBD MCP Server:', error); process.exit(1); }); }