UNPKG

mcp-prompt-optimizer

Version:

Professional cloud-based MCP server for AI-powered prompt optimization with intelligent context detection, team collaboration, enterprise-grade features, startup validation, and robust API key management. Universal compatibility with Claude Desktop, Curso

348 lines (293 loc) 12.3 kB
/** * Cloud API Key Manager for MCP Prompt Optimizer * Mirrors the patterns from the local license-manager.js for consistency */ const fs = require('fs').promises; const path = require('path'); const https = require('https'); const os = require('os'); class CloudApiKeyManager { constructor(apiKey, options = {}) { this.apiKey = apiKey; this.backendUrl = options.backendUrl || 'https://p01--project-optimizer--fvrdk8m9k9j.code.run'; this.cacheFile = path.join(os.homedir(), '.mcp-cloud-api-cache.json'); this.cacheExpiry = options.cacheExpiry || 24 * 60 * 60 * 1000; // 24 hours this.logPrefix = '[CloudApiKeyManager]'; this.offlineMode = options.offlineMode || false; } 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}`); } } async validateApiKey() { this.log('Validating API key...'); if (!this.apiKey) { throw new Error('API key is required. Set OPTIMIZER_API_KEY environment variable or provide key directly.'); } if (!this.apiKey.startsWith('sk-opt-') && !this.apiKey.startsWith('sk-team-')) { throw new Error('Invalid API key format. Must be a cloud API key (sk-opt-* or sk-team-*)'); } try { // Try to validate with backend first const validation = await this.validateWithBackend(); // Cache valid results if (validation.valid) { await this.cacheValidation(validation); this.log(`API key validated successfully: ${validation.tier}`, 'success'); return validation; } else { throw new Error(validation.error || 'API key validation failed'); } } catch (error) { this.log(`Backend validation failed: ${error.message}`, 'warn'); // Try cached validation as fallback const cachedValidation = await this.getCachedValidation(); if (cachedValidation && !this.isCacheExpired(cachedValidation)) { this.log('Using cached API key validation', 'warn'); return cachedValidation.data; } // 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'); return cachedValidation.data; } throw new Error(`API key validation failed: ${error.message}`); } } async validateWithBackend() { return new Promise((resolve, reject) => { const url = `${this.backendUrl}/api/v1/mcp/validate-key`; const options = { method: 'POST', headers: { // Use lowercase header to match backend expectations 'x-api-key': this.apiKey, 'Content-Type': 'application/json', 'User-Agent': 'mcp-prompt-optimizer/1.2.0' }, timeout: 10000 // 10 second timeout }; this.log(`Making request with header: x-api-key = ${this.apiKey.substring(0, 16)}...`); const req = https.request(url, options, (res) => { let data = ''; res.on('data', (chunk) => { data += chunk; }); res.on('end', () => { this.log(`Response status: ${res.statusCode}`); this.log(`Response body: ${data.substring(0, 200)}...`); try { if (res.statusCode === 200) { const validation = JSON.parse(data); resolve(validation); } 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(10000); req.end(); }); } async checkQuotaStatus(validation) { const quota = validation.quota || {}; if (quota.unlimited) { return { allowed: true, unlimited: true }; } const quotaUsed = quota.used || 0; const quotaLimit = quota.limit || 5000; const quotaRemaining = quota.remaining || (quotaLimit - quotaUsed); if (quotaUsed >= quotaLimit) { const tier = validation.tier || 'explorer'; throw new Error( `Monthly quota exceeded (${quotaUsed}/${quotaLimit}). ` + `${tier === 'explorer' ? 'Upgrade to Creator ($25.99/mo) for 18,000 optimizations: https://promptoptimizer-blog.vercel.app/pricing' : 'Quota will reset on your next billing cycle.'}` ); } return { allowed: true, unlimited: false, used: quotaUsed, limit: quotaLimit, remaining: quotaRemaining }; } async getQuotaStatus() { try { const url = `${this.backendUrl}/api/v1/mcp/quota-status`; const options = { method: 'GET', headers: { 'x-api-key': this.apiKey, 'User-Agent': 'mcp-prompt-optimizer/1.2.0' }, timeout: 10000 }; return new Promise((resolve, reject) => { const req = https.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(10000); req.end(); }); } catch (error) { this.log(`Quota status check failed: ${error.message}`, 'warn'); throw error; } } async cacheValidation(validation) { try { const cacheData = { timestamp: Date.now(), data: validation }; await fs.writeFile(this.cacheFile, JSON.stringify(cacheData, null, 2)); this.log('API key validation cached'); } catch (error) { this.log(`Failed to cache validation: ${error.message}`, 'warn'); } } async getCachedValidation() { try { const cacheContent = await fs.readFile(this.cacheFile, 'utf8'); return JSON.parse(cacheContent); } catch (error) { return null; // No cache file or invalid cache } } isCacheExpired(cachedData) { return (Date.now() - cachedData.timestamp) > this.cacheExpiry; } async clearCache() { try { await fs.unlink(this.cacheFile); this.log('API key cache cleared'); } catch (error) { // File might not exist, that's fine } } async validateAndPrepare() { this.log('Starting API key validation and preparation...'); try { // Step 1: Validate API key const validation = await this.validateApiKey(); // Step 2: Check quota const quotaStatus = await this.checkQuotaStatus(validation); // Step 3: Log success if (quotaStatus.unlimited) { this.log(`API key valid: ${validation.tier} (unlimited usage)`, 'success'); } else { this.log(`API key valid: ${validation.tier} (${quotaStatus.remaining}/${quotaStatus.limit} remaining this month)`, 'success'); } return { validation, quotaStatus, tier: validation.tier, features: validation.features || {} }; } catch (error) { this.log(`API key validation failed: ${error.message}`, 'error'); throw error; } } // Helper method to get API key info for display async getApiKeyInfo() { try { const validation = await this.validateApiKey(); const quotaStatus = await this.checkQuotaStatus(validation); return { tier: validation.tier, features: validation.features || {}, quota: quotaStatus, isValid: true, keyType: validation.api_key_type || (this.apiKey.startsWith('sk-team-') ? 'team' : 'individual') }; } catch (error) { return { tier: null, features: {}, quota: { allowed: false }, isValid: false, error: error.message, keyType: 'unknown' }; } } // 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)}`; } } module.exports = CloudApiKeyManager;