UNPKG

@cloudagent/aws-deploy

Version:

CloudAgent Deploy - MCP Server for CloudFormation deployments via backend API

891 lines (886 loc) 73.2 kB
#!/usr/bin/env node import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js'; import * as fs from 'fs/promises'; import * as path from 'path'; import { BackendApiClient } from './backend-client.js'; // Global configuration let config = null; // Load configuration async function loadConfig() { // Try to load from environment variables first if (process.env.CLOUDAGENT_API_ENDPOINT && process.env.CLOUDAGENT_API_KEY) { return { apiEndpoint: process.env.CLOUDAGENT_API_ENDPOINT, apiKey: process.env.CLOUDAGENT_API_KEY, projectRoot: process.env.CLOUDAGENT_PROJECT_ROOT || process.cwd() }; } // Try to load from config file try { const configPath = path.join(process.cwd(), '.cloudagent-deploy.json'); const configData = await fs.readFile(configPath, 'utf-8'); return JSON.parse(configData); } catch (error) { throw new Error('No configuration found. Please set CLOUDAGENT_API_ENDPOINT and CLOUDAGENT_API_KEY environment variables or create .cloudagent-deploy.json'); } } // Get configuration with error handling async function getConfig() { if (!config) { config = await loadConfig(); } return config; } // File utilities for static deployment (unchanged) async function collectFiles(rootDir) { const files = {}; const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB per file const ALLOWED_EXTENSIONS = [ '.html', '.htm', '.css', '.js', '.json', '.xml', '.txt', '.png', '.jpg', '.jpeg', '.gif', '.svg', '.ico', '.webp', '.woff', '.woff2', '.ttf', '.eot' ]; // Validate the root directory try { const stats = await fs.stat(rootDir); if (!stats.isDirectory()) { throw new Error(`Path is not a directory: ${rootDir}`); } } catch (error) { throw new Error(`Cannot access directory: ${rootDir} - ${error instanceof Error ? error.message : 'Unknown error'}`); } // Resolve to absolute path to prevent directory traversal const absoluteRootDir = path.resolve(rootDir); async function scanDirectory(dir, baseDir) { try { const entries = await fs.readdir(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dir, entry.name); const relativePath = path.relative(baseDir, fullPath); // Skip if path goes outside the base directory (security check) if (relativePath.startsWith('..')) { continue; } if (entry.isDirectory()) { // Skip common directories if (['.git', 'node_modules', '.mcp', 'dist', 'build', '.DS_Store'].includes(entry.name)) { continue; } if (entry.name.startsWith('.') && entry.name !== '.well-known') { continue; } try { await scanDirectory(fullPath, baseDir); } catch (error) { continue; } } else if (entry.isFile()) { try { const ext = path.extname(entry.name).toLowerCase(); if (ALLOWED_EXTENSIONS.includes(ext)) { const stats = await fs.stat(fullPath); if (stats.size <= MAX_FILE_SIZE) { const content = await fs.readFile(fullPath); files[relativePath] = content.toString('base64'); } } } catch (error) { continue; } } } } catch (error) { // Skip directories we can't read } } await scanDirectory(absoluteRootDir, absoluteRootDir); return files; } // Infrastructure template detection async function detectInfrastructureTemplates(rootDir) { const templates = []; const templatePatterns = [ /\.ya?ml$/i, /\.json$/i, /cloudformation/i, /infrastructure/i, /template/i ]; try { const entries = await fs.readdir(rootDir, { withFileTypes: true }); for (const entry of entries) { if (entry.isFile()) { const fileName = entry.name; // Check if file matches CloudFormation patterns const matchesPattern = templatePatterns.some(pattern => pattern.test(fileName)); if (matchesPattern) { try { const filePath = path.join(rootDir, fileName); const content = await fs.readFile(filePath, 'utf-8'); // Check for CloudFormation indicators in content const isCloudFormation = (content.includes('AWSTemplateFormatVersion') || content.includes('AWS::') || content.includes('Resources:') || content.includes('"Resources"') || content.includes('Type: AWS::') || content.includes('"Type": "AWS::')); if (isCloudFormation) { templates.push(fileName); } } catch { // Skip files we can't read } } } } } catch { // Skip directories we can't read } return { hasTemplates: templates.length > 0, templates }; } // Static deployment (placeholder - would use original logic) async function deployStatic(directory, outputDir) { try { // This would use the original static deployment logic // For now, return a placeholder return { success: true, url: 'https://placeholder-static-deployment.example.com', error: undefined }; } catch (error) { return { success: false, error: error.message }; } } // Helper function to check if stack is in progress function isStackInProgress(status) { const inProgressStatuses = [ 'CREATE_IN_PROGRESS', 'UPDATE_IN_PROGRESS', 'DELETE_IN_PROGRESS', 'ROLLBACK_IN_PROGRESS', 'UPDATE_ROLLBACK_IN_PROGRESS', 'REVIEW_IN_PROGRESS' ]; return inProgressStatuses.includes(status); } // Main MCP server implementation async function main() { const server = new Server({ name: 'cloudagent-deploy', version: '1.0.0', }, { capabilities: { tools: {}, }, }); // List available tools server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: 'deploy', description: 'Deploy cloud infrastructure using CloudFormation templates with automatic rollback and dependency management.', inputSchema: { type: 'object', properties: { directory: { type: 'string', description: 'REQUIRED: Full absolute path to the project directory.', }, outputDir: { type: 'string', description: 'Optional: Relative path to built files directory within the project.', }, projectName: { type: 'string', description: 'Optional: Name for this deployment. If not provided, will use the directory name.', }, }, required: ['directory'], }, }, { name: 'validate-infrastructure', description: 'Validate a CloudFormation template. Stack names must use "cloudagentmcp-" prefix. Returns validation errors that must be fixed before deployment.', inputSchema: { type: 'object', properties: { template: { type: 'string', description: 'REQUIRED: CloudFormation template content (YAML or JSON format)', }, stackName: { type: 'string', pattern: '^cloudagentmcp-', description: 'REQUIRED: Stack name with "cloudagentmcp-" prefix (example: "cloudagentmcp-webapp-prod")', }, }, required: ['template', 'stackName'], }, }, { name: 'deploy-infrastructure', description: 'Deploy infrastructure using AI-driven template selection or custom CloudFormation. Supports enhanced metadata for automatic environment variables, tech stack detection, and template evolution.', inputSchema: { type: 'object', properties: { correlationId: { type: 'string', description: 'Set when continuing an interactive session started earlier (NEEDS_INPUT)' }, appName: { type: 'string', description: 'REQUIRED: Application name (will become stack name with "cloudagentmcp-" prefix)', }, description: { type: 'string', description: 'Application description for smart template detection (e.g., "Python API server with database", "React app with file uploads")', }, template: { type: 'string', description: 'Optional: Legacy CloudFormation template content (YAML or JSON) or infrastructure description', }, metadata: { type: 'object', description: 'Optional: Enhanced application metadata for smart deployment', properties: { techStack: { type: 'string', description: 'Technology stack: python, node, php, ruby, go, docker, etc.', }, dependencies: { type: 'array', description: 'Application dependencies for smart detection', items: { type: 'string', }, }, startCommand: { type: 'string', description: 'Command to start the application (e.g., "gunicorn app:app", "npm start")', }, port: { type: 'integer', description: 'Application port (default: 3000 for Node.js, 5000 for Python)', }, buildCommand: { type: 'string', description: 'Build command (e.g., "npm run build", "pip install -r requirements.txt")', }, environmentVars: { type: 'array', description: 'Environment variables for secure SSM Parameter Store', items: { type: 'object', properties: { name: { type: 'string', description: 'Environment variable name', }, value: { type: 'string', description: 'Default value (will be stored in SSM)', }, secure: { type: 'boolean', description: 'Use SecureString for sensitive data (API keys, passwords)', }, }, required: ['name'], }, }, }, }, parameters: { type: 'object', description: 'Optional: CloudFormation stack parameters', additionalProperties: { type: 'string', }, }, tags: { type: 'object', description: 'Optional: Tags for the CloudFormation stack', additionalProperties: { type: 'string', }, }, capabilities: { type: 'array', description: 'Optional: CloudFormation capabilities (e.g., CAPABILITY_IAM)', items: { type: 'string', }, }, intent: { type: 'object', description: 'Interactive answers, e.g., { hosting: "ec2-server", runtime, entrypoint, port }' }, answers: { type: 'object', description: 'Interactive secret/non-secret answers map (e.g., { "env.OPENAI_API_KEY": "sk-..." }) when continuing NEEDS_INPUT flows.', additionalProperties: { type: ['string', 'number', 'boolean', 'object'] } }, region: { type: 'string', description: 'Optional: AWS region for deployment (default: us-east-1). Alias for targetRegion.', }, targetRegion: { type: 'string', description: 'Optional: AWS region for deployment (required if organization policy allows multiple regions)', }, }, required: ['appName'], }, }, { name: 'delete-infrastructure', description: 'Delete CloudFormation stack. Deletions take 5-10 minutes - use get-stack-status to monitor progress. Only stacks with "cloudagentmcp-" prefix can be deleted.', inputSchema: { type: 'object', properties: { stackName: { type: 'string', pattern: '^cloudagentmcp-', description: 'REQUIRED: Stack name with "cloudagentmcp-" prefix to delete (example: "cloudagentmcp-webapp-prod")', }, }, required: ['stackName'], }, }, { name: 'get-stack-status', description: 'Monitor CloudFormation deployment progress. Use this during deployments to track status. Stack names must use "cloudagentmcp-" prefix.', inputSchema: { type: 'object', properties: { stackName: { type: 'string', pattern: '^cloudagentmcp-', description: 'REQUIRED: Stack name with "cloudagentmcp-" prefix to check (example: "cloudagentmcp-webapp-prod")', }, }, required: ['stackName'], }, }, { name: 'list-stacks', description: 'List CloudFormation stacks.', inputSchema: { type: 'object', properties: {}, }, }, { name: 'get-presigned-url', description: 'Generate presigned URLs for S3 operations on deployed infrastructure buckets. For FRESH deployments only.', inputSchema: { type: 'object', properties: { stackName: { type: 'string', pattern: '^cloudagentmcp-', description: 'REQUIRED: Stack name with "cloudagentmcp-" prefix containing the S3 bucket', }, objectKey: { type: 'string', description: 'REQUIRED: S3 object key/path for the operation (e.g., "index.html", "static/js/main.js")', }, operation: { type: 'string', enum: ['upload', 'download'], description: 'REQUIRED: Whether to generate upload (PUT) or download (GET) URL', }, expirationMinutes: { type: 'number', default: 60, minimum: 1, maximum: 1440, description: 'Optional: URL expiration time in minutes (1-1440, default 60)', }, }, required: ['stackName', 'objectKey', 'operation'], }, }, { name: 'get-batch-presigned-urls', description: 'Generate presigned URLs for multiple file operations in a single request. Much more efficient than individual requests. For FRESH deployments only.', inputSchema: { type: 'object', properties: { stackName: { type: 'string', pattern: '^cloudagentmcp-', description: 'REQUIRED: Stack name with "cloudagentmcp-" prefix containing the S3 bucket', }, files: { type: 'array', items: { type: 'string' }, description: 'REQUIRED: Array of file paths/keys for the operations (e.g., ["index.html", "style.css", "app.js"])', }, operation: { type: 'string', enum: ['upload', 'download'], description: 'REQUIRED: Whether to generate upload (PUT) or download (GET) URLs', }, expirationMinutes: { type: 'number', default: 60, minimum: 1, maximum: 1440, description: 'Optional: URL expiration time in minutes (1-1440, default 60)', }, }, required: ['stackName', 'files', 'operation'], }, }, ], }; }); // Handle tool calls server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { switch (name) { case 'deploy': { const { directory, outputDir, projectName } = args; if (!directory) { throw new McpError(ErrorCode.InvalidParams, 'Directory parameter is required'); } // Get user's deployment preferences from backend const config = await getConfig(); const client = new BackendApiClient(config); const userProfile = await client.getUserProfile(); // Resolve directory path const resolvedProjectDir = path.resolve(directory); // Smart detection: Check for CloudFormation templates const infrastructureDetected = await detectInfrastructureTemplates(resolvedProjectDir); if (userProfile.deploymentType === 'SHARED') { // AI-Driven Shared Infrastructure Deployment const sharedStacks = userProfile.allowedSharedStacks || []; const stackDescriptions = sharedStacks.map(stack => `• ${stack.stackName} (${stack.region}) - ${stack.description}`).join('\n'); return { content: [ { type: 'text', text: `Infrastructure deployment available for this project. To deploy infrastructure: 1. Describe your requirements: \`deploy-infrastructure\` with your project needs and a stack name 2. Upload files efficiently: \`get-batch-presigned-urls\` for multi-file operations Available infrastructure options: ${stackDescriptions}`, }, ], }; } else { // Traditional FRESH deployment workflow if (infrastructureDetected.hasTemplates) { // Found CloudFormation templates return { content: [ { type: 'text', text: `CloudFormation templates detected: ${infrastructureDetected.templates.map(t => `• ${t}`).join('\n')} To deploy: 1. Validate template: \`validate-infrastructure\` 2. Deploy stack: \`deploy-infrastructure\` 3. Monitor progress: \`get-stack-status\` 4. Upload files: \`get-batch-presigned-urls\` (efficient multi-file upload)`, }, ], }; } else { // No CloudFormation templates found - guide toward infrastructure deployment return { content: [ { type: 'text', text: `No infrastructure templates found in this directory. To deploy your app: 1. Use \`deploy-infrastructure\` with your app name and description - I'll auto-generate the right infrastructure for you 2. Upload files with \`get-batch-presigned-urls\` for efficient multi-file operations Example: \`deploy-infrastructure\` with appName: "my-streamlit-app" and description: "Python Streamlit dashboard for data visualization"`, }, ], }; } } } case 'validate-infrastructure': { const { template, stackName } = args; if (!template) { throw new Error('Template parameter is required. Please provide the CloudFormation template content.'); } if (!stackName) { throw new Error('Stack name parameter is required. Please provide a name for the CloudFormation stack.'); } const config = await getConfig(); const client = new BackendApiClient(config); const validationResult = await client.validateTemplate(template); if (validationResult.valid) { return { content: [ { type: 'text', text: `Template validation passed.\n\nStack Name: ${stackName}\nWarnings: ${validationResult.warnings.length}\n\n${validationResult.warnings.length > 0 ? 'Warnings:\n' + validationResult.warnings.map(w => `⚠️ ${w}`).join('\n') + '\n\n' : ''}Template is ready for deployment. Use \`deploy-infrastructure\` to deploy this stack.`, }, ], }; } else { let errorText = `Template validation failed.\n\nStack Name: ${stackName}\nErrors: ${validationResult.errors.length}\nWarnings: ${validationResult.warnings.length}\n\n`; if (validationResult.errors.length > 0) { errorText += 'Errors:\n' + validationResult.errors.map(e => `❌ ${e}`).join('\n') + '\n\n'; } if (validationResult.warnings.length > 0) { errorText += 'Warnings:\n' + validationResult.warnings.map(w => `⚠️ ${w}`).join('\n') + '\n\n'; } errorText += 'Next Steps:\n'; errorText += '1. Fix all validation errors\n'; errorText += '2. Address warnings\n'; errorText += '3. Run validation again\n'; errorText += '4. Use `deploy-infrastructure` once validation passes'; return { content: [ { type: 'text', text: errorText, }, ], }; } } case 'deploy-infrastructure': { const { appName, description, template, metadata, parameters, tags, capabilities, region, // Legacy parameters for backward compatibility stackName, targetRegion, templateType, intent, correlationId, answers } = args; // Interactive scaffold path: if no template is provided and no templateType, start intent-driven flow const cfg = await getConfig(); const api = new BackendApiClient(cfg); // Normalize region input so downstream consistently receives targetRegion const normalizedRegion = targetRegion || region; if (!template && !templateType) { // If correlationId present, continue flow with provided answers if (correlationId) { const cont = await api.continueDeployInteractive({ correlationId, appName, intent, answers, targetRegion: normalizedRegion }); // If backend still needs input, surface questions if (cont?.status === 'NEEDS_INPUT') { return { content: [{ type: 'text', text: `More information required:\n${JSON.stringify(cont.questions, null, 2)}` }] }; } // Otherwise, return backend response summary return { content: [{ type: 'text', text: `Deployment started.\nStack: ${cont.stackName || stackName || ''}\nStatus: ${cont.status || ''}\nRegion: ${cont.region || normalizedRegion || ''}` }] }; } const res = await api.startDeployInteractive({ appName, description, metadata, tags, targetRegion: normalizedRegion }); // Always return after attempting to start interactive to avoid fall-through if (res?.status === 'NEEDS_INPUT') { return { content: [ { type: 'text', text: `I need a few details to scaffold your infrastructure.\n\nCorrelation ID: ${res.correlationId}\n\nPlease provide answers to:\n${JSON.stringify(res.questions, null, 2)}\n\nThen re-run this tool with: correlationId, appName, intent.hosting, targetRegion.` } ] }; } else { // Surface whatever the backend returned and stop here return { content: [ { type: 'text', text: `Interactive session initiated.\n${res?.message ? res.message + '\n' : ''}${res?.correlationId ? `Correlation ID: ${res.correlationId}` : 'Awaiting further input.'}` } ] }; } } // If templateType is provided, server will construct stackName from appName; stackName optional // Enforce MCP naming convention for safety (only if stackName is provided) if (stackName && !stackName.startsWith('cloudagentmcp-')) { throw new Error(`Stack name must start with 'cloudagentmcp-' prefix. Suggested: 'cloudagentmcp-${stackName}'`); } // Get user profile to determine deployment type const deployClient = api; const userProfile = await deployClient.getUserProfile(); // Determine region (targetRegion takes precedence for multi-region compliance) const deploymentRegion = normalizedRegion; // For SHARED users, skip template validation and deploy directly if (userProfile.deploymentType === 'SHARED') { const result = await deployClient.deployStack({ appName, description, template, metadata, stackName, parameters, tags, capabilities, targetRegion: deploymentRegion, }); // Handle region selection requirement if (result.requiresRegionSelection) { const regions = result.allowedRegions; const regionList = regions.map((r, i) => `${i + 1}. ${r}`).join('\n'); throw new Error(`Your organization policy allows deployment to multiple regions. Please specify which region to use:\n\n${regionList}\n\nTo deploy to a specific region, please specify the targetRegion parameter. For example:\n• To deploy to ${regions[0]}: Add targetRegion: "${regions[0]}"\n• To deploy to ${regions[1]}: Add targetRegion: "${regions[1]}"\n\nThis ensures compliance with your organization's multi-region governance policy.`); } if (result.success) { // Check if deployment is complete or still in progress const isInProgress = result.status?.includes('IN_PROGRESS'); const successMessage = isInProgress ? 'Stack deployment started successfully.' : 'Stack deployed successfully.'; let successText = `${successMessage}\n\n`; successText += `Stack: ${result.stackName}\n`; successText += `Status: ${result.status}\n`; successText += `Stack ID: ${result.stackId}\n`; if (result.outputs && Object.keys(result.outputs).length > 0) { successText += `\nOutputs:\n`; Object.entries(result.outputs).forEach(([key, value]) => { successText += ` • ${key}: ${value}\n`; }); const codeBucket = result.outputs.CodeBucket; if (codeBucket) { const publicDns = result.outputs.PublicDNS; const publicIp = result.outputs.PublicIP; successText += `\nNext steps (EC2 code upload):\n`; successText += ` • Upload app.zip to s3://${codeBucket}/app.zip\n`; successText += ` • Zip root must contain your app files (no top-level folder).\n`; successText += ` • Do not include .env or secrets in your zip; secrets are managed via SSM Parameter Store.\n`; successText += ` - Node: package.json (with scripts.start), server.js\n`; successText += ` - Python: requirements.txt, app.py (or your entry)\n`; successText += ` • Verify: ${publicDns ? `http://${publicDns}` : (publicIp ? `http://${publicIp}` : 'instance URL')}\n`; } } if (isInProgress) { successText += `\nDeployment is in progress. Use \`get-stack-status\` to monitor progress.`; } else { successText += `\nFuture deployments take 5-10 minutes. Use \`get-stack-status\` to monitor progress.`; } return { content: [ { type: 'text', text: successText, }, ], }; } else { let errorText = `Stack deployment failed.\n\n`; errorText += `Stack: ${result.stackName}\n`; errorText += `Status: ${result.status}\n`; errorText += `Error: ${result.error}\n`; if (result.events && result.events.length > 0) { errorText += `\nRecent Events:\n`; result.events.slice(0, 5).forEach(event => { errorText += ` • ${event.logicalResourceId}: ${event.resourceStatus}${event.resourceStatusReason ? ` (${event.resourceStatusReason})` : ''}\n`; }); } errorText += '\nCheck the AWS CloudFormation console for more details.'; return { content: [ { type: 'text', text: errorText, }, ], }; } } // For FRESH users, validate that template looks like CloudFormation // If templateType provided, skip local validation and let backend scaffold if (!templateType) { const looksLikeCloudFormation = (typeof template === 'string' && (template.includes('AWSTemplateFormatVersion') || template.includes('Resources:') || template.includes('"Resources"') || template.includes('Type: AWS::') || template.includes('"Type": "AWS::'))); if (!looksLikeCloudFormation) { // Detect intent from infrastructure description and use template generation const templateStr = typeof template === 'string' ? template : ''; const looksLikeStaticSite = templateStr.toLowerCase().includes('static') || templateStr.toLowerCase().includes('next.js') || templateStr.toLowerCase().includes('cloudfront') || templateStr.toLowerCase().includes('s3 bucket'); if (looksLikeStaticSite) { // Extract app name from stack name const appNameFromStack = stackName ? stackName.replace('cloudagentmcp-', '') : (appName || ''); // Use template generation for static site const result = await api.deployStack({ templateType: 'static-site-cloudfront-oac', appName: appNameFromStack, stackName, parameters, tags, capabilities, targetRegion: deploymentRegion }); // Handle the deployment result if (result.success) { return { content: [{ type: 'text', text: `Static site infrastructure deployment started!\n\n🚀 **Stack**: ${result.stackName}\n📊 **Status**: ${result.status}\n🌍 **Region**: ${deploymentRegion}\n\n✨ **Generated Infrastructure:**\n• S3 Bucket with private access\n• CloudFront Distribution with OAC\n• Security headers and SPA routing support\n\n⏱️ Deployment takes 5-10 minutes. Use \`get-stack-status\` to monitor progress.` }] }; } else { return { content: [{ type: 'text', text: `Static site deployment failed.\n\nStack: ${result.stackName}\nError: ${result.error}\n\nPlease try again or check the AWS console for details.` }] }; } } else { // For other infrastructure descriptions, fall back to interactive flow const result = await api.startDeployInteractive(); if (result?.status === 'NEEDS_INPUT') { return { content: [{ type: 'text', text: `I need more details to generate your infrastructure. Please provide:\n\n${result.questions.map((q) => `• **${q.label}**: ${q.help || q.key}`).join('\n')}\n\nThen re-run with the specific template type.` }] }; } } } } const config = await getConfig(); const client = new BackendApiClient(config); // Check if stack is already in progress (only when stackName provided) if (stackName) { try { const currentStatus = await client.getStackStatus(stackName); if (currentStatus.status && isStackInProgress(currentStatus.status)) { throw new Error(`❌ Stack "${stackName}" is currently ${currentStatus.status}. Wait for completion before redeploying. Use get-stack-status to monitor progress.`); } } catch (error) { // If stack doesn't exist, that's fine - we can deploy if (!error.message.includes('does not exist')) { throw error; } } } // First validate template const validationResult = await client.validateTemplate(template); if (!validationResult.valid) { const errorCount = validationResult.errors.length; const errorSummary = validationResult.errors.slice(0, 3).join(', '); throw new Error(`CloudFormation template validation failed with ${errorCount} error(s). Fix these issues before deployment:\n\n ❌ ${errorSummary}${errorCount > 3 ? '\n ... and more' : ''}\n\nRun 'validate-infrastructure' for full details.`); } // Deploy stack via backend API const result = await client.deployStack({ template, stackName, parameters, tags, capabilities, targetRegion: deploymentRegion }); // Handle region selection requirement if (result.requiresRegionSelection) { const regions = result.allowedRegions; const regionList = regions.map((r, i) => `${i + 1}. ${r}`).join('\n'); throw new Error(`Your organization policy allows deployment to multiple regions. Please specify which region to use:\n\n${regionList}\n\nTo deploy to a specific region, please specify the targetRegion parameter. For example:\n• To deploy to ${regions[0]}: Add targetRegion: "${regions[0]}"\n• To deploy to ${regions[1]}: Add targetRegion: "${regions[1]}"\n\nThis ensures compliance with your organization's multi-region governance policy.`); } if (result.success) { // Check if deployment is complete or still in progress const isInProgress = result.status?.includes('IN_PROGRESS'); const successMessage = isInProgress ? 'Stack deployment started successfully.' : 'Stack deployed successfully.'; let successText = `${successMessage}\n\n`; successText += `Stack: ${result.stackName}\n`; successText += `Status: ${result.status}\n`; successText += `Stack ID: ${result.stackId}\n`; if (result.outputs && Object.keys(result.outputs).length > 0) { successText += `\nOutputs:\n`; Object.entries(result.outputs).forEach(([key, value]) => { successText += ` • ${key}: ${value}\n`; }); const codeBucket = result.outputs.CodeBucket; if (codeBucket) { const publicDns = result.outputs.PublicDNS; const publicIp = result.outputs.PublicIP; successText += `\nNext steps (EC2 code upload):\n`; successText += ` • Upload app.zip to s3://${codeBucket}/app.zip\n`; successText += ` • Zip root must contain your app files (no top-level folder).\n`; successText += ` • Do not include .env or secrets in your zip; secrets are managed via SSM Parameter Store.\n`; successText += ` - Node: package.json (with scripts.start), server.js\n`; successText += ` - Python: requirements.txt, app.py (or your entry)\n`; successText += ` • Verify: ${publicDns ? `http://${publicDns}` : (publicIp ? `http://${publicIp}` : 'instance URL')}\n`; } } if (isInProgress) { successText += `\nDeployment is in progress. Use \`get-stack-status\` to monitor progress.`; } else { successText += `\nFuture deployments take 5-10 minutes. Use \`get-stack-status\` to monitor progress.`; } return { content: [ { type: 'text', text: successText, }, ], }; } else { let errorText = `Stack deployment failed.\n\n`; errorText += `Stack: ${result.stackName}\n`; errorText += `Status: ${result.status}\n`; errorText += `Error: ${result.error}\n`; if (result.events && result.events.length > 0) { errorText += `\nRecent Events:\n`; result.events.slice(0, 5).forEach(event => { errorText += ` • ${event.logicalResourceId}: ${event.resourceStatus}${event.resourceStatusReason ? ` (${event.resourceStatusReason})` : ''}\n`; }); } errorText += '\nCheck the AWS CloudFormation console for more details.'; return { content: [ { type: 'text', text: errorText, }, ], }; } } case 'delete-infrastructure': { const { stackName } = args; if (!stackName) { throw new Error('Stack name parameter is required. Please provide the name of the CloudFormation stack to delete.'); } // Enforce MCP naming convention for safety if (!stackName.star