list_projects
Discover available Optimizely DXP projects with names, IDs, hosting types, and active status to prepare for project switching.
Instructions
š List all configured projects in multi-project setup. REAL-TIME: <1s. Returns project names, IDs, hosting types (DXP/self-hosted), and active status. Use to discover available projects before switch_project() call. No parameters. Returns array of project summaries.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| limit | No | Max results to return (1-100) | |
| offset | No | Pagination offset |
Implementation Reference
- lib/tools/project-tools.ts:717-889 (handler)Main handler function for the 'list_projects' tool. Parses environment variables and dynamic configs to list all configured Optimizely projects with diagnostics, validation status (DXP/Self-hosted/Unknown), pagination support, security sanitization, and structured JSON output for automation.static async listProjects(_args: any = {}): Promise<any> { try { // DXP-76-3: Add pagination support const { limit = 20, offset = 0 } = _args; // List all projects const allProjects = this.getConfiguredProjects(); const diagnostics = this.validateConfiguration(); // Apply pagination const total = allProjects.length; const projects = allProjects.slice(offset, offset + limit); if (allProjects.length === 0) { return ResponseBuilder.formatResponse({ success: false, message: 'No projects configured yet', details: [ 'ā ļø No Optimizely projects found.', '', '**Quick Start - Just provide credentials when using any command:**', '', 'Simply include ALL these parameters with your first command:', '⢠projectName: "Your Project Name" (e.g., "Production", "Staging")', '⢠projectId: "REPLACE_WITH_UUID"', '⢠apiKey: "REPLACE_WITH_ACTUAL_KEY"', '⢠apiSecret: "REPLACE_WITH_ACTUAL_SECRET"', '', '**Example:**', '"List deployments for Production with projectName Production, projectId abc-123, apiKey SAMPLE_API_KEY, apiSecret SAMPLE_API_SECRET"', '', '**After the first use:**', 'The project will be auto-registered and you can simply say:', '"List deployments for Production"', '"Deploy on Production"', '', 'š” **Why Project Names Matter:**', 'Project names make it easy to reference your projects without remembering UUIDs!', '', '**Alternative: Pre-configure projects:**', 'Set environment variables like:', 'PRODUCTION="id=uuid;key=value;secret=value"', 'STAGING="id=uuid;key=value;secret=value"', 'ACME_CORP="id=uuid;key=value;secret=value"' ].join('\n') }); } const sections: string[] = []; // Header (DXP-76-3: Show pagination info) const paginationInfo = total > limit ? ` (showing ${offset + 1}-${Math.min(offset + limit, total)} of ${total})` : ''; sections.push(`š Configured Optimizely Projects${paginationInfo}`); sections.push('=' .repeat(50)); // List each project (name first for easier reference) // DXP-76-3: Adjust numbering for pagination offset projects.forEach((project, index) => { const sanitized = SecurityHelper.sanitizeObject(project); const actualIndex = offset + index; const defaultLabel = (actualIndex === 0) ? ' ā (Default)' : ''; const dynamicLabel = project.addedAt ? ' š (Added)' : ''; // Check for configuration issues for this project const projectDiag = diagnostics.projects.find(p => p.name === project.name); const hasErrors = projectDiag && projectDiag.errors.length > 0; const hasWarnings = projectDiag && projectDiag.warnings.length > 0; sections.push(''); let typeLabel = ' āļø (DXP PaaS)'; if (project.isSelfHosted) { typeLabel = ' š (Self-Hosted)'; } else if (project.isUnknown) { typeLabel = ' ā (Unknown - Needs Config)'; } sections.push(`${actualIndex + 1}. **${project.name}**${typeLabel}${defaultLabel}${dynamicLabel}${hasErrors ? ' ā ļø' : ''}`); sections.push(` Project ID: ${project.projectId}`); if (project.isUnknown) { // Show Unknown project info and guidance sections.push(` Type: Unknown (Paths Only)`); sections.push(` Status: ā ļø Needs Configuration`); if (project.blobPath) sections.push(` Blob Path: ${project.blobPath}`); if (project.logPath) sections.push(` Log Path: ${project.logPath}`); if (project.dbPath) sections.push(` DB Path: ${project.dbPath}`); sections.push(` š” To configure: Add connectionString (self-hosted) or id/key/secret (DXP)`); } else if (project.isSelfHosted) { // Show connection string status for self-hosted sections.push(` Type: Self-Hosted Azure`); sections.push(` Connection String: ${sanitized.connectionString ? 'ā Configured' : 'ā Not configured'}`); if (project.blobPath) sections.push(` Blob Path: ${project.blobPath}`); if (project.logPath) sections.push(` Log Path: ${project.logPath}`); } else { // Show API credentials for DXP PaaS sections.push(` Type: DXP PaaS`); sections.push(` API Key: ${sanitized.apiKey ? sanitized.apiKey : 'ā Not configured'}`); sections.push(` API Secret: ${sanitized.apiSecret ? 'ā Configured' : 'ā Not configured'}`); } // Show configuration errors/warnings if (hasErrors) { projectDiag!.errors.forEach(err => { sections.push(` ā Error: ${err}`); }); } if (hasWarnings) { projectDiag!.warnings.forEach(warn => { sections.push(` ā ļø Warning: ${warn}`); }); } if (project.lastUsed) { const lastUsed = new Date(project.lastUsed); const now = new Date(); const diffHours = Math.floor((now.getTime() - lastUsed.getTime()) / (1000 * 60 * 60)); if (diffHours < 1) { sections.push(` Last used: Just now`); } else if (diffHours < 24) { sections.push(` Last used: ${diffHours} hour${diffHours > 1 ? 's' : ''} ago`); } else { const diffDays = Math.floor(diffHours / 24); sections.push(` Last used: ${diffDays} day${diffDays > 1 ? 's' : ''} ago`); } } }); // Footer with usage instructions sections.push(''); sections.push('=' .repeat(50)); sections.push('š” Usage Tips:'); sections.push('⢠Use project name or ID in commands'); sections.push('⢠Example: "Deploy on Project 1"'); sections.push('⢠Example: "List deployments for ' + (projects[0]?.name || 'project-name') + '"'); // DXP-66: Build structured data for automation tools // DXP-76-3: Add pagination metadata const structuredData = { totalProjects: total, projects: projects.map((project, index) => ({ name: project.name, projectId: project.projectId, type: project.isSelfHosted ? 'self-hosted' : project.isUnknown ? 'unknown' : 'dxp', isDefault: (offset + index) === 0, isDynamic: !!project.addedAt, hasConnectionString: !!project.connectionString, hasApiCredentials: !!(project.apiKey && project.apiSecret), blobPath: project.blobPath || null, logPath: project.logPath || null, dbPath: project.dbPath || null, lastUsed: project.lastUsed || null })), pagination: { total, limit, offset, hasMore: (offset + limit) < total } }; return ResponseBuilder.successWithStructuredData( structuredData, sections.join('\n') ); } catch (error: any) { return ResponseBuilder.formatResponse({ success: false, message: 'Failed to list projects', error: error.message }); } }
- lib/tools/project-tools.ts:186-557 (helper)Core helper function that discovers and parses all project configurations from environment variables (DXP API keys, self-hosted connection strings, unknown placeholders) and dynamic in-memory additions. Handles validation, error logging, and merging.static getConfiguredProjects(): ProjectConfig[] { // Dynamic configurations are kept in memory only for current session const projects: ProjectConfig[] = []; const configErrors: ConfigError[] = []; // Check ALL environment variables for our specific format // Any env var with format: "id=uuid;key=value;secret=value" is treated as an API key configuration // Examples: // ACME="id=uuid;key=value;secret=value" // PRODUCTION="id=uuid;key=value;secret=value" // CLIENT_A_STAGING="id=uuid;key=value;secret=value" // DEBUG: Log all environment variables that contain our format const relevantEnvVars = Object.keys(process.env).filter(key => { const value = process.env[key]; return value && typeof value === 'string' && ((value.includes('id=') && value.includes('key=') && value.includes('secret=')) || value.startsWith('DefaultEndpointsProtocol=') || ((value.includes('blobPath=') || value.includes('logPath=')) && !value.includes('id=') && !value.includes('key=') && !value.includes('secret='))); }); OutputLogger.debug(`Checking environment variables...`); OutputLogger.debug(`Found ${relevantEnvVars.length} relevant env vars (DXP, self-hosted, and unknown):`, relevantEnvVars); Object.keys(process.env).forEach(key => { const value = process.env[key]; // Skip if not a string if (typeof value !== 'string') { return; } // Check if empty string (placeholder project) // Only treat as project if name looks like a project name if (value === '') { // Skip if this doesn't look like a project name // Project names typically follow patterns like: // - "ACME-int", "CONTOSO-prod", "FABRIKAM-staging" (project-environment) // - "DEMO-self", "TEST-local" (project-type) // - Single short words like "zilch", "test", "demo" const hasProjectPattern = // Pattern: WORD-env (like ACME-int, CONTOSO-prod) key.match(/^[A-Z0-9]+-(?:int|prod|staging|test|dev|qa|uat|demo|local|self)$/i) || // Pattern: Short single word (max 10 chars) (key.match(/^[A-Z0-9]+$/i) && key.length <= 10) || // Explicitly starts with common project prefixes key.match(/^(?:TEST|DEMO|DEV|PROD|STAGING|QA)[-_]/i); if (!hasProjectPattern) { return; } // Create Unknown placeholder project const projectName = key.replace(/_/g, ' '); const projectConfig: ProjectConfig = { name: projectName, projectId: `unknown-${projectName.toLowerCase().replace(/\s+/g, '-')}`, isUnknown: true, projectType: 'unknown', needsConfiguration: true, configurationHint: 'Empty project - add connectionString for self-hosted or id/key/secret for DXP', environments: ['Unknown'], configSource: 'environment' }; projects.push(projectConfig); return; } // Check if this looks like our API key format OR a connection string OR self-hosted paths // Must contain either: // 1. DXP format: (id=, key=, secret=) // 2. Azure connection string: (DefaultEndpointsProtocol=) // 3. Self-hosted with paths only: (blobPath= or logPath=) but no id/key/secret const hasDxpFormat = value.includes('id=') && value.includes('key=') && value.includes('secret='); const hasConnectionString = value.startsWith('DefaultEndpointsProtocol='); const hasSelfHostedPaths = (value.includes('blobPath=') || value.includes('logPath=')) && !value.includes('id=') && !value.includes('key=') && !value.includes('secret='); const hasCorrectFormat = hasDxpFormat || hasConnectionString || hasSelfHostedPaths; if (!hasCorrectFormat) { return; } // Use the environment variable name as the project name (underscores become spaces) const projectName = key.replace(/_/g, ' '); try { // Check if this is a raw Azure connection string (self-hosted mode) if (value.startsWith('DefaultEndpointsProtocol=')) { // Extract connection string and any additional parameters // Format: DefaultEndpointsProtocol=...;EndpointSuffix=core.windows.net;blobPath=/path;logPath=/path // Find where the connection string ends (after EndpointSuffix) const endpointMatch = value.match(/EndpointSuffix=[^;]+/); let connectionString = value; let additionalParams: Record<string, string> = {}; if (endpointMatch) { const endIndex = value.indexOf(endpointMatch[0]) + endpointMatch[0].length; connectionString = value.substring(0, endIndex); // Parse any additional parameters after the connection string const remaining = value.substring(endIndex); if (remaining) { const extraParts = remaining.split(';').filter(p => p.trim()); extraParts.forEach(part => { const [key, val] = part.split('='); if (key && val) { additionalParams[key] = val; } }); } } const projectConfig: ProjectConfig = { name: projectName, projectId: `self-hosted-${projectName.toLowerCase().replace(/\s+/g, '-')}`, apiKey: '', apiSecret: '', connectionString: connectionString, isSelfHosted: true, environments: ['Production'], // Self-hosted typically has one environment configSource: 'environment' }; // Add optional paths if provided if (additionalParams.blobPath) { projectConfig.blobPath = additionalParams.blobPath; } if (additionalParams.logPath) { projectConfig.logPath = additionalParams.logPath; } if (additionalParams.dbPath) { projectConfig.dbPath = additionalParams.dbPath; } projects.push(projectConfig); return; } // Otherwise parse semicolon-separated key=value pairs for DXP projects const params: Record<string, string> = {}; const parts = value.split(';').filter(p => p.trim()); if (parts.length === 0) { // Empty configuration - treat as Unknown project placeholder const projectConfig: ProjectConfig = { name: projectName, projectId: `unknown-${projectName.toLowerCase().replace(/\s+/g, '-')}`, isUnknown: true, projectType: 'unknown', needsConfiguration: true, configurationHint: 'Empty project - add connectionString for self-hosted or id/key/secret for DXP', environments: ['Unknown'], configSource: 'environment' }; projects.push(projectConfig); return; } parts.forEach(param => { const equalIndex = param.indexOf('='); if (equalIndex === -1) { configErrors.push({ project: projectName, error: `Invalid parameter format: "${param}" (expected key=value)`, variable: key }); return; } const paramKey = param.substring(0, equalIndex).trim(); const paramValue = param.substring(equalIndex + 1).trim(); if (!paramKey || !paramValue) { configErrors.push({ project: projectName, error: `Empty key or value in parameter: "${param}"`, variable: key }); return; } params[paramKey] = paramValue; }); // Extract credentials using standard format let projectId = params.id; // Changed to let to allow reassignment const apiKey = params.key; const apiSecret = params.secret; let connectionString = params.connectionString || params.connStr; // Check if this is a path-only project (has paths but no DXP credentials or connection string) const isPathOnlyProject = (params.blobPath || params.logPath || params.dbPath) && !params.id && !params.key && !params.secret && !connectionString; // Determine project type and handle accordingly if (connectionString) { // Self-hosted mode with connection string if (!projectId) { // Generate a simple ID from the project name const projectIdBase = projectName.toLowerCase().replace(/\s+/g, '-'); projectId = `self-hosted-${projectIdBase}`; } } else if (isPathOnlyProject) { // Unknown type - has paths but no clear indication of type if (!projectId) { // Generate a simple ID from the project name const projectIdBase = projectName.toLowerCase().replace(/\s+/g, '-'); projectId = `unknown-${projectIdBase}`; } OutputLogger.debug(`Unknown project type "${projectName}" configured with paths only`); } else if (!params.id || !params.key || !params.secret) { // DXP mode - needs full API credentials const missingFields: string[] = []; if (!projectId) missingFields.push('id'); if (!apiKey) missingFields.push('key'); if (!apiSecret) missingFields.push('secret'); if (missingFields.length > 0) { configErrors.push({ project: projectName, error: `Missing required fields: ${missingFields.join(', ')}`, variable: key, hint: `Format: "id=<uuid>;key=<key>;secret=<secret>"` }); return; } } // Validate UUID format for project ID (skip for self-hosted and unknown) const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; const isSelfHostedId = projectId && projectId.startsWith('self-hosted-'); const isUnknownId = projectId && projectId.startsWith('unknown-'); if (!isSelfHostedId && !isUnknownId && !uuidRegex.test(projectId!)) { configErrors.push({ project: projectName, error: `Invalid project ID format: "${projectId}"`, variable: key, hint: `Project ID should be a UUID like: abc12345-1234-5678-9abc-def123456789` }); } // Validate environments if specified if (params.environments) { const validEnvs = ['Integration', 'Preproduction', 'Production']; const envs = params.environments.split(',').map(e => e.trim()); const invalidEnvs = envs.filter(e => !validEnvs.includes(e)); if (invalidEnvs.length > 0) { configErrors.push({ project: projectName, error: `Invalid environments: ${invalidEnvs.join(', ')}`, variable: key, hint: `Valid environments are: Integration, Preproduction, Production` }); } } // Add project if validation passed const projectConfig: ProjectConfig = { name: projectName, projectId: projectId!, apiKey: apiKey || '', apiSecret: apiSecret || '', environments: params.environments ? params.environments.split(',').map(e => e.trim()) : ['Integration', 'Preproduction', 'Production'], configSource: 'environment' }; // Determine project type based on available credentials if (connectionString) { // Self-hosted with connection string projectConfig.connectionString = connectionString; projectConfig.isSelfHosted = true; projectConfig.projectType = 'self-hosted'; } else if (isPathOnlyProject) { // Unknown type - has paths but no credentials projectConfig.isUnknown = true; projectConfig.projectType = 'unknown'; projectConfig.needsConfiguration = true; // For unknown projects, we need to guide users to add credentials projectConfig.configurationHint = 'Add connectionString for self-hosted or id/key/secret for DXP'; } else if (apiKey && apiSecret) { // DXP PaaS project projectConfig.projectType = 'dxp-paas'; } // Add compact configuration fields if present if (params.blobPath) { projectConfig.blobPath = params.blobPath; } if (params.dbPath) { projectConfig.dbPath = params.dbPath; } if (params.logPath) { projectConfig.logPath = params.logPath; } if (params.telemetry) { projectConfig.telemetry = params.telemetry.toLowerCase() === 'true'; } projects.push(projectConfig); } catch (error: any) { configErrors.push({ project: projectName, error: `Failed to parse configuration: ${error.message}`, variable: key }); } }); // Log configuration errors if any if (configErrors.length > 0) { console.error('\nā ļø Configuration Errors Found:'); configErrors.forEach(err => { console.error(`\n Project: ${err.project}`); console.error(` Variable: ${err.variable}`); console.error(` Error: ${err.error}`); if (err.hint) { console.error(` Hint: ${err.hint}`); } if (err.value) { console.error(` Value: ${err.value.substring(0, 50)}...`); } }); console.error('\n'); } // Add dynamically added configurations this.dynamicConfigurations.forEach(dynConfig => { // Check if this dynamic config should replace an existing project const existingIndex = projects.findIndex(p => { // Match by original name (for renames) if (dynConfig.originalName && p.name === dynConfig.originalName) { return true; } // Match by current name if (p.name === dynConfig.name || p.name.toLowerCase() === dynConfig.name.toLowerCase()) { return true; } // Match by project ID if (p.projectId === dynConfig.projectId) { return true; } return false; }); if (existingIndex >= 0) { // Replace existing project with dynamic configuration (upgrade/rename scenario) projects[existingIndex] = dynConfig; } else if (!projects.find(p => p.projectId === dynConfig.projectId)) { // Only add if not already in list (avoid duplicates by ID) projects.push(dynConfig); } }); // First project is always the default (simplified logic) // DEBUG: Log final projects found OutputLogger.debug('Final projects found:'); projects.forEach((p, i) => { OutputLogger.debug(` ${i + 1}. Name: "${p.name}", ID: ${p.projectId || 'undefined'}, Source: ${p.configSource || 'unknown'}`); }); return projects; }
- lib/tools/project-tools.ts:588-662 (helper)Validates all discovered projects, categorizes by type (DXP PaaS, Self-hosted, Unknown), checks credential validity, detects placeholders/errors, and returns comprehensive diagnostics used by list_projects.static validateConfiguration(): Diagnostics { const projects = this.getConfiguredProjects(); const diagnostics: Diagnostics = { valid: true, projectCount: projects.length, hasDefault: false, errors: [], warnings: [], projects: [] }; // First project is always the default if (projects.length > 0) { diagnostics.hasDefault = true; } // Validate each project projects.forEach(project => { const projectDiag: ProjectDiag = { name: project.name, valid: true, errors: [], warnings: [] }; // Check credentials based on project type if (project.isUnknown) { // Unknown projects need configuration guidance projectDiag.warnings.push('Project type unknown - needs connection string (self-hosted) or API credentials (DXP)'); projectDiag.warnings.push('Currently using local paths only'); // Unknown projects are valid but need configuration for full functionality projectDiag.valid = true; } else if (project.isSelfHosted) { // Self-hosted projects use connection strings if (!project.connectionString || project.connectionString.length < 50) { projectDiag.errors.push('Connection string appears invalid or too short'); projectDiag.valid = false; } } else { // DXP projects use API Key/Secret if (!project.apiKey || project.apiKey.length < 20) { projectDiag.errors.push('API Key appears invalid or too short'); projectDiag.valid = false; } if (!project.apiSecret || project.apiSecret.length < 20) { projectDiag.errors.push('API Secret appears invalid or too short'); projectDiag.valid = false; } // Check for common mistakes if (project.apiKey && (project.apiKey.includes('REPLACE_WITH') || project.apiKey.includes('PLACEHOLDER') || project.apiKey.includes('SAMPLE'))) { projectDiag.errors.push('API Key is a placeholder value'); projectDiag.valid = false; } if (project.apiSecret && (project.apiSecret.includes('REPLACE_WITH') || project.apiSecret.includes('PLACEHOLDER') || project.apiSecret.includes('SAMPLE'))) { projectDiag.errors.push('API Secret is a placeholder value'); projectDiag.valid = false; } } // Removed warning about environment names in project names // This is actually a valid use case - some organizations create // separate API keys per environment for security reasons if (projectDiag.errors.length > 0) { diagnostics.valid = false; } diagnostics.projects.push(projectDiag); }); return diagnostics; }
- lib/utils/tool-availability-matrix.ts:96-100 (registration)Tool availability registration in the matrix that defines hosting compatibility (all types) and metadata for 'list_projects'.'list_projects': { hostingTypes: ['dxp-paas', 'dxp-saas', 'self-hosted', 'unknown'], category: 'Project Management', description: 'List all configured projects' },
- lib/nlp-parser.ts:182-183 (helper)NLP pattern matching rules that route natural language queries about projects to the list_projects tool.{ pattern: /^(what|which|show).*(current.*project)/i, tool: 'list_projects', category: 'info', options: { current: true } }, { pattern: /^(list|show).*(project)/i, tool: 'list_projects', category: 'info' },