UNPKG

ultimate-mcp-server

Version:

The definitive all-in-one Model Context Protocol server for AI-assisted coding across 30+ platforms

215 lines 7.41 kB
import { createClient } from "redis"; import postgres from "postgres"; import { Logger } from "../utils/logger.js"; import crypto from "crypto"; import { SessionStorage } from "./session-storage.js"; export class ContextManager { redis = null; sql = null; sessionStorage; logger; currentConversationId; initialized = false; inMemoryCache = new Map(); constructor() { this.logger = new Logger("ContextManager"); this.currentConversationId = this.generateId(); this.sessionStorage = new SessionStorage(); } async initialize() { if (this.initialized) return; try { // Initialize session storage (file-based, always available) await this.sessionStorage.initialize(); this.logger.info("Session storage initialized"); // Initialize Redis for fast context retrieval (optional) if (process.env.REDIS_URL) { this.redis = createClient({ url: process.env.REDIS_URL }); await this.redis.connect(); this.logger.info("Connected to Redis"); } // Initialize PostgreSQL for persistent storage (optional) if (process.env.DATABASE_URL) { this.sql = postgres(process.env.DATABASE_URL); await this.setupDatabase(); this.logger.info("Connected to PostgreSQL"); } this.initialized = true; } catch (error) { this.logger.warn("Some persistence layers not available:", error); // Continue with available storage options } } async setupDatabase() { if (!this.sql) return; await this.sql ` CREATE TABLE IF NOT EXISTS conversations ( id VARCHAR(64) PRIMARY KEY, title VARCHAR(255), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) `; await this.sql ` CREATE TABLE IF NOT EXISTS messages ( id VARCHAR(64) PRIMARY KEY, conversation_id VARCHAR(64) REFERENCES conversations(id), role VARCHAR(20) NOT NULL, content TEXT NOT NULL, name VARCHAR(100), metadata JSONB, tokens INTEGER, timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP, INDEX idx_conversation_timestamp (conversation_id, timestamp) ) `; } generateId() { return crypto.randomBytes(16).toString("hex"); } async startNewConversation(title) { this.currentConversationId = this.generateId(); if (this.sql) { await this.sql ` INSERT INTO conversations (id, title) VALUES (${this.currentConversationId}, ${title || null}) `; } // Clear Redis cache for new conversation if (this.redis) { await this.redis.del(`conversation:${this.currentConversationId}`); } this.logger.info(`Started new conversation: ${this.currentConversationId}`); return this.currentConversationId; } async addMessage(message) { const messageId = this.generateId(); const timestamp = new Date(); // Always save to session storage (file-based) await this.sessionStorage.addMessage(this.currentConversationId, message); // Save to PostgreSQL if available if (this.sql) { await this.sql ` INSERT INTO messages (id, conversation_id, role, content, name, metadata, timestamp) VALUES ( ${messageId}, ${this.currentConversationId}, ${message.role}, ${message.content}, ${message.name || null}, ${JSON.stringify(message.metadata) || null}, ${timestamp} ) `; } // Cache in Redis if available if (this.redis) { const key = `conversation:${this.currentConversationId}`; const messageData = JSON.stringify({ ...message, id: messageId, timestamp }); await this.redis.rPush(key, messageData); await this.redis.expire(key, 3600); // 1 hour TTL } // Also update in-memory cache const messageRecord = { ...message, id: messageId, conversation_id: this.currentConversationId, timestamp }; if (!this.inMemoryCache.has(this.currentConversationId)) { this.inMemoryCache.set(this.currentConversationId, []); } this.inMemoryCache.get(this.currentConversationId).push(messageRecord); } async getConversationHistory(conversationId, limit = 50) { const targetId = conversationId || this.currentConversationId; // Try in-memory cache first (fastest) if (this.inMemoryCache.has(targetId)) { const cached = this.inMemoryCache.get(targetId); return cached.slice(-limit); } // Try Redis next (fast) if (this.redis) { const key = `conversation:${targetId}`; const messages = await this.redis.lRange(key, -limit, -1); if (messages.length > 0) { return messages.map(m => JSON.parse(m)); } } // Try PostgreSQL (persistent) if (this.sql) { const messages = await this.sql ` SELECT * FROM messages WHERE conversation_id = ${targetId} ORDER BY timestamp DESC LIMIT ${limit} `; return messages.reverse(); } // Fallback to session storage (always available) const sessionMessages = await this.sessionStorage.getConversationHistory(targetId, limit); return sessionMessages.map(m => ({ ...m, id: this.generateId(), conversation_id: targetId, timestamp: new Date() })); } async searchMessages(query, limit = 20) { if (!this.sql) { return []; } const messages = await this.sql ` SELECT * FROM messages WHERE content ILIKE ${`%${query}%`} ORDER BY timestamp DESC LIMIT ${limit} `; return messages; } async getConversations(limit = 20) { if (!this.sql) { return []; } const conversations = await this.sql ` SELECT * FROM conversations ORDER BY updated_at DESC LIMIT ${limit} `; return conversations; } async deleteConversation(conversationId) { if (this.sql) { await this.sql ` DELETE FROM messages WHERE conversation_id = ${conversationId} `; await this.sql ` DELETE FROM conversations WHERE id = ${conversationId} `; } if (this.redis) { await this.redis.del(`conversation:${conversationId}`); } } getCurrentConversationId() { return this.currentConversationId; } async close() { // Save session storage await this.sessionStorage.cleanup(); // Close Redis if connected if (this.redis) { await this.redis.quit(); } // Close PostgreSQL if connected if (this.sql) { await this.sql.end(); } // Clear in-memory cache this.inMemoryCache.clear(); } } //# sourceMappingURL=context-manager.js.map