cloudagent-deploy
Version:
CloudAgent Deploy - MCP Server for CloudFormation deployments via backend API
903 lines (899 loc) • 66.9 kB
JavaScript
#!/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: {
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',
},
},
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 AWS infrastructure:
1. Create a CloudFormation template and use \`deploy-infrastructure\`
OR
2. Describe your infrastructure needs with \`deploy-infrastructure\` (auto-generates templates)
3. Upload files with \`get-batch-presigned-urls\` for efficient multi-file operations`,
},
],
};
}
}
}
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 } = 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);
if (!template && !templateType) {
// If correlationId present, continue flow with provided answers
if (correlationId || appName || intent || targetRegion) {
const cont = await api.continueDeployInteractive({ correlationId, appName, intent, targetRegion });
// 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 || targetRegion || ''}` }] };
}
const res = await api.startDeployInteractive();
if (res?.status === 'NEEDS_INPUT') {
return {
content: [
{ type: 'text', text: `I need a few details to scaffold your infrastructure. Please provide answers to:\n${JSON.stringify(res.questions, null, 2)}\n\nThen re-run this tool with: correlationId, appName, intent.hosting, targetRegion.` }
]
};
}
}
// 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 = targetRegion || region;
// 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`;
});
}
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 looksLikeStaticSite = template.toLowerCase().includes('static') ||
template.toLowerCase().includes('next.js') ||
template.toLowerCase().includes('cloudfront') ||
template.toLowerCase().includes('s3 bucket');
if (looksLikeStaticSite) {
// Extract app name from stack name
const appName = stackName.replace('cloudagentmcp-', '');
// Use template generation for static site
const result = await api.deployStack({
templateType: 'static-site-cloudfront-oac',
appName: appName,
stackName,
parameters,
tags,
capabilities,
targetRegion
});
// 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**: ${targetRegion}\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
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
});
// 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`;
});
}
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.startsWith('cloudagentmcp-')) {
throw new Error(`Only stacks with 'cloudagentmcp-' prefix can be deleted for safety. Stack '${stackName}' was not created by this MCP service.`);
}
const config = await getConfig();
const client = new BackendApiClient(config);
// Check if stack is in progress before deletion
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 deletion. Use get-stack-status to monitor progress.`);
}
}
catch (error) {
// If stack doesn't exist, we can't delete it
if (error.message.includes('does not exist')) {
throw new Error(`❌ Stack "${stackName}" does not exist and cannot be deleted.`);
}
throw error;
}
const result = await client.deleteStack(stackName);
if (result.success) {
return {
content: [
{
type: 'text',
text: (() => {
const isInProgress = result.status?.includes('IN_PROGRESS');
const successMessage = isInProgress
? 'Stack deletion started successfully.'
: 'Stack deleted successfully.';
const statusMessage = isInProgress
? 'Deletion is in progress. Use `get-stack-status` to monitor progress.'
: 'All resources created by this stack have been removed.\n\nFuture operations take 5-10 minutes. Use `get-stack-status` to monitor progress.';
return `${successMessage}\n\nStack: ${result.stackName}\nStatus: ${result.status}\n\n${statusMessage}`;
})(),
},
],
};
}
else {
return {
content: [
{
type: 'text',
text: `Stack deletion failed.\n\nStack: ${result.stackName}\nStatus: ${result.status}\nError: ${result.error}${result.error ? '' : ''}\n\nCheck the AWS CloudFormation console for more details.`,
},
],
};
}
}
case 'get-stack-status': {
const { stackName } = args;
if (!stackName) {
throw new Error('Stack name parameter is required.');
}
const config = await getConfig();
const client = new BackendApiClient(config);
const result = await client.getStackStatus(stackName, true); // Include events for enhanced reporting
if (result.success) {
let statusText = `Stack Status\n\n`;
statusText += `Stack: ${stackName}\n`;
statusText += `Status: ${result.status}\n`;
statusText += `Stack ID: ${result.stackId}\n`;
// Enhanced failure analysis for failed stacks
if (result.failureDetails && (result.status?.includes('FAILED') || result.status?.includes('ROLLBACK'))) {
const analysis = result.failureDetails;
statusText += `\nFailure Analysis (${analysis.totalFailures} failure${analysis.totalFailures !== 1 ? 's' : ''})\n\n`;
// Primary failure
statusText += `Primary Failure:\n`;
statusText += ` • Resource: ${analysis.primaryFailure.resource}\n`;
statusText += ` • Type: ${analysis.primaryFailure.resourceType}\n`;
statusText += ` • Reason: ${analysis.primaryFailure.re