Skip to main content
Glama

Industrial MCP Server

by intecrel
storage.ts12.3 kB
/** * OAuth Storage Abstraction Layer * Provides persistent storage for OAuth clients and authorization codes * Supports both Redis (production/preview) and in-memory (development/fallback) */ import { Redis } from '@upstash/redis'; import { OAuthClient } from './clients'; import { RedisKeys, getEnvironmentInfo } from './redis-keys'; export interface AuthCodeData { client_id: string; scope: string; redirect_uri: string; code_challenge?: string; code_challenge_method?: string; created_at: number; } export interface StorageAdapter { // Client management setClient(clientId: string, client: OAuthClient): Promise<void>; getClient(clientId: string): Promise<OAuthClient | null>; getAllClients(): Promise<OAuthClient[]>; deleteClient(clientId: string): Promise<void>; // Authorization code management setAuthCode(code: string, data: AuthCodeData, ttlSeconds: number): Promise<void>; getAuthCode(code: string): Promise<AuthCodeData | null>; deleteAuthCode(code: string): Promise<void>; // Rate limiting (optional) incrementRateLimit(clientId: string, window: string): Promise<number>; getRateLimit(clientId: string, window: string): Promise<number>; // Health check ping(): Promise<boolean>; // Cleanup cleanup(): Promise<void>; } /** * Redis-based storage adapter using Upstash */ class RedisStorageAdapter implements StorageAdapter { private redis: Redis; constructor() { const url = process.env.UPSTASH_REDIS_REST_URL; const token = process.env.UPSTASH_REDIS_REST_TOKEN; if (!url || !token) { throw new Error('Redis configuration missing: UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN required'); } this.redis = new Redis({ url, token }); const envInfo = getEnvironmentInfo(); console.log(`✅ Redis storage adapter initialized (${envInfo.environment})`, envInfo); } async setClient(clientId: string, client: OAuthClient): Promise<void> { try { await this.redis.set(RedisKeys.client(clientId), JSON.stringify(client)); console.log(`📦 Client stored in Redis: ${clientId}`); } catch (error) { console.error(`❌ Redis setClient error:`, error); throw new Error(`Failed to store client: ${error instanceof Error ? error.message : 'Unknown error'}`); } } async getClient(clientId: string): Promise<OAuthClient | null> { try { const data = await this.redis.get(RedisKeys.client(clientId)); if (!data) { return null; } // Handle both string and object responses from Upstash Redis let client: OAuthClient; if (typeof data === 'string') { client = JSON.parse(data) as OAuthClient; } else if (typeof data === 'object') { client = data as OAuthClient; } else { console.error(`❌ Unexpected Redis data type: ${typeof data}`); return null; } console.log(`📦 Client retrieved from Redis: ${clientId}`); return client; } catch (error) { console.error(`❌ Redis getClient error:`, error); return null; // Graceful degradation } } async getAllClients(): Promise<OAuthClient[]> { try { const keys = await this.redis.keys(RedisKeys.clientPattern()); if (keys.length === 0) { return []; } const clients: OAuthClient[] = []; for (const key of keys) { const data = await this.redis.get(key); if (data) { // Handle both string and object responses from Upstash Redis if (typeof data === 'string') { clients.push(JSON.parse(data) as OAuthClient); } else if (typeof data === 'object') { clients.push(data as OAuthClient); } } } console.log(`📦 Retrieved ${clients.length} clients from Redis`); return clients; } catch (error) { console.error(`❌ Redis getAllClients error:`, error); return []; // Graceful degradation } } async deleteClient(clientId: string): Promise<void> { try { await this.redis.del(RedisKeys.client(clientId)); console.log(`🗑️ Client deleted from Redis: ${clientId}`); } catch (error) { console.error(`❌ Redis deleteClient error:`, error); throw new Error(`Failed to delete client: ${error instanceof Error ? error.message : 'Unknown error'}`); } } async setAuthCode(code: string, data: AuthCodeData, ttlSeconds: number): Promise<void> { try { await this.redis.setex(RedisKeys.authCode(code), ttlSeconds, JSON.stringify(data)); console.log(`🔐 Auth code stored in Redis with TTL ${ttlSeconds}s`); } catch (error) { console.error(`❌ Redis setAuthCode error:`, error); throw new Error(`Failed to store auth code: ${error instanceof Error ? error.message : 'Unknown error'}`); } } async getAuthCode(code: string): Promise<AuthCodeData | null> { try { const data = await this.redis.get(RedisKeys.authCode(code)); if (!data) { return null; } // Handle both string and object responses from Upstash Redis let authData: AuthCodeData; if (typeof data === 'string') { authData = JSON.parse(data) as AuthCodeData; } else if (typeof data === 'object') { authData = data as AuthCodeData; } else { console.error(`❌ Unexpected Redis auth code data type: ${typeof data}`); return null; } console.log(`🔐 Auth code retrieved from Redis`); return authData; } catch (error) { console.error(`❌ Redis getAuthCode error:`, error); return null; // Graceful degradation } } async deleteAuthCode(code: string): Promise<void> { try { await this.redis.del(RedisKeys.authCode(code)); console.log(`🗑️ Auth code deleted from Redis`); } catch (error) { console.error(`❌ Redis deleteAuthCode error:`, error); // Don't throw - auth codes are temporary anyway } } async incrementRateLimit(clientId: string, window: string): Promise<number> { try { const key = RedisKeys.rateLimit(clientId, window); const count = await this.redis.incr(key); // Set TTL on first increment if (count === 1) { await this.redis.expire(key, 3600); // 1 hour TTL } return count; } catch (error) { console.error(`❌ Redis incrementRateLimit error:`, error); return 1; // Fail open for rate limiting } } async getRateLimit(clientId: string, window: string): Promise<number> { try { const count = await this.redis.get(RedisKeys.rateLimit(clientId, window)); return count ? parseInt(count as string, 10) : 0; } catch (error) { console.error(`❌ Redis getRateLimit error:`, error); return 0; // Fail open for rate limiting } } async ping(): Promise<boolean> { try { const result = await this.redis.ping(); return result === 'PONG'; } catch (error) { console.error(`❌ Redis ping error:`, error); return false; } } async cleanup(): Promise<void> { // Redis handles TTL cleanup automatically console.log('🧹 Redis cleanup: TTL-based, no manual cleanup needed'); } } /** * In-memory storage adapter for development and fallback */ class InMemoryStorageAdapter implements StorageAdapter { private clients = new Map<string, OAuthClient>(); private authCodes = new Map<string, { data: AuthCodeData; expires: number }>(); private rateLimits = new Map<string, { count: number; expires: number }>(); constructor() { console.log('✅ In-memory storage adapter initialized'); // Cleanup expired items every 5 minutes setInterval(() => { this.cleanup(); }, 5 * 60 * 1000); } async setClient(clientId: string, client: OAuthClient): Promise<void> { this.clients.set(clientId, client); console.log(`📦 Client stored in memory: ${clientId}`); } async getClient(clientId: string): Promise<OAuthClient | null> { const client = this.clients.get(clientId); if (client) { console.log(`📦 Client retrieved from memory: ${clientId}`); } return client || null; } async getAllClients(): Promise<OAuthClient[]> { const clients = Array.from(this.clients.values()); console.log(`📦 Retrieved ${clients.length} clients from memory`); return clients; } async deleteClient(clientId: string): Promise<void> { this.clients.delete(clientId); console.log(`🗑️ Client deleted from memory: ${clientId}`); } async setAuthCode(code: string, data: AuthCodeData, ttlSeconds: number): Promise<void> { const expires = Date.now() + (ttlSeconds * 1000); this.authCodes.set(code, { data, expires }); console.log(`🔐 Auth code stored in memory with TTL ${ttlSeconds}s`); } async getAuthCode(code: string): Promise<AuthCodeData | null> { const entry = this.authCodes.get(code); if (!entry) { return null; } if (Date.now() > entry.expires) { this.authCodes.delete(code); return null; } console.log(`🔐 Auth code retrieved from memory`); return entry.data; } async deleteAuthCode(code: string): Promise<void> { this.authCodes.delete(code); console.log(`🗑️ Auth code deleted from memory`); } async incrementRateLimit(clientId: string, window: string): Promise<number> { const key = `${clientId}:${window}`; const now = Date.now(); const hourStart = Math.floor(now / (60 * 60 * 1000)) * (60 * 60 * 1000); const expires = hourStart + (60 * 60 * 1000); const entry = this.rateLimits.get(key); if (!entry || now > entry.expires) { this.rateLimits.set(key, { count: 1, expires }); return 1; } entry.count++; return entry.count; } async getRateLimit(clientId: string, window: string): Promise<number> { const key = `${clientId}:${window}`; const entry = this.rateLimits.get(key); if (!entry || Date.now() > entry.expires) { return 0; } return entry.count; } async ping(): Promise<boolean> { return true; // Always healthy } async cleanup(): Promise<void> { const now = Date.now(); // Clean expired auth codes Array.from(this.authCodes.entries()).forEach(([code, entry]) => { if (now > entry.expires) { this.authCodes.delete(code); } }); // Clean expired rate limits Array.from(this.rateLimits.entries()).forEach(([key, entry]) => { if (now > entry.expires) { this.rateLimits.delete(key); } }); console.log('🧹 In-memory cleanup completed'); } } /** * Storage factory - creates appropriate adapter based on environment */ export const createStorageAdapter = (): StorageAdapter => { const useRedis = process.env.ENABLE_REDIS_STORAGE === 'true' && process.env.UPSTASH_REDIS_REST_URL && process.env.UPSTASH_REDIS_REST_TOKEN; if (useRedis) { try { return new RedisStorageAdapter(); } catch (error) { console.error('❌ Failed to initialize Redis storage, falling back to in-memory:', error); return new InMemoryStorageAdapter(); } } else { console.log('📝 Using in-memory storage (development mode)'); return new InMemoryStorageAdapter(); } }; // Singleton storage instance let storageInstance: StorageAdapter | null = null; /** * Get the global storage adapter instance */ export const getStorageAdapter = (): StorageAdapter => { if (!storageInstance) { storageInstance = createStorageAdapter(); } return storageInstance; }; /** * Reset storage adapter (for testing) */ export const resetStorageAdapter = (): void => { storageInstance = null; }; /** * Get Redis client for direct usage (Auth0 integration) */ export const getRedisClient = (): Redis => { const url = process.env.UPSTASH_REDIS_REST_URL; const token = process.env.UPSTASH_REDIS_REST_TOKEN; if (!url || !token) { throw new Error('Redis configuration missing: UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN required'); } return new Redis({ url, token }); };

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/intecrel/industrial-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server