update_project
Modify Optimizely DXP project configuration settings including credentials, name, and environment parameters to maintain operational continuity across deployment environments.
Instructions
✏️ Update project configuration settings. INSTANT: <1s. Modifies stored credentials, project name, or environment settings. Changes persist for session. Use with caution - invalid credentials will break subsequent operations. Required: projectName, updates (object with fields to change). Returns updated project config. Test with test_connection() after updating.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| projectName | No | ||
| projectId | No | ||
| renameTo | No | ||
| apiKey | No | ||
| apiSecret | No | ||
| connectionString | No | ||
| blobPath | No | ||
| dbPath | No | ||
| logPath | No | ||
| makeDefault | No | Set this project as default for getCurrentProject() - auto-enabled when providing inline credentials |
Implementation Reference
- lib/tools/project-tools.ts:1468-1651 (handler)Core handler for the 'update_project' tool. Updates project configuration including renaming, credentials (API key/secret or connection string), paths (blob/db/log), and settings (make default). Handles project type conversions (DXP PaaS ↔ Self-Hosted ↔ Unknown). Uses dynamic in-memory configuration storage.static updateProject(args: UpdateProjectArgs): any { const { projectName, projectId, // Rename renameTo, // Credentials apiKey, apiSecret, connectionString, // Paths blobPath, dbPath, logPath, // Settings makeDefault } = args; // Find the project to update const projects = this.getConfiguredProjects(); const project = projects.find(p => p.name === projectName || p.name?.toLowerCase() === projectName?.toLowerCase() || p.projectId === projectId ); if (!project) { // If no existing project, create new one if we have enough info if (projectName && (apiKey || connectionString)) { return this.getProjectInfo(args as GetProjectArgs); // Use existing creation logic } return ResponseBuilder.formatResponse({ success: false, message: `Project '${projectName || projectId}' not found`, details: 'Specify an existing project to update or provide credentials to create a new one' }); } // Build updated configuration const updatedConfig: ProjectConfig = { ...project }; let changes: string[] = []; // Handle rename if (renameTo && renameTo !== project.name) { updatedConfig.name = renameTo; updatedConfig.originalName = project.name; changes.push(`Renamed from '${project.name}' to '${renameTo}'`); } // Handle credentials update if (apiKey && apiKey !== project.apiKey) { updatedConfig.apiKey = apiKey; changes.push('Updated API key'); // If providing API credentials, this becomes a DXP project if (apiSecret) { if (project.projectType === 'self-hosted' || project.isSelfHosted) { changes.push('Converted from Self-Hosted to DXP PaaS'); } updatedConfig.projectType = 'dxp-paas'; updatedConfig.isSelfHosted = false; delete updatedConfig.connectionString; updatedConfig.environments = ['Integration', 'Preproduction', 'Production']; } } if (apiSecret && apiSecret !== project.apiSecret) { updatedConfig.apiSecret = apiSecret; changes.push('Updated API secret'); // If providing API credentials, this becomes a DXP project if (apiKey || updatedConfig.apiKey) { if (project.projectType === 'self-hosted' || project.isSelfHosted) { changes.push('Converted from Self-Hosted to DXP PaaS'); } updatedConfig.projectType = 'dxp-paas'; updatedConfig.isSelfHosted = false; delete updatedConfig.connectionString; updatedConfig.environments = ['Integration', 'Preproduction', 'Production']; } } if (connectionString && connectionString !== project.connectionString) { updatedConfig.connectionString = connectionString; if (project.projectType === 'dxp-paas' || (!project.isSelfHosted && project.apiKey)) { changes.push('Converted from DXP PaaS to Self-Hosted'); } updatedConfig.isSelfHosted = true; updatedConfig.projectType = 'self-hosted'; updatedConfig.environments = ['Production']; // Remove DXP-specific fields delete updatedConfig.apiKey; delete updatedConfig.apiSecret; changes.push('Updated connection string'); } // Handle project ID update (for DXP projects) if (projectId && projectId !== project.projectId && !project.projectId.startsWith('unknown-')) { updatedConfig.projectId = projectId; changes.push('Updated project ID'); } // Handle paths if (blobPath && blobPath !== project.blobPath) { updatedConfig.blobPath = blobPath; changes.push('Updated blob path'); } if (dbPath && dbPath !== project.dbPath) { updatedConfig.dbPath = dbPath; changes.push('Updated database path'); } if (logPath && logPath !== project.logPath) { updatedConfig.logPath = logPath; changes.push('Updated log path'); } // Handle default setting if (makeDefault) { updatedConfig.isDefault = true; // Remove default from other projects this.dynamicConfigurations.forEach(c => { if (c.name !== updatedConfig.name) { c.isDefault = false; } }); // Set as last used project for getCurrentProject() resolution this.setLastUsedProject(updatedConfig.name); changes.push('Set as default project'); } // If project was Unknown and now has credentials, mark as configured if (project.isUnknown && (apiKey || connectionString)) { updatedConfig.isUnknown = false; updatedConfig.needsConfiguration = false; delete updatedConfig.configurationHint; if (apiKey) { updatedConfig.projectType = 'dxp-paas'; changes.push('Upgraded from Unknown to DXP PaaS'); } else if (connectionString) { updatedConfig.projectType = 'self-hosted'; changes.push('Upgraded from Unknown to Self-Hosted'); } } if (changes.length === 0) { return ResponseBuilder.formatResponse({ success: true, message: 'No changes to apply', details: `Project '${project.name}' is already up to date` }); } // Update timestamps updatedConfig.configSource = 'dynamic'; updatedConfig.lastUpdated = new Date().toISOString(); // Save the configuration this.addConfiguration(updatedConfig); // DXP-148 FIX: When credentials are provided inline, treat as implicit makeDefault // This ensures the most recently added/updated project becomes the default // Critical for multi-project n8n workflows where projects are added dynamically if ((apiKey && apiSecret) || connectionString) { this.setLastUsedProject(updatedConfig.name); OutputLogger.debug(`[DXP-148] Set '${updatedConfig.name}' as last used project (inline credentials provided)`); } // Build response const sections: string[] = []; sections.push(`✅ **Project Updated Successfully**`); sections.push(''); sections.push(`**${updatedConfig.name}**`); sections.push(`Type: ${updatedConfig.projectType === 'self-hosted' ? 'Self-Hosted' : 'DXP PaaS'}`); sections.push(''); sections.push('**Changes Applied:**'); changes.forEach(change => sections.push(`• ${change}`)); if (updatedConfig.blobPath || updatedConfig.dbPath || updatedConfig.logPath) { sections.push(''); sections.push('**Download Paths:**'); if (updatedConfig.blobPath) sections.push(`• Blobs: ${updatedConfig.blobPath}`); if (updatedConfig.dbPath) sections.push(`• Database: ${updatedConfig.dbPath}`); if (updatedConfig.logPath) sections.push(`• Logs: ${updatedConfig.logPath}`); } return ResponseBuilder.success(sections.join('\n')); }
- lib/tools/project-tools.ts:120-131 (schema)Type definition for input parameters to the update_project tool handler.interface UpdateProjectArgs { projectName?: string; projectId?: string; renameTo?: string; apiKey?: string; apiSecret?: string; connectionString?: string; blobPath?: string; dbPath?: string; logPath?: string; makeDefault?: boolean; }
- lib/utils/tool-availability-matrix.ts:111-115 (registration)Tool availability registration in the central matrix used for hosting-type based access control.'update_project': { hostingTypes: ['dxp-paas', 'dxp-saas', 'self-hosted', 'unknown'], category: 'Project Management', description: 'Update project configuration: rename, credentials, paths, or settings' },
- lib/tools/project-tools.ts:186-556 (helper)Key helper method that parses environment variables and dynamic configs into ProjectConfig objects, used by update_project for finding and updating projects.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:148-181 (helper)Helper for adding or updating dynamic (in-memory) project configurations, called by update_project to persist changes.static addConfiguration(configInfo: ProjectConfig): ProjectConfig { // Check if configuration already exists by ID or name // Special handling: If upgrading from Unknown, match by name only const existingIndex = this.dynamicConfigurations.findIndex(c => { // Match by name (case-insensitive) const nameMatch = c.name.toLowerCase() === configInfo.name.toLowerCase(); // If names match and one is Unknown being upgraded, that's a match if (nameMatch && (c.isUnknown || configInfo.wasUnknown)) { return true; } // Otherwise match by projectId or name return c.projectId === configInfo.projectId || c.name === configInfo.name; }); if (existingIndex >= 0) { // Update existing configuration this.dynamicConfigurations[existingIndex] = { ...this.dynamicConfigurations[existingIndex], ...configInfo, lastUsed: new Date().toISOString() }; } else { // Add new configuration this.dynamicConfigurations.push({ ...configInfo, addedAt: new Date().toISOString(), lastUsed: new Date().toISOString() }); } return configInfo; }