Skip to main content
Glama
generate-manifests.ts24.2 kB
/** * Generate Manifests Tool - AI-driven manifest generation with validation loop * Supports both capability-based solutions (K8s manifests) and Helm-based solutions (values.yaml) */ import { z } from 'zod'; import { ErrorHandler, ErrorCategory, ErrorSeverity } from '../core/error-handling'; import { DotAI, maybeGetFeedbackMessage } from '../core/index'; import { Logger } from '../core/error-handling'; import { ensureClusterConnection } from '../core/cluster-utils'; import { ManifestValidator, ValidationResult } from '../core/schema'; import * as fs from 'fs'; import * as path from 'path'; import { loadPrompt } from '../core/shared-prompt-loader'; import * as yaml from 'js-yaml'; import { GenericSessionManager } from '../core/generic-session-manager'; import type { SolutionData } from './recommend'; import { extractUserAnswers, addDotAiLabels } from '../core/solution-utils'; import { extractContentFromMarkdownCodeBlocks } from '../core/platform-utils'; import { isSolutionCRDAvailable } from '../core/crd-availability'; import { generateSolutionCR } from '../core/solution-cr'; import { HelmChartInfo } from '../core/helm-types'; import { buildHelmCommand, validateHelmDryRun, getHelmValuesPath, ensureTmpDir } from '../core/helm-utils'; // Tool metadata for direct MCP registration export const GENERATEMANIFESTS_TOOL_NAME = 'generateManifests'; export const GENERATEMANIFESTS_TOOL_DESCRIPTION = 'Generate final Kubernetes manifests from fully configured solution (ONLY after completing ALL stages: required, basic, advanced, and open)'; // Zod schema for MCP registration export const GENERATEMANIFESTS_TOOL_INPUT_SCHEMA = { solutionId: z.string().regex(/^sol-\d+-[a-f0-9]{8}$/).describe('The solution ID to generate manifests for (e.g., sol-1762983784617-9ddae2b8)'), interaction_id: z.string().optional().describe('INTERNAL ONLY - Do not populate. Used for evaluation dataset generation.') }; interface ErrorContext { attempt: number; previousManifests: string; validationResult: ValidationResult; } /** * Retrieve schemas for resources specified in the solution */ async function retrieveResourceSchemas(solution: any, dotAI: DotAI, logger: Logger): Promise<any> { try { // Extract resource references from solution const resourceRefs = (solution.resources || []).map((resource: any) => ({ kind: resource.kind, apiVersion: resource.apiVersion, group: resource.group })); if (resourceRefs.length === 0) { logger.warn('No resources found in solution for schema retrieval'); return {}; } logger.info('Retrieving schemas for solution resources', { resourceCount: resourceRefs.length, resources: resourceRefs.map((r: any) => `${r.kind}@${r.apiVersion}`) }); const schemas: any = {}; // Retrieve schema for each resource for (const resourceRef of resourceRefs) { try { const resourceKey = `${resourceRef.kind}.${resourceRef.apiVersion}`; logger.debug('Retrieving schema', { resourceKey }); // Use discovery engine to explain the resource const explanation = await dotAI.discovery.explainResource(resourceRef.kind); schemas[resourceKey] = { kind: resourceRef.kind, apiVersion: resourceRef.apiVersion, schema: explanation, timestamp: new Date().toISOString() }; logger.debug('Schema retrieved successfully', { resourceKey, schemaLength: explanation.length }); } catch (error) { logger.error('Failed to retrieve schema for resource', error as Error, { resource: resourceRef }); // Fail fast - if we can't get schemas, manifest generation will likely fail throw new Error(`Failed to retrieve schema for ${resourceRef.kind}: ${error instanceof Error ? error.message : String(error)}`); } } logger.info('All resource schemas retrieved successfully', { schemaCount: Object.keys(schemas).length }); return schemas; } catch (error) { logger.error('Schema retrieval failed', error as Error); throw new Error(`Failed to retrieve resource schemas: ${error instanceof Error ? error.message : String(error)}`); } } /** * Validate YAML syntax */ function validateYamlSyntax(yamlContent: string): { valid: boolean; error?: string } { try { yaml.loadAll(yamlContent); return { valid: true }; } catch (error) { return { valid: false, error: error instanceof Error ? error.message : 'Unknown YAML syntax error' }; } } /** * Validate manifests using multi-layer approach */ async function validateManifests(yamlPath: string): Promise<ValidationResult> { // First check if file exists if (!fs.existsSync(yamlPath)) { return { valid: false, errors: [`Manifest file not found: ${yamlPath}`], warnings: [] }; } // Read YAML content for syntax validation const yamlContent = fs.readFileSync(yamlPath, 'utf8'); // 1. YAML syntax validation const syntaxCheck = validateYamlSyntax(yamlContent); if (!syntaxCheck.valid) { return { valid: false, errors: [`YAML syntax error: ${syntaxCheck.error}`], warnings: [] }; } // 2. kubectl dry-run validation using ManifestValidator const validator = new ManifestValidator(); return await validator.validateManifest(yamlPath, { dryRunMode: 'server' }); } /** * Generate manifests using AI provider */ async function generateManifestsWithAI( solution: any, solutionId: string, dotAI: DotAI, logger: Logger, errorContext?: ErrorContext, dotAiLabels?: Record<string, string>, interaction_id?: string ): Promise<string> { // Retrieve schemas for solution resources const resourceSchemas = await retrieveResourceSchemas(solution, dotAI, logger); // Prepare template variables const solutionData = JSON.stringify(solution, null, 2); const previousAttempt = errorContext ? ` ### Generated Manifests: \`\`\`yaml ${errorContext.previousManifests} \`\`\` ` : 'None - this is the first attempt.'; const errorDetails = errorContext ? ` **Attempt**: ${errorContext.attempt} **Validation Errors**: ${errorContext.validationResult.errors.join(', ')} **Validation Warnings**: ${errorContext.validationResult.warnings.join(', ')} ` : 'None - this is the first attempt.'; // Prepare template variables const schemasData = JSON.stringify(resourceSchemas, null, 2); const labelsData = dotAiLabels ? JSON.stringify(dotAiLabels, null, 2) : '{}'; const aiPrompt = loadPrompt('capabilities-generation', { solution: solutionData, schemas: schemasData, previous_attempt: previousAttempt, error_details: errorDetails, labels: labelsData }); const isRetry = !!errorContext; logger.info('Generating manifests with AI', { isRetry, attempt: errorContext?.attempt, hasErrorContext: !!errorContext, solutionId }); // Get AI provider from dotAI const aiProvider = dotAI.ai; // Send prompt to AI const response = await aiProvider.sendMessage(aiPrompt, 'recommend-manifests-generation', { user_intent: solution.initialIntent || 'Kubernetes manifest generation', interaction_id: interaction_id }); // Extract YAML content from response // Use shared utility to extract from code blocks if wrapped const manifestContent = extractContentFromMarkdownCodeBlocks(response.content, 'yaml'); logger.info('AI manifest generation completed', { manifestLength: manifestContent.length, isRetry, solutionId }); return manifestContent; } /** * Error context for Helm validation retries */ interface HelmErrorContext { attempt: number; previousValues: string; validationResult: ValidationResult; } /** * Generate Helm values.yaml using AI provider */ async function generateHelmValuesWithAI( solution: any, solutionId: string, dotAI: DotAI, logger: Logger, errorContext?: HelmErrorContext, interaction_id?: string ): Promise<string> { // Fetch chart values.yaml for reference const chart: HelmChartInfo = solution.chart; const { valuesYaml } = await dotAI.schema.fetchHelmChartContent(chart); // Prepare template variables const solutionData = JSON.stringify(solution, null, 2); const previousAttempt = errorContext ? ` ### Generated Values: \`\`\`yaml ${errorContext.previousValues} \`\`\` ` : 'None - this is the first attempt.'; const errorDetails = errorContext ? ` **Attempt**: ${errorContext.attempt} **Validation Errors**: ${errorContext.validationResult.errors.join(', ')} **Validation Warnings**: ${errorContext.validationResult.warnings.join(', ')} ` : 'None - this is the first attempt.'; const aiPrompt = loadPrompt('helm-generation', { solution: solutionData, chart_values: valuesYaml || '# No default values available', previous_attempt: previousAttempt, error_details: errorDetails }); const isRetry = !!errorContext; logger.info('Generating Helm values with AI', { isRetry, attempt: errorContext?.attempt, hasErrorContext: !!errorContext, solutionId, chart: `${chart.repositoryName}/${chart.chartName}` }); // Get AI provider from dotAI const aiProvider = dotAI.ai; // Send prompt to AI const response = await aiProvider.sendMessage(aiPrompt, 'helm-values-generation', { user_intent: solution.intent || 'Helm chart installation', interaction_id: interaction_id }); // Extract YAML content from response const valuesContent = extractContentFromMarkdownCodeBlocks(response.content, 'yaml'); logger.info('AI Helm values generation completed', { valuesLength: valuesContent.length, isRetry, solutionId }); return valuesContent; } /** * Validate Helm installation using dry-run (wrapper around shared utility) */ async function validateHelmInstallation( chart: HelmChartInfo, releaseName: string, namespace: string, valuesPath: string, logger: Logger ): Promise<ValidationResult> { logger.info('Running Helm dry-run validation', { chart: `${chart.repositoryName}/${chart.chartName}`, releaseName, namespace }); const result = await validateHelmDryRun(chart, releaseName, namespace, valuesPath); if (result.success) { logger.info('Helm dry-run validation successful'); return { valid: true, errors: [], warnings: [] }; } logger.warn('Helm dry-run validation failed', { error: result.error }); return { valid: false, errors: [result.error || 'Unknown Helm validation error'], warnings: [] }; } /** * Handle Helm solution generation */ async function handleHelmGeneration( solution: any, solutionId: string, dotAI: DotAI, logger: Logger, requestId: string, interaction_id?: string ): Promise<{ content: { type: 'text'; text: string }[] }> { const maxAttempts = 10; const chart: HelmChartInfo = solution.chart; const userAnswers = extractUserAnswers(solution); // Extract release name and namespace from answers const releaseName = userAnswers.name; const namespace = userAnswers.namespace || 'default'; if (!releaseName) { throw ErrorHandler.createError( ErrorCategory.VALIDATION, ErrorSeverity.HIGH, 'Release name (name) is required for Helm installation', { operation: 'helm_generation', component: 'GenerateManifestsTool', requestId, suggestedActions: ['Ensure the "name" question was answered in the configuration'] } ); } // Prepare file paths using shared utilities ensureTmpDir(); const valuesPath = getHelmValuesPath(solutionId); // AI generation and validation loop let lastError: HelmErrorContext | undefined; for (let attempt = 1; attempt <= maxAttempts; attempt++) { logger.info('Helm values generation attempt', { attempt, maxAttempts, isRetry: attempt > 1, requestId, chart: `${chart.repositoryName}/${chart.chartName}` }); try { // Generate values.yaml with AI const valuesYaml = await generateHelmValuesWithAI( solution, solutionId, dotAI, logger, lastError, interaction_id ); // Save values to file fs.writeFileSync(valuesPath, valuesYaml, 'utf8'); logger.info('Helm values saved to file', { valuesPath, attempt, requestId }); // Save attempt for debugging const attemptPath = valuesPath.replace('.yaml', `_attempt_${attempt.toString().padStart(2, '0')}.yaml`); fs.writeFileSync(attemptPath, valuesYaml, 'utf8'); // Validate with helm dry-run const validation = await validateHelmInstallation( chart, releaseName, namespace, valuesPath, logger ); if (validation.valid) { logger.info('Helm validation successful', { attempt, valuesPath, requestId }); // Build user-friendly helm command with generic values file path // (internal valuesPath is used for actual execution, not shown to user) const helmCommand = buildHelmCommand(chart, releaseName, namespace, 'values.yaml'); // Check if we should show feedback message const feedbackMessage = maybeGetFeedbackMessage(); const response = { success: true, status: 'helm_command_generated', solutionId: solutionId, solutionType: 'helm', helmCommand: helmCommand, valuesYaml: valuesYaml, chart: { repository: chart.repository, repositoryName: chart.repositoryName, chartName: chart.chartName, version: chart.version }, releaseName: releaseName, namespace: namespace, validationAttempts: attempt, timestamp: new Date().toISOString(), ...(feedbackMessage ? { message: feedbackMessage } : {}) }; return { content: [{ type: 'text' as const, text: JSON.stringify(response, null, 2) }] }; } // Validation failed, prepare error context for next attempt lastError = { attempt, previousValues: valuesYaml, validationResult: validation }; logger.warn('Helm validation failed', { attempt, maxAttempts, validationErrors: validation.errors, validationWarnings: validation.warnings, requestId }); } catch (error) { logger.error('Error during Helm values generation attempt', error as Error); if (attempt === maxAttempts) { throw error; } // Prepare error context for retry lastError = { attempt, previousValues: lastError?.previousValues || '', validationResult: { valid: false, errors: [error instanceof Error ? error.message : String(error)], warnings: [] } }; } } // All attempts failed throw new Error(`Failed to generate valid Helm values after ${maxAttempts} attempts. Last errors: ${lastError?.validationResult.errors.join(', ')}`); } /** * Direct MCP tool handler for generateManifests functionality */ export async function handleGenerateManifestsTool( args: { solutionId: string; interaction_id?: string }, dotAI: DotAI, logger: Logger, requestId: string ): Promise<{ content: { type: 'text'; text: string }[] }> { return await ErrorHandler.withErrorHandling( async () => { const maxAttempts = 10; logger.debug('Handling generateManifests request', { requestId, solutionId: args?.solutionId }); // Input validation is handled automatically by MCP SDK with Zod schema // args are already validated and typed when we reach this point // Initialize session manager const sessionManager = new GenericSessionManager<SolutionData>('sol'); logger.debug('Session manager initialized', { requestId }); // Ensure cluster connectivity before proceeding await ensureClusterConnection(dotAI, logger, requestId, 'GenerateManifestsTool'); // Load solution session const session = sessionManager.getSession(args.solutionId); if (!session) { throw ErrorHandler.createError( ErrorCategory.VALIDATION, ErrorSeverity.HIGH, `Solution not found: ${args.solutionId}`, { operation: 'solution_loading', component: 'GenerateManifestsTool', requestId, input: { solutionId: args.solutionId }, suggestedActions: [ 'Verify the solution ID is correct', 'Ensure the solution was created by the recommend tool', 'Ensure all configuration stages were completed', 'Check that the session has not expired' ] } ); } const solution = session.data; logger.debug('Solution loaded successfully', { solutionId: args.solutionId, solutionType: solution.type, hasQuestions: !!solution.questions, primaryResources: solution.resources }); // Branch based on solution type if (solution.type === 'helm') { logger.info('Detected Helm solution, using Helm generation flow', { solutionId: args.solutionId, chart: solution.chart ? `${solution.chart.repositoryName}/${solution.chart.chartName}` : 'unknown' }); return await handleHelmGeneration( solution, args.solutionId, dotAI, logger, requestId, args.interaction_id ); } // Capability-based solution: Generate Kubernetes manifests logger.info('Using capability-based manifest generation flow', { solutionId: args.solutionId }); // Prepare file path for manifests (store in tmp directory) const tmpDir = path.join(process.cwd(), 'tmp'); if (!fs.existsSync(tmpDir)) { fs.mkdirSync(tmpDir, { recursive: true }); } const yamlPath = path.join(tmpDir, `${args.solutionId}.yaml`); // AI generation and validation loop let lastError: ErrorContext | undefined; for (let attempt = 1; attempt <= maxAttempts; attempt++) { logger.info('AI manifest generation attempt', { attempt, maxAttempts, isRetry: attempt > 1, requestId }); try { // Extract user answers and generate required labels const userAnswers = extractUserAnswers(solution); const dotAiLabels = addDotAiLabels(undefined, userAnswers, solution); // Generate manifests with AI (including labels) const aiManifests = await generateManifestsWithAI( solution, args.solutionId, dotAI, logger, lastError, dotAiLabels, args.interaction_id ); // Check if Solution CRD is available and generate Solution CR if present let solutionCR = ''; try { const crdAvailable = await isSolutionCRDAvailable(); if (crdAvailable) { solutionCR = generateSolutionCR({ solutionId: args.solutionId, namespace: userAnswers.namespace || 'default', solution: solution, generatedManifestsYaml: aiManifests }); logger.info('Solution CR generated successfully', { solutionId: args.solutionId }); } else { logger.info('Solution CRD not available, skipping Solution CR generation (graceful degradation)', { solutionId: args.solutionId }); } } catch (error) { logger.warn('Failed to check CRD availability or generate Solution CR, skipping', { solutionId: args.solutionId, error: error instanceof Error ? error.message : String(error) }); // Graceful degradation - continue without Solution CR } // Combine all manifests (Solution CR + AI manifests) const manifestParts: string[] = []; if (solutionCR) { manifestParts.push(solutionCR); } manifestParts.push(aiManifests); const manifests = manifestParts.length > 1 ? manifestParts.join('---\n') : manifestParts[0]; // Save manifests to file fs.writeFileSync(yamlPath, manifests, 'utf8'); logger.info('Manifests saved to file', { yamlPath, attempt, requestId }); // Save a copy of this attempt for debugging const attemptPath = yamlPath.replace('.yaml', `_attempt_${attempt.toString().padStart(2, '0')}.yaml`); fs.writeFileSync(attemptPath, manifests, 'utf8'); logger.info('Saved manifest attempt for debugging', { attempt, attemptPath, requestId }); // Validate manifests const validation = await validateManifests(yamlPath); if (validation.valid) { logger.info('Manifest validation successful', { attempt, yamlPath, requestId }); // Success! Return the validated manifests // Check if we should show feedback message (workflow completion point) const feedbackMessage = maybeGetFeedbackMessage(); const response = { success: true, status: 'manifests_generated', solutionId: args.solutionId, manifests: manifests, yamlPath: yamlPath, validationAttempts: attempt, timestamp: new Date().toISOString(), ...(feedbackMessage ? { message: feedbackMessage } : {}) }; return { content: [{ type: 'text' as const, text: JSON.stringify(response, null, 2) }] }; } // Validation failed, prepare error context for next attempt // Only pass AI-generated manifests to avoid duplicates on retry lastError = { attempt, previousManifests: aiManifests, validationResult: validation }; logger.warn('Manifest validation failed', { attempt, maxAttempts, validationErrors: validation.errors, validationWarnings: validation.warnings, requestId }); } catch (error) { logger.error('Error during manifest generation attempt', error as Error); // Check if this is a validation error that should not be retried const errorMessage = error instanceof Error ? error.message : 'Unknown error'; const isValidationError = errorMessage.includes('Application name is required') || errorMessage.includes('Application intent is required'); // If this is a validation error or the last attempt, throw the error immediately if (isValidationError || attempt === maxAttempts) { throw error; } // Prepare error context for retry lastError = { attempt, previousManifests: lastError?.previousManifests || '', validationResult: { valid: false, errors: [errorMessage], warnings: [] } }; } } // If we reach here, all attempts failed throw new Error(`Failed to generate valid manifests after ${maxAttempts} attempts. Last errors: ${lastError?.validationResult.errors.join(', ')}`); }, { operation: 'generate_manifests', component: 'GenerateManifestsTool', requestId, input: args } ); }

Latest Blog Posts

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/vfarcic/dot-ai'

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