alphe-redis-mcp-server
Version:
The most comprehensive Redis MCP Server for Alphe.AI - Optimized for sub-5 second response times with multi-layer caching
667 lines (569 loc) • 20.8 kB
text/typescript
import Redis from 'ioredis';
import { Redis as UpstashRedis } from '@upstash/redis';
import LRUCache from 'lru-cache';
import { createClient as createSupabaseClient, SupabaseClient } from '@supabase/supabase-js';
import { MilvusClient } from '@zilliz/milvus2-sdk-node';
import {
RedisConfig,
UpstashConfig,
ZillizConfig,
SupabaseConfig,
PerformanceConfig,
CacheOptions,
CacheEntry,
CacheStats,
PerformanceMetrics,
RedisResponse,
HealthCheck,
ConnectionPool,
MemoryItem,
SearchOptions,
SearchResult
} from '../types/index.js';
export class AlpheRedisClient {
private redisClient: Redis | null = null;
private upstashClient: UpstashRedis | null = null;
private supabaseClient: SupabaseClient | null = null;
private milvusClient: MilvusClient | null = null;
// Multi-layer caching
private memoryCache: LRUCache<string, CacheEntry>;
private compressionEnabled: boolean = true;
private performanceMetrics: PerformanceMetrics;
// Configuration
private config: {
redis?: RedisConfig;
upstash?: UpstashConfig;
zilliz?: ZillizConfig;
supabase?: SupabaseConfig;
performance: PerformanceConfig;
};
constructor(config: any) {
this.config = config;
// Initialize in-memory L1 cache
this.memoryCache = new LRUCache<string, CacheEntry>({
max: 10000, // Maximum number of items
maxSize: config.performance.maxCacheSizeMB * 1024 * 1024, // Convert MB to bytes
ttl: config.performance.cacheTtlSeconds * 1000, // Convert to milliseconds
sizeCalculation: (value: CacheEntry) => value.size,
dispose: (value: CacheEntry, key: string) => {
console.log(`🗑️ Evicting cache entry: ${key}`);
}
});
this.compressionEnabled = config.performance.enableCompression;
// Initialize performance metrics
this.performanceMetrics = {
latency: { min: 0, max: 0, avg: 0, p50: 0, p95: 0, p99: 0 },
throughput: { requestsPerSecond: 0, bytesPerSecond: 0 },
memory: { used: 0, available: 0, fragmentation: 0 },
connections: { active: 0, idle: 0, total: 0 },
cache: {
totalKeys: 0,
totalHits: 0,
totalMisses: 0,
hitRate: 0,
memoryUsage: 0,
compressionRatio: 0,
avgLatency: 0,
topKeys: []
}
};
}
async initialize(): Promise<void> {
console.log('🚀 Initializing Alphe Redis Client with multi-layer caching...');
try {
// Initialize Redis client
if (this.config.redis) {
await this.initializeRedis();
}
// Initialize Upstash client
if (this.config.upstash) {
await this.initializeUpstash();
}
// Initialize Zilliz client
if (this.config.zilliz) {
await this.initializeZilliz();
}
// Initialize Supabase client
if (this.config.supabase) {
await this.initializeSupabase();
}
console.log('✅ All clients initialized successfully');
} catch (error) {
console.error('❌ Failed to initialize clients:', error);
throw error;
}
}
private async initializeRedis(): Promise<void> {
const config = this.config.redis!;
if (config.cluster && config.nodes) {
// Redis Cluster
this.redisClient = new Redis.Cluster(config.nodes, {
enableReadyCheck: config.enableReadyCheck ?? false,
redisOptions: {
password: config.password,
username: config.username,
connectTimeout: config.connectTimeout ?? 10000,
commandTimeout: config.commandTimeout ?? 5000,
retryDelayOnFailover: config.retryDelayOnFailover ?? 100,
maxRetriesPerRequest: config.maxRetriesPerRequest ?? 3,
lazyConnect: true
}
});
} else {
// Single Redis instance
this.redisClient = new Redis({
host: config.host ?? 'localhost',
port: config.port ?? 6379,
password: config.password,
username: config.username,
db: config.db ?? 0,
connectTimeout: config.connectTimeout ?? 10000,
commandTimeout: config.commandTimeout ?? 5000,
retryDelayOnFailover: config.retryDelayOnFailover ?? 100,
maxRetriesPerRequest: config.maxRetriesPerRequest ?? 3,
enableReadyCheck: config.enableReadyCheck ?? false,
lazyConnect: true,
keepAlive: config.keepAlive ?? 30000,
family: config.family ?? 4
});
}
// Event handlers
this.redisClient.on('connect', () => {
console.log('✅ Redis connected');
this.performanceMetrics.connections.active++;
});
this.redisClient.on('error', (error) => {
console.error('❌ Redis error:', error);
});
this.redisClient.on('ready', () => {
console.log('🚀 Redis ready');
});
this.redisClient.on('close', () => {
console.log('👋 Redis disconnected');
if (this.performanceMetrics.connections.active > 0) {
this.performanceMetrics.connections.active--;
}
});
await this.redisClient.connect();
}
private async initializeUpstash(): Promise<void> {
const config = this.config.upstash!;
this.upstashClient = new UpstashRedis({
url: config.url,
token: config.token,
retry: {
retries: 3,
backoff: (retryCount: number) => Math.exp(retryCount) * 50,
},
enableTelemetry: false
});
// Test connection
await this.upstashClient.ping();
console.log('✅ Upstash Redis connected');
}
private async initializeZilliz(): Promise<void> {
const config = this.config.zilliz!;
this.milvusClient = new MilvusClient({
address: config.endpoint,
token: config.token,
username: config.username,
password: config.password,
ssl: true,
timeout: 10000
});
console.log('✅ Zilliz/Milvus connected');
}
private async initializeSupabase(): Promise<void> {
const config = this.config.supabase!;
this.supabaseClient = createSupabaseClient(
config.url,
config.serviceRoleKey,
{
auth: {
persistSession: false,
autoRefreshToken: false
},
global: {
headers: {
'x-client-info': 'alphe-redis-mcp-server'
}
}
}
);
// Test connection
const { error } = await this.supabaseClient.from('mcp_cache').select('count').limit(1);
if (error && !error.message.includes('relation "mcp_cache" does not exist')) {
throw new Error(`Supabase connection failed: ${error.message}`);
}
console.log('✅ Supabase connected');
}
// Multi-layer GET with automatic fallback
async get<T = string>(key: string, options?: CacheOptions): Promise<RedisResponse<T>> {
const startTime = performance.now();
try {
// L1: Memory cache
const memoryResult = this.getFromMemoryCache<T>(key);
if (memoryResult.data !== undefined) {
this.performanceMetrics.cache.totalHits++;
return {
success: true,
data: memoryResult.data,
cached: true,
latency: performance.now() - startTime,
fromLevel: 'memory'
};
}
// L2: Redis cache
if (this.redisClient) {
const redisValue = await this.redisClient.get(key);
if (redisValue !== null) {
const data = this.deserializeValue<T>(redisValue);
// Store in memory cache
this.setInMemoryCache(key, data, options);
this.performanceMetrics.cache.totalHits++;
return {
success: true,
data,
cached: true,
latency: performance.now() - startTime,
fromLevel: 'redis'
};
}
}
// L3: Upstash fallback
if (this.upstashClient) {
const upstashValue = await this.upstashClient.get(key);
if (upstashValue !== null) {
const data = typeof upstashValue === 'string'
? this.deserializeValue<T>(upstashValue)
: upstashValue as T;
// Store in both Redis and memory cache
if (this.redisClient) {
await this.redisClient.set(key, this.serializeValue(data), 'EX', options?.ttl ?? this.config.performance.cacheTtlSeconds);
}
this.setInMemoryCache(key, data, options);
this.performanceMetrics.cache.totalHits++;
return {
success: true,
data,
cached: true,
latency: performance.now() - startTime,
fromLevel: 'redis'
};
}
}
// L4: Check Zilliz for vector-based retrieval (if key suggests it's a search query)
if (this.milvusClient && key.startsWith('search:')) {
const searchResult = await this.searchInZilliz(key);
if (searchResult.length > 0) {
const data = searchResult as unknown as T;
// Cache the result
await this.set(key, data, { ttl: options?.ttl ?? 1800 }); // 30 min cache
return {
success: true,
data,
cached: false,
latency: performance.now() - startTime,
fromLevel: 'zilliz'
};
}
}
// L5: Check Supabase for persistent storage
if (this.supabaseClient && key.startsWith('persistent:')) {
const supabaseResult = await this.getFromSupabase(key);
if (supabaseResult !== null) {
const data = supabaseResult as T;
// Cache the result in all layers
await this.set(key, data, { ttl: options?.ttl ?? 3600 }); // 1 hour cache
return {
success: true,
data,
cached: false,
latency: performance.now() - startTime,
fromLevel: 'supabase'
};
}
}
this.performanceMetrics.cache.totalMisses++;
return {
success: false,
error: 'Key not found in any cache layer',
latency: performance.now() - startTime
};
} catch (error) {
this.performanceMetrics.cache.totalMisses++;
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
latency: performance.now() - startTime
};
}
}
// Multi-layer SET with intelligent caching
async set<T = string>(key: string, value: T, options?: CacheOptions): Promise<RedisResponse<boolean>> {
const startTime = performance.now();
try {
const serializedValue = this.serializeValue(value);
const ttl = options?.ttl ?? this.config.performance.cacheTtlSeconds;
// Store in all available layers based on options
const promises: Promise<any>[] = [];
// L1: Memory cache (always store)
this.setInMemoryCache(key, value, options);
// L2: Redis
if (this.redisClient) {
promises.push(this.redisClient.set(key, serializedValue, 'EX', ttl));
}
// L3: Upstash (for backup/distribution)
if (this.upstashClient) {
promises.push(this.upstashClient.set(key, serializedValue, { ex: ttl }));
}
// L4: Zilliz (for vector data)
if (this.milvusClient && this.isVectorData(value)) {
promises.push(this.storeInZilliz(key, value as any));
}
// L5: Supabase (for persistent data)
if (this.supabaseClient && options?.namespace === 'persistent') {
promises.push(this.storeInSupabase(key, value));
}
await Promise.all(promises);
return {
success: true,
data: true,
latency: performance.now() - startTime
};
} catch (error) {
return {
success: false,
data: false,
error: error instanceof Error ? error.message : 'Unknown error',
latency: performance.now() - startTime
};
}
}
// Batch operations for better performance
async mget(keys: string[]): Promise<RedisResponse<(string | null)[]>> {
const startTime = performance.now();
try {
const results: (string | null)[] = new Array(keys.length);
const missingKeys: { index: number; key: string }[] = [];
// Check memory cache first
for (let i = 0; i < keys.length; i++) {
const memoryResult = this.getFromMemoryCache(keys[i]);
if (memoryResult.data !== undefined) {
results[i] = this.serializeValue(memoryResult.data);
this.performanceMetrics.cache.totalHits++;
} else {
missingKeys.push({ index: i, key: keys[i] });
}
}
// Fetch missing keys from Redis
if (missingKeys.length > 0 && this.redisClient) {
const redisKeys = missingKeys.map(mk => mk.key);
const redisResults = await this.redisClient.mget(...redisKeys);
for (let i = 0; i < redisResults.length; i++) {
const value = redisResults[i];
const { index, key } = missingKeys[i];
if (value !== null) {
results[index] = value;
// Cache in memory
this.setInMemoryCache(key, this.deserializeValue(value));
this.performanceMetrics.cache.totalHits++;
} else {
results[index] = null;
this.performanceMetrics.cache.totalMisses++;
}
}
}
return {
success: true,
data: results,
latency: performance.now() - startTime
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
latency: performance.now() - startTime
};
}
}
// Advanced search with semantic similarity
async semanticSearch(query: string, options: SearchOptions = {}): Promise<SearchResult[]> {
if (!this.milvusClient) {
throw new Error('Zilliz/Milvus client not initialized for semantic search');
}
const cacheKey = `semantic_search:${query}:${JSON.stringify(options)}`;
// Check cache first
const cachedResult = await this.get<SearchResult[]>(cacheKey);
if (cachedResult.success && cachedResult.data) {
return cachedResult.data;
}
// Perform semantic search in Zilliz
const results = await this.searchInZilliz(query, options);
// Cache results for future queries
await this.set(cacheKey, results, { ttl: 900, namespace: 'search' }); // 15 min cache
return results;
}
// Performance monitoring
async getPerformanceMetrics(): Promise<PerformanceMetrics> {
const cacheSize = this.memoryCache.size;
const cacheCalculatedSize = this.memoryCache.calculatedSize ?? 0;
this.performanceMetrics.cache.totalKeys = cacheSize;
this.performanceMetrics.cache.memoryUsage = cacheCalculatedSize;
this.performanceMetrics.cache.hitRate =
this.performanceMetrics.cache.totalHits /
(this.performanceMetrics.cache.totalHits + this.performanceMetrics.cache.totalMisses) || 0;
if (this.redisClient) {
try {
const info = await this.redisClient.info('memory');
const memoryMatch = info.match(/used_memory:(\d+)/);
if (memoryMatch) {
this.performanceMetrics.memory.used = parseInt(memoryMatch[1]);
}
} catch (error) {
console.warn('Could not fetch Redis memory info:', error);
}
}
return { ...this.performanceMetrics };
}
// Health check for all systems
async healthCheck(): Promise<HealthCheck> {
const health: HealthCheck = {
redis: { connected: false, latency: 0, memory: 0, version: 'unknown' },
zilliz: { connected: false, latency: 0, collections: 0 },
supabase: { connected: false, latency: 0 },
overall: 'unhealthy'
};
try {
// Redis health check
if (this.redisClient) {
const startTime = performance.now();
const pong = await this.redisClient.ping();
health.redis.latency = performance.now() - startTime;
health.redis.connected = pong === 'PONG';
const info = await this.redisClient.info();
const versionMatch = info.match(/redis_version:([^\r\n]+)/);
if (versionMatch) health.redis.version = versionMatch[1];
}
// Zilliz health check
if (this.milvusClient) {
const startTime = performance.now();
const collections = await this.milvusClient.listCollections();
health.zilliz.latency = performance.now() - startTime;
health.zilliz.connected = true;
health.zilliz.collections = collections.collection_names?.length ?? 0;
}
// Supabase health check
if (this.supabaseClient) {
const startTime = performance.now();
const { error } = await this.supabaseClient.from('mcp_cache').select('count').limit(1);
health.supabase.latency = performance.now() - startTime;
health.supabase.connected = !error || error.message.includes('relation "mcp_cache" does not exist');
}
// Determine overall health
const connectedSystems = [
health.redis.connected,
health.zilliz.connected,
health.supabase.connected
].filter(Boolean).length;
if (connectedSystems >= 2) {
health.overall = 'healthy';
} else if (connectedSystems >= 1) {
health.overall = 'degraded';
}
} catch (error) {
console.error('Health check error:', error);
}
return health;
}
// Utility methods
private getFromMemoryCache<T>(key: string): { data: T | undefined; cached: boolean } {
const entry = this.memoryCache.get(key);
if (entry && Date.now() - entry.timestamp < entry.ttl * 1000) {
entry.hits++;
return { data: entry.data as T, cached: true };
}
return { data: undefined, cached: false };
}
private setInMemoryCache<T>(key: string, value: T, options?: CacheOptions): void {
const serialized = this.serializeValue(value);
const compressed = this.compressionEnabled && serialized.length > 1000;
const size = compressed ? Math.floor(serialized.length / 2) : serialized.length;
const entry: CacheEntry<T> = {
data: value,
timestamp: Date.now(),
ttl: options?.ttl ?? this.config.performance.cacheTtlSeconds,
hits: 1,
size,
compressed,
tags: options?.tags ?? [],
priority: options?.priority ?? 1
};
this.memoryCache.set(key, entry);
}
private serializeValue<T>(value: T): string {
if (typeof value === 'string') return value;
return JSON.stringify(value);
}
private deserializeValue<T>(value: string): T {
try {
return JSON.parse(value) as T;
} catch {
return value as unknown as T;
}
}
private isVectorData(value: any): boolean {
return Array.isArray(value) && value.length > 0 && typeof value[0] === 'number';
}
private async searchInZilliz(query: string, options: SearchOptions = {}): Promise<SearchResult[]> {
// This would implement actual semantic search using embeddings
// For now, return empty results
return [];
}
private async storeInZilliz(key: string, value: any): Promise<void> {
// Implement vector storage in Zilliz
console.log(`Storing vector data in Zilliz: ${key}`);
}
private async getFromSupabase(key: string): Promise<any> {
if (!this.supabaseClient) return null;
try {
const { data, error } = await this.supabaseClient
.from('mcp_cache')
.select('cached_response')
.eq('cache_key', key)
.single();
if (error) return null;
return data?.cached_response;
} catch {
return null;
}
}
private async storeInSupabase(key: string, value: any): Promise<void> {
if (!this.supabaseClient) return;
try {
await this.supabaseClient
.from('mcp_cache')
.upsert({
cache_key: key,
cached_response: value,
created_at: new Date().toISOString(),
expires_at: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString() // 24 hours
});
} catch (error) {
console.error('Failed to store in Supabase:', error);
}
}
async disconnect(): Promise<void> {
console.log('🔌 Disconnecting all clients...');
const disconnectPromises: Promise<void>[] = [];
if (this.redisClient) {
disconnectPromises.push(this.redisClient.quit());
}
if (this.milvusClient) {
// Milvus client doesn't have explicit disconnect
}
await Promise.all(disconnectPromises);
this.memoryCache.clear();
console.log('👋 All clients disconnected');
}
}