Skip to main content
Glama

Optimizely DXP MCP Server

by JaxonDigital
self-hosted-config.js13.6 kB
/** * Self-Hosted Azure Storage Configuration * Flexible configuration system for customer-managed Azure environments * Part of DXP-4: Support for self-hosted Optimizely CMS on Azure */ const fs = require('fs').promises; const path = require('path'); const crypto = require('crypto'); const OutputLogger = require('./output-logger'); const SecurityHelper = require('./security-helper'); class SelfHostedConfig { /** * Load configuration from multiple sources * Priority: CLI args > Environment vars > Config file > Defaults */ static async loadConfiguration(args = {}) { const config = { accounts: {}, containerMappings: {}, defaultAccount: null, authentication: {} }; // 1. Load from config file if it exists const configFile = await this.loadConfigFile(); if (configFile) { Object.assign(config, configFile); } // 2. Load from environment variables this.loadFromEnvironment(config); // 3. Override with CLI arguments this.loadFromArgs(config, args); // 4. Apply container discovery if needed if (config.autoDiscover !== false) { await this.discoverContainers(config); } return config; } /** * Load configuration from file * Default location: ~/.optimizely-mcp/self-hosted.json */ static async loadConfigFile() { try { const configDir = path.join(process.env.HOME || process.env.USERPROFILE, '.optimizely-mcp'); const configPath = path.join(configDir, 'self-hosted.json'); const content = await fs.readFile(configPath, 'utf8'); return JSON.parse(content); } catch (error) { // Config file doesn't exist or is invalid - that's OK return null; } } /** * Load configuration from environment variables * Supports multiple storage accounts */ static loadFromEnvironment(config) { // Primary account from connection string if (process.env.AZURE_STORAGE_CONNECTION_STRING) { const parsed = this.parseConnectionString(process.env.AZURE_STORAGE_CONNECTION_STRING); config.accounts[parsed.accountName] = { accountName: parsed.accountName, accountKey: parsed.accountKey, endpointSuffix: parsed.endpointSuffix || 'core.windows.net' }; config.defaultAccount = parsed.accountName; } // Alternative: Account name + key if (process.env.AZURE_STORAGE_ACCOUNT && process.env.AZURE_STORAGE_KEY) { const accountName = process.env.AZURE_STORAGE_ACCOUNT; config.accounts[accountName] = { accountName: accountName, accountKey: process.env.AZURE_STORAGE_KEY, endpointSuffix: process.env.AZURE_STORAGE_ENDPOINT || 'core.windows.net' }; if (!config.defaultAccount) { config.defaultAccount = accountName; } } // Service Principal authentication if (process.env.AZURE_CLIENT_ID && process.env.AZURE_CLIENT_SECRET && process.env.AZURE_TENANT_ID) { config.authentication.servicePrincipal = { clientId: process.env.AZURE_CLIENT_ID, clientSecret: process.env.AZURE_CLIENT_SECRET, tenantId: process.env.AZURE_TENANT_ID }; } // Container mappings (comma-separated) // Format: AZURE_LOG_CONTAINERS=logs,application-logs,web-logs if (process.env.AZURE_LOG_CONTAINERS) { config.containerMappings.logs = process.env.AZURE_LOG_CONTAINERS.split(',').map(c => c.trim()); } if (process.env.AZURE_MEDIA_CONTAINERS) { config.containerMappings.media = process.env.AZURE_MEDIA_CONTAINERS.split(',').map(c => c.trim()); } // Support for multiple accounts // Format: AZURE_STORAGE_ACCOUNTS=account1:key1,account2:key2 if (process.env.AZURE_STORAGE_ACCOUNTS) { const accounts = process.env.AZURE_STORAGE_ACCOUNTS.split(','); accounts.forEach(accountStr => { const [name, key] = accountStr.split(':'); if (name && key) { config.accounts[name.trim()] = { accountName: name.trim(), accountKey: key.trim(), endpointSuffix: 'core.windows.net' }; } }); } } /** * Load configuration from CLI arguments */ static loadFromArgs(config, args) { // Connection string takes precedence if (args.connectionString) { const parsed = this.parseConnectionString(args.connectionString); config.accounts[parsed.accountName] = { accountName: parsed.accountName, accountKey: parsed.accountKey, endpointSuffix: parsed.endpointSuffix || 'core.windows.net' }; config.defaultAccount = parsed.accountName; } // Individual account parameters if (args.storageAccount && args.accountKey) { config.accounts[args.storageAccount] = { accountName: args.storageAccount, accountKey: args.accountKey, endpointSuffix: args.endpointSuffix || 'core.windows.net' }; if (!config.defaultAccount) { config.defaultAccount = args.storageAccount; } } // Service Principal if (args.clientId && args.clientSecret && args.tenantId) { config.authentication.servicePrincipal = { clientId: args.clientId, clientSecret: args.clientSecret, tenantId: args.tenantId }; } // Container mappings if (args.logContainers) { config.containerMappings.logs = Array.isArray(args.logContainers) ? args.logContainers : args.logContainers.split(',').map(c => c.trim()); } if (args.mediaContainers) { config.containerMappings.media = Array.isArray(args.mediaContainers) ? args.mediaContainers : args.mediaContainers.split(',').map(c => c.trim()); } // Specific account selection if (args.useAccount) { config.defaultAccount = args.useAccount; } } /** * Parse Azure Storage connection string */ static parseConnectionString(connectionString) { const parts = {}; connectionString.split(';').forEach(part => { const [key, value] = part.split('='); if (key && value) { parts[key] = value; } }); if (!parts.AccountName) { throw new Error('Invalid connection string: missing AccountName'); } return { accountName: parts.AccountName, accountKey: parts.AccountKey, endpointSuffix: parts.EndpointSuffix || 'core.windows.net', protocol: parts.DefaultEndpointsProtocol || 'https' }; } /** * Generate a SAS token from account key * This creates a time-limited, scoped access token */ static generateSasToken(accountName, accountKey, containerName, permissions = 'rl', expiryHours = 24) { const start = new Date(); const expiry = new Date(start.getTime() + (expiryHours * 60 * 60 * 1000)); // Format dates for Azure const startStr = start.toISOString().slice(0, 19) + 'Z'; const expiryStr = expiry.toISOString().slice(0, 19) + 'Z'; // Build the string to sign const stringToSign = [ permissions, // sp (permissions) startStr, // st (start time) expiryStr, // se (expiry time) `/blob/${accountName}/${containerName}`, // sr (resource) '', // identifier '', // IP 'https', // protocol '2021-08-06', // version 'b', // resource type (blob) '', // snapshot time '', // encryption scope '', // cache control '', // content disposition '', // content encoding '', // content language '' // content type ].join('\n'); // Sign with HMAC-SHA256 const key = Buffer.from(accountKey, 'base64'); const signature = crypto .createHmac('sha256', key) .update(stringToSign, 'utf8') .digest('base64'); // Build SAS token const sasToken = new URLSearchParams({ 'sv': '2021-08-06', 'ss': 'b', 'srt': 'co', 'sp': permissions, 'se': expiryStr, 'st': startStr, 'spr': 'https', 'sig': signature }).toString(); return sasToken; } /** * Discover containers in storage accounts * This helps map what containers exist without hardcoding */ static async discoverContainers(config) { if (!config.accounts || Object.keys(config.accounts).length === 0) { return; } // For each configured account, we could list containers // This would require account-level access // For MVP, we'll rely on explicit configuration // Auto-detect common patterns if not configured if (!config.containerMappings.logs || config.containerMappings.logs.length === 0) { // Common log container patterns config.containerMappings.logs = [ 'logs', 'application-logs', 'web-logs', 'app-logs', 'site-logs', 'azure-application-logs', 'azure-web-logs', 'insights-logs-appserviceconsolelogs', 'insights-logs-appservicehttplogs' ]; } if (!config.containerMappings.media || config.containerMappings.media.length === 0) { // Common media container patterns config.containerMappings.media = [ 'media', 'assets', 'blobs', 'mysitemedia', 'content', 'uploads', 'files' ]; } } /** * Save configuration to file for persistence */ static async saveConfiguration(config) { try { const configDir = path.join(process.env.HOME || process.env.USERPROFILE, '.optimizely-mcp'); await fs.mkdir(configDir, { recursive: true }); const configPath = path.join(configDir, 'self-hosted.json'); // Mask sensitive data before saving const safeConfig = this.maskSensitiveConfig(config); await fs.writeFile(configPath, JSON.stringify(safeConfig, null, 2)); OutputLogger.info(`✅ Configuration saved to ${configPath}`); return true; } catch (error) { OutputLogger.error(`Failed to save configuration: ${error.message}`); return false; } } /** * Mask sensitive information in config */ static maskSensitiveConfig(config) { const masked = JSON.parse(JSON.stringify(config)); // Deep clone // Mask account keys if (masked.accounts) { for (const account of Object.values(masked.accounts)) { if (account.accountKey) { account.accountKey = SecurityHelper.maskSecret(account.accountKey); } } } // Mask service principal secret if (masked.authentication?.servicePrincipal?.clientSecret) { masked.authentication.servicePrincipal.clientSecret = SecurityHelper.maskSecret( masked.authentication.servicePrincipal.clientSecret ); } return masked; } /** * Get the appropriate storage account for a container type */ static getAccountForContainer(config, containerType) { // For now, use the default account // In future, could map specific containers to specific accounts if (!config.defaultAccount) { throw new Error('No storage account configured'); } const account = config.accounts[config.defaultAccount]; if (!account) { throw new Error(`Storage account '${config.defaultAccount}' not found in configuration`); } return account; } /** * List all containers that match a type (logs, media, etc) */ static getContainersForType(config, type) { if (!config.containerMappings || !config.containerMappings[type]) { return []; } return config.containerMappings[type]; } } module.exports = SelfHostedConfig;

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/JaxonDigital/optimizely-dxp-mcp'

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