UNPKG

mcp-prompt-optimizer

Version:

Professional cloud-based MCP server for AI-powered prompt optimization with intelligent context detection, Bayesian optimization, AG-UI real-time optimization, template auto-save, optimization insights, personal model configuration via WebUI, team collabo

751 lines (655 loc) 28.7 kB
/** * Cloud API Key Manager for MCP Prompt Optimizer * Production-grade with enhanced network resilience and development mode * ALIGNED with backend API requirements - FIXED API ENDPOINTS */ const fs = require('fs').promises; const path = require('path'); const https = require('https'); const http = require('http'); const os = require('os'); const packageJson = require('../package.json'); class CloudApiKeyManager { constructor(apiKey, options = {}) { this.apiKey = apiKey; this.backendUrl = options.backendUrl || process.env.OPTIMIZER_BACKEND_URL || 'https://p01--project-optimizer--fvmrdk8m9k9j.code.run'; this.cacheFile = path.join(os.homedir(), '.mcp-cloud-api-cache.json'); this.healthFile = path.join(os.homedir(), '.mcp-cloud-health.json'); this.cacheExpiry = options.cacheExpiry || 24 * 60 * 60 * 1000; // 24 hours this.fallbackCacheExpiry = options.fallbackCacheExpiry || 7 * 24 * 60 * 60 * 1000; // 7 days this.logPrefix = '[CloudApiKeyManager]'; this.offlineMode = options.offlineMode || false; this.developmentMode = options.developmentMode || process.env.NODE_ENV === 'development' || process.env.OPTIMIZER_DEV_MODE === 'true'; this.maxRetries = options.maxRetries || 5; // Increased for production this.baseRetryDelay = options.baseRetryDelay || 1000; this.maxRetryDelay = options.maxRetryDelay || 30000; this.requestTimeout = options.requestTimeout || 15000; // Network health tracking this.networkHealth = { consecutiveFailures: 0, lastSuccessful: null, avgResponseTime: null, lastErrorType: null }; } log(message, level = 'info') { const timestamp = new Date().toISOString(); const prefix = `${timestamp} ${this.logPrefix}`; if (level === 'error') { console.error(`${prefix} ❌ ${message}`); } else if (level === 'warn') { console.warn(`${prefix} ⚠️ ${message}`); } else if (level === 'success') { console.log(`${prefix} ✅ ${message}`); } else { console.log(`${prefix} ℹ️ ${message}`); } } // Production-grade exponential backoff with jitter calculateRetryDelay(attempt) { const exponentialDelay = Math.min( this.baseRetryDelay * Math.pow(2, attempt - 1), this.maxRetryDelay ); // Add jitter to prevent thundering herd const jitter = Math.random() * 0.3 * exponentialDelay; return Math.floor(exponentialDelay + jitter); } // Enhanced API key format validation validateApiKeyFormat(apiKey) { if (!apiKey || typeof apiKey !== 'string') { return { valid: false, error: 'API key must be a string' }; } // Support development keys const validPrefixes = ['sk-opt-', 'sk-team-', 'sk-local-', 'sk-dev-']; const hasValidPrefix = validPrefixes.some(prefix => apiKey.startsWith(prefix)); if (!hasValidPrefix) { return { valid: false, error: 'Invalid API key format. Must start with "sk-opt-" (individual), "sk-team-" (team), "sk-local-" (development), or "sk-dev-" (testing)' }; } // Check minimum length for security if (apiKey.length < 20) { return { valid: false, error: 'API key too short' }; } // Determine type let keyType = 'unknown'; if (apiKey.startsWith('sk-opt-')) { keyType = 'individual'; } else if (apiKey.startsWith('sk-team-')) { keyType = 'team'; } else if (apiKey.startsWith('sk-local-')) { keyType = 'development'; } else if (apiKey.startsWith('sk-dev-')) { keyType = 'testing'; } return { valid: true, keyType: keyType }; } // Development mode mock responses generateMockValidation(keyType) { const mockResponses = { individual: { valid: true, tier: 'explorer', api_key_type: 'individual', quota: { limit: 5000, used: Math.floor(Math.random() * 1000), unlimited: false }, features: { ai_context_detection: true, template_management: true, optimization_insights: true } }, team: { valid: true, tier: 'creator', api_key_type: 'team', quota: { limit: 18000, used: Math.floor(Math.random() * 3000), unlimited: false }, features: { ai_context_detection: true, template_management: true, team_collaboration: true, optimization_insights: true } }, development: { valid: true, tier: 'development', api_key_type: 'development', quota: { unlimited: true }, features: { ai_context_detection: true, template_management: true, optimization_insights: true, development_mode: true } }, testing: { valid: true, tier: 'testing', api_key_type: 'testing', quota: { limit: 1000, used: Math.floor(Math.random() * 100), unlimited: false }, features: { ai_context_detection: true, template_management: true, optimization_insights: true, testing_mode: true } } }; const response = mockResponses[keyType] || mockResponses.development; response.mock_mode = true; response.backend_url = 'mock://development-mode'; return response; } // Enhanced API key validation with production resilience async validateApiKey() { this.log('Starting comprehensive API key validation...'); if (!this.apiKey) { throw new Error('API key is required. Set OPTIMIZER_API_KEY environment variable or provide key directly.'); } // Step 1: Format validation const formatCheck = this.validateApiKeyFormat(this.apiKey); if (!formatCheck.valid) { throw new Error(formatCheck.error); } this.log(`API key format valid: ${formatCheck.keyType}`); if (this.developmentMode || formatCheck.keyType === 'testing') { this.log('Development/testing mode detected, using mock validation', 'warn'); const mockValidation = this.generateMockValidation(formatCheck.keyType); await this.cacheValidation(mockValidation); return mockValidation; } try { // Step 3: Backend validation with enhanced retry logic const validation = await this.validateWithBackendRetry(); // Step 4: Validate response structure if (validation && validation.valid) { await this.cacheValidation(validation); await this.updateNetworkHealth(true); this.log(`API key validated successfully: ${validation.tier}`, 'success'); return validation; } else { throw new Error(validation?.detail || validation?.error || 'API key validation failed'); } } catch (error) { this.log(`Backend validation failed: ${error.message}`, 'warn'); await this.updateNetworkHealth(false, error.message); // Enhanced fallback strategy const cachedValidation = await this.getCachedValidation(); if (cachedValidation && !this.isCacheExpired(cachedValidation)) { this.log('Using cached API key validation', 'warn'); return cachedValidation.data; } // Extended fallback for network issues if (cachedValidation && !this.isFallbackCacheExpired(cachedValidation)) { this.log('Using extended fallback cache due to network issues', 'warn'); const fallbackData = cachedValidation.data; fallbackData.fallback_mode = true; fallbackData.network_issue = error.message; return fallbackData; } // If we're in explicit offline mode and have any cache, use it if (this.offlineMode && cachedValidation) { this.log('Offline mode: using cached validation despite expiry', 'warn'); const offlineData = cachedValidation.data; offlineData.offline_mode = true; return offlineData; } throw new Error(`API key validation failed: ${error.message}`); } } // Production-grade retry logic with exponential backoff async validateWithBackendRetry() { let lastError; for (let attempt = 1; attempt <= this.maxRetries; attempt++) { try { this.log(`Validation attempt ${attempt}/${this.maxRetries}...`); const startTime = Date.now(); const result = await this.validateWithBackend(); // Track response time for health monitoring const responseTime = Date.now() - startTime; if (this.networkHealth.avgResponseTime === null) { this.networkHealth.avgResponseTime = responseTime; } else { this.networkHealth.avgResponseTime = (this.networkHealth.avgResponseTime + responseTime) / 2; } return result; } catch (error) { lastError = error; this.log(`Attempt ${attempt} failed: ${error.message}`, 'warn'); if (attempt < this.maxRetries) { const delay = this.calculateRetryDelay(attempt); this.log(`Retrying in ${delay}ms...`); await new Promise(resolve => setTimeout(resolve, delay)); } else { this.log('All retry attempts exhausted', 'error'); } } } throw lastError; } // ✅ FIXED: Correct API endpoint URL async validateWithBackend() { const endpoint = '/api/v1/api-keys/validate'; const method = 'POST'; try { const validation = await this._makeBackendRequest(endpoint, null, method); this.log(`Validation successful: ${JSON.stringify(validation, null, 2)}`); return validation; } catch (error) { this.log(`Backend validation request failed: ${error.message}`, 'error'); throw error; } } // ✅ FIXED: Correct API endpoint URL async getQuotaStatus() { try { const url = `${this.backendUrl}/api/v1/api-keys/quota-status`; const options = { method: 'GET', headers: { 'x-api-key': this.apiKey, 'User-Agent': `mcp-prompt-optimizer/${packageJson.version}`, 'Connection': 'close' }, timeout: this.requestTimeout }; return new Promise((resolve, reject) => { const client = this.backendUrl.startsWith('https://') ? https : http; const req = client.request(url, options, (res) => { let data = ''; res.on('data', (chunk) => { data += chunk; }); res.on('end', () => { try { if (res.statusCode === 200) { const result = JSON.parse(data); resolve(result); } else { let errorMessage; try { const error = JSON.parse(data); errorMessage = error.detail || `HTTP ${res.statusCode}`; } catch { errorMessage = `HTTP ${res.statusCode}: ${data}`; } reject(new Error(errorMessage)); } } catch (parseError) { reject(new Error(`Invalid response: ${parseError.message}`)); } }); }); req.on('error', (error) => { reject(new Error(`Network error: ${error.message}`)); }); req.on('timeout', () => { req.destroy(); reject(new Error('Request timeout')); }); req.setTimeout(this.requestTimeout); req.end(); }); } catch (error) { this.log(`Quota status check failed: ${error.message}`, 'warn'); throw error; } } // Network health tracking async updateNetworkHealth(success, errorMessage = null) { try { if (success) { this.networkHealth.consecutiveFailures = 0; this.networkHealth.lastSuccessful = Date.now(); this.networkHealth.lastErrorType = null; } else { this.networkHealth.consecutiveFailures++; this.networkHealth.lastErrorType = errorMessage; } // Save health metrics await fs.writeFile(this.healthFile, JSON.stringify(this.networkHealth, null, 2)); } catch (error) { this.log(`Failed to update network health: ${error.message}`, 'warn'); } } // Enhanced caching with metadata async cacheValidation(validation) { try { const cacheData = { timestamp: Date.now(), apiKeyPrefix: this.apiKey.substring(0, 20) + '...', // Safe prefix only data: validation, backendUrl: this.backendUrl, packageVersion: packageJson.version, networkHealth: { ...this.networkHealth } }; await fs.writeFile(this.cacheFile, JSON.stringify(cacheData, null, 2)); this.log('API key validation cached successfully'); } catch (error) { this.log(`Failed to cache validation: ${error.message}`, 'warn'); } } async getCachedValidation() { try { const cacheContent = await fs.readFile(this.cacheFile, 'utf8'); const cached = JSON.parse(cacheContent); // Validate cache structure if (!cached.timestamp || !cached.data) { this.log('Invalid cache structure, ignoring', 'warn'); return null; } return cached; } catch (error) { if (error.code !== 'ENOENT') { this.log(`Cache read error: ${error.message}`, 'warn'); } return null; } } isCacheExpired(cachedData) { if (!cachedData || !cachedData.timestamp) { return true; } const age = Date.now() - cachedData.timestamp; const expired = age > this.cacheExpiry; if (expired) { this.log(`Cache expired: ${Math.round(age / 1000 / 60)} minutes old`); } return expired; } // Extended fallback cache for network issues isFallbackCacheExpired(cachedData) { if (!cachedData || !cachedData.timestamp) { return true; } const age = Date.now() - cachedData.timestamp; const expired = age > this.fallbackCacheExpiry; if (expired) { this.log(`Fallback cache expired: ${Math.round(age / 1000 / 60 / 60)} hours old`); } return expired; } async clearCache() { try { await fs.unlink(this.cacheFile); this.log('API key cache cleared successfully'); } catch (error) { if (error.code !== 'ENOENT') { this.log(`Cache clear error: ${error.message}`, 'warn'); } } try { await fs.unlink(this.healthFile); this.log('Network health cache cleared successfully'); } catch (error) { if (error.code !== 'ENOENT') { this.log(`Health cache clear error: ${error.message}`, 'warn'); } } } // Enhanced validation and preparation async validateAndPrepare() { this.log('Starting comprehensive API key validation and preparation...'); try { // Step 1: Validate API key const validation = await this.validateApiKey(); // Step 2: Get comprehensive quota status const info = await this.getApiKeyInfo(); let quotaStatus = info.quota; validation = info; // Use info as the main validation object for consistency // Step 3: Log success const mode = validation.mock_mode ? '(mock)' : validation.fallback_mode ? '(fallback)' : validation.offline_mode ? '(offline)' : ''; if (quotaStatus.unlimited) { this.log(`API key valid: ${validation.tier} ${mode} (unlimited usage)`, 'success'); } else { this.log(`API key valid: ${validation.tier} ${mode} (${quotaStatus.remaining}/${quotaStatus.limit} remaining this month)`, 'success'); } return { validation, quotaStatus, tier: validation.tier, features: validation.features || {}, mode: { development: this.developmentMode, mock: validation.mock_mode || false, fallback: validation.fallback_mode || false, offline: validation.offline_mode || false } }; } catch (error) { this.log(`API key validation failed: ${error.message}`, 'error'); throw error; } } // Enhanced API key info with mode detection async getApiKeyInfo() { const formatCheck = this.validateApiKeyFormat(this.apiKey); if (this.developmentMode || formatCheck.keyType === 'development' || formatCheck.keyType === 'testing') { this.log('Development/testing mode detected for getApiKeyInfo, returning mock data.', 'warn'); return { tier: 'testing', features: { optimization: true, template_search: true, template_auto_save: true, optimization_insights: true, bayesian_optimization: true, agui_features: true }, quota: { unlimited: false, used: Math.floor(Math.random() * 100), limit: 1000, remaining: 1000 - Math.floor(Math.random() * 100) }, isValid: true, keyType: formatCheck.keyType, mode: { mock: true, fallback: false, offline: false, development: this.developmentMode } }; } try { // Directly call the new quota-status endpoint const quotaStatusResponse = await this._makeBackendRequest('/api/v1/api-keys/quota-status', null, 'GET'); // The backend /mcp/quota-status endpoint returns a comprehensive object // that includes tier, quota details, and features. return { tier: quotaStatusResponse.tier, features: quotaStatusResponse.features_available || {}, quota: quotaStatusResponse.quota, isValid: true, keyType: quotaStatusResponse.account_type || this.validateApiKeyFormat(this.apiKey).keyType, mode: { mock: false, // This endpoint doesn't return mock status fallback: false, offline: false, development: this.developmentMode } }; } catch (error) { this.log(`Error getting API key info: ${error.message}`, 'error'); return { tier: null, features: {}, quota: { allowed: false }, isValid: false, error: error.message, keyType: 'unknown', mode: { mock: false, fallback: false, offline: false, development: this.developmentMode } }; } } // Static method to get API key from environment static getApiKey() { const envKey = process.env.OPTIMIZER_API_KEY; if (envKey) { return envKey; } throw new Error( 'API key required. Set the OPTIMIZER_API_KEY environment variable.\n' + 'Get your API key at: https://promptoptimizer-blog.vercel.app/pricing' ); } // Static method to create manager with environment key static fromEnvironment(options = {}) { const apiKey = CloudApiKeyManager.getApiKey(); return new CloudApiKeyManager(apiKey, options); } // Format key for display (hide sensitive parts) formatKeyForDisplay() { if (!this.apiKey) return 'No key'; return `${this.apiKey.substring(0, 8)}...${this.apiKey.slice(-4)}`; } async getDiagnosticInfo() { const diagnosticInfo = { apiKey: this.apiKey ? this.formatKeyForDisplay() : 'Not set', backendUrl: this.backendUrl, cacheFile: this.cacheFile, healthFile: this.healthFile, cacheExpiry: this.cacheExpiry, fallbackCacheExpiry: this.fallbackCacheExpiry, offlineMode: this.offlineMode, developmentMode: this.developmentMode, maxRetries: this.maxRetries, requestTimeout: this.requestTimeout, nodeEnv: process.env.NODE_ENV || 'not set', packageVersion: packageJson.version, networkHealth: { ...this.networkHealth }, cache: {}, keyFormat: this.validateApiKeyFormat(this.apiKey), backendConnectivity: { status: 'unknown', error: null, responseTime: null } }; // Get cache status try { const cached = await this.getCachedValidation(); if (cached) { diagnosticInfo.cache.exists = true; diagnosticInfo.cache.expired = this.isCacheExpired(cached); diagnosticInfo.cache.fallbackExpired = this.isFallbackCacheExpired(cached); diagnosticInfo.cache.age = Math.round((Date.now() - cached.timestamp) / 1000 / 60); diagnosticInfo.cache.backendUrl = cached.backendUrl; diagnosticInfo.cache.packageVersion = cached.packageVersion; } else { diagnosticInfo.cache.exists = false; } } catch (error) { diagnosticInfo.cache.error = error.message; diagnosticInfo.cache.exists = false; } // Check backend connectivity if (this.apiKey) { // Only check if API key is present try { const startTime = Date.now(); await this.validateWithBackend(); // This will attempt to connect to the backend const responseTime = Date.now() - startTime; diagnosticInfo.backendConnectivity.status = 'success'; diagnosticInfo.backendConnectivity.responseTime = responseTime; } catch (error) { diagnosticInfo.backendConnectivity.status = 'failed'; diagnosticInfo.backendConnectivity.error = error.message; } } else { diagnosticInfo.backendConnectivity.status = 'skipped'; diagnosticInfo.backendConnectivity.error = 'No API key provided for connectivity check.'; } return diagnosticInfo; } async _makeBackendRequest(endpoint, data, method = 'POST') { return new Promise((resolve, reject) => { const url = `${this.backendUrl}${endpoint}`; const options = { method: method, headers: { 'x-api-key': this.apiKey, 'Content-Type': 'application/json', 'User-Agent': `mcp-prompt-optimizer/${packageJson.version}`, 'Accept': 'application/json', 'Connection': 'close' }, timeout: this.requestTimeout }; const client = this.backendUrl.startsWith('https://') ? https : http; const req = client.request(url, options, (res) => { let responseData = ''; res.on('data', (chunk) => { responseData += chunk; }); res.on('end', () => { try { if (res.statusCode >= 200 && res.statusCode < 300) { const parsed = JSON.parse(responseData); resolve(parsed); } else { let errorMessage; try { const error = JSON.parse(responseData); errorMessage = error.detail || error.message || `HTTP ${res.statusCode}`; } catch { errorMessage = `HTTP ${res.statusCode}: ${responseData}`; } reject(new Error(errorMessage)); } } catch (parseError) { reject(new Error(`Invalid response format: ${parseError.message}`)); } }); }); req.on('error', (error) => { if (error.code === 'ENOTFOUND') { reject(new Error(`DNS resolution failed: Cannot resolve ${this.backendUrl.replace(/^https?:\/\//, '')}`)); } else if (error.code === 'ECONNREFUSED') { reject(new Error(`Connection refused: Backend server may be down`)); } else if (error.code === 'ETIMEDOUT') { reject(new Error(`Connection timeout: Backend server is not responding`)); } else if (error.code === 'ECONNRESET') { reject(new Error(`Connection reset: Network instability detected`)); } else { reject(new Error(`Network error: ${error.message}`)); } }); req.on('timeout', () => { req.destroy(); reject(new Error('Request timeout - backend may be unavailable')); }); req.setTimeout(this.requestTimeout); if (method !== 'GET' && data) { req.write(JSON.stringify(data)); } req.end(); }); } } module.exports = CloudApiKeyManager;