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
JavaScript
/**
* 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;