UNPKG

@chinchillaenterprises/mcp-amplify

Version:

AWS Amplify MCP server with intelligent deployment automation, specialized logging suite, and recursive resource discovery

351 lines 15.8 kB
import { ListStackResourcesCommand } from "@aws-sdk/client-cloudformation"; import { DescribeLogGroupsCommand } from "@aws-sdk/client-cloudwatch-logs"; import { getCurrentClients } from './account-handlers.js'; import { handleAmplifyGetAppInfo } from './app-handlers.js'; // Helper function to extract stack name from ARN function getStackNameFromArn(stackArn) { // ARN format: arn:aws:cloudformation:region:account:stack/STACK_NAME/id const parts = stackArn.split('/'); return parts[1]; // The stack name is the second part after splitting by '/' } // Recursive function to discover all resources in nested stacks async function recursiveStackDiscovery(stackName, clients, level = 0) { const allResources = { lambdaFunctions: [], dynamoDBTables: [], cognitoUserPools: [], cognitoIdentityPools: [], s3Buckets: [], appSyncApis: [], apiGatewayRestApis: [], nestedStacks: [], otherResources: [] }; try { const resourcesCommand = new ListStackResourcesCommand({ StackName: stackName }); const resourcesResponse = await clients.cloudformation.send(resourcesCommand); for (const resource of resourcesResponse.StackResourceSummaries || []) { const baseInfo = { logicalId: resource.LogicalResourceId, physicalId: resource.PhysicalResourceId, resourceType: resource.ResourceType, status: resource.ResourceStatus, stackName, stackLevel: level }; if (resource.ResourceType === 'AWS::CloudFormation::Stack') { // Found a nested stack - recurse into it allResources.nestedStacks.push(baseInfo); if (resource.PhysicalResourceId && (resource.ResourceStatus === 'CREATE_COMPLETE' || resource.ResourceStatus === 'UPDATE_COMPLETE')) { // Extract stack name from ARN for nested stack const nestedStackName = resource.PhysicalResourceId.startsWith('arn:aws:cloudformation:') ? getStackNameFromArn(resource.PhysicalResourceId) : resource.PhysicalResourceId; const nestedResources = await recursiveStackDiscovery(nestedStackName, clients, level + 1); // Merge nested resources into our collection Object.keys(nestedResources).forEach(key => { if (Array.isArray(allResources[key]) && Array.isArray(nestedResources[key])) { allResources[key].push(...nestedResources[key]); } }); } } else { // Categorize the resource switch (resource.ResourceType) { case 'AWS::Lambda::Function': allResources.lambdaFunctions.push({ ...baseInfo, functionName: resource.PhysicalResourceId, logGroupName: `/aws/lambda/${resource.PhysicalResourceId}` }); break; case 'AWS::DynamoDB::Table': allResources.dynamoDBTables.push({ ...baseInfo, tableName: resource.PhysicalResourceId }); break; case 'AWS::Cognito::UserPool': allResources.cognitoUserPools.push({ ...baseInfo, userPoolId: resource.PhysicalResourceId }); break; case 'AWS::Cognito::IdentityPool': allResources.cognitoIdentityPools.push({ ...baseInfo, identityPoolId: resource.PhysicalResourceId }); break; case 'AWS::S3::Bucket': allResources.s3Buckets.push({ ...baseInfo, bucketName: resource.PhysicalResourceId }); break; case 'AWS::AppSync::GraphQLApi': allResources.appSyncApis.push({ ...baseInfo, apiId: resource.PhysicalResourceId }); break; case 'AWS::ApiGateway::RestApi': allResources.apiGatewayRestApis.push({ ...baseInfo, apiId: resource.PhysicalResourceId }); break; default: // Check for custom resources that represent DynamoDB tables if (resource.LogicalResourceId?.endsWith('Table') && resource.ResourceType === 'Custom::AmplifyDynamoDBTable') { allResources.dynamoDBTables.push({ ...baseInfo, tableName: resource.PhysicalResourceId, isCustomResource: true }); } else { allResources.otherResources.push(baseInfo); } } } } } catch (error) { console.error(`Error scanning stack ${stackName}:`, error); } return allResources; } // Comprehensive log group discovery async function discoverAllLogGroups(resources, clients) { const logGroups = []; const logGroupsToCheck = new Map(); // Lambda function log groups resources.lambdaFunctions?.forEach((func) => { const logGroupName = `/aws/lambda/${func.functionName}`; logGroupsToCheck.set(logGroupName, { name: logGroupName, type: 'Lambda', resourceName: func.functionName, logicalId: func.logicalId, service: 'lambda' }); }); // AppSync API log groups resources.appSyncApis?.forEach((api) => { const logGroupName = `/aws/appsync/apis/${api.apiId}`; logGroupsToCheck.set(logGroupName, { name: logGroupName, type: 'AppSync GraphQL', resourceName: api.apiId, logicalId: api.logicalId, service: 'appsync' }); }); // API Gateway execution log groups resources.apiGatewayRestApis?.forEach((api) => { // API Gateway uses a specific pattern for execution logs const logGroupName = `API-Gateway-Execution-Logs_${api.apiId}/prod`; logGroupsToCheck.set(logGroupName, { name: logGroupName, type: 'API Gateway Execution', resourceName: api.apiId, logicalId: api.logicalId, service: 'apigateway' }); // Also check for access logs pattern const accessLogGroupName = `/aws/apigateway/${api.apiId}/prod`; logGroupsToCheck.set(accessLogGroupName, { name: accessLogGroupName, type: 'API Gateway Access', resourceName: api.apiId, logicalId: api.logicalId, service: 'apigateway' }); }); // Cognito log groups (if logging is enabled) resources.cognitoUserPools?.forEach((pool) => { const logGroupName = `/aws/cognito/userpools/${pool.userPoolId}`; logGroupsToCheck.set(logGroupName, { name: logGroupName, type: 'Cognito UserPool', resourceName: pool.userPoolId, logicalId: pool.logicalId, service: 'cognito' }); }); // Step Functions log groups (check in other resources) resources.otherResources?.forEach((resource) => { if (resource.resourceType === 'AWS::StepFunctions::StateMachine') { const logGroupName = `/aws/vendedlogs/states/${resource.physicalId}`; logGroupsToCheck.set(logGroupName, { name: logGroupName, type: 'Step Functions', resourceName: resource.physicalId, logicalId: resource.logicalId, service: 'stepfunctions' }); } }); // Verify which log groups actually exist for (const [logGroupName, logGroupInfo] of logGroupsToCheck) { try { const describeCommand = new DescribeLogGroupsCommand({ logGroupNamePrefix: logGroupName, limit: 1 }); const response = await clients.cloudwatchLogs.send(describeCommand); if (response.logGroups && response.logGroups.length > 0) { const actualLogGroup = response.logGroups[0]; logGroups.push({ ...logGroupInfo, exists: true, actualName: actualLogGroup.logGroupName, sizeBytes: actualLogGroup.storedBytes || 0, retentionDays: actualLogGroup.retentionInDays, creationTime: actualLogGroup.creationTime, lastEventTime: actualLogGroup.lastEventTime, metricFilterCount: actualLogGroup.metricFilterCount || 0 }); } else { logGroups.push({ ...logGroupInfo, exists: false, reason: 'Log group not found' }); } } catch (error) { logGroups.push({ ...logGroupInfo, exists: false, reason: 'Failed to check', error: error instanceof Error ? error.message : String(error) }); } } return logGroups; } export async function handleAmplifyDiscoverResources(args) { const { appId, branchName, resourceType = 'all' } = args; if (!appId) { throw new Error('appId is required'); } try { const clients = getCurrentClients(); // GOLDEN KEY APPROACH: Get app info to extract CloudFormation stack ARN const appInfo = await handleAmplifyGetAppInfo({ appId }); // Find the target branch let targetBranch = branchName; if (!targetBranch) { if (appInfo.branches?.length === 1) { targetBranch = appInfo.branches[0].branchName; } else if (appInfo.branches?.length > 1) { throw new Error(`Multiple branches found. Please specify branchName: ${appInfo.branches.map((b) => b.branchName).join(', ')}`); } else { throw new Error(`No branches found for app ${appId}`); } } // Find the specific branch const branch = appInfo.branches?.find((b) => b.branchName === targetBranch); if (!branch) { throw new Error(`Branch '${targetBranch}' not found. Available branches: ${appInfo.branches?.map((b) => b.branchName).join(', ')}`); } // Extract CloudFormation stack ARN (THE GOLDEN KEY!) if (!branch.backend?.stackArn) { throw new Error(`No CloudFormation stack found for branch '${targetBranch}'. The branch might not have backend resources deployed.`); } const stackArn = branch.backend.stackArn; const mainStackName = stackArn.split('/')[1]; // RECURSIVE DISCOVERY: Get ALL resources from nested stacks const allResources = await recursiveStackDiscovery(mainStackName, clients); // COMPREHENSIVE LOG GROUP DISCOVERY const allLogGroups = await discoverAllLogGroups(allResources, clients); // Categorize log groups const logGroupsByService = { lambda: allLogGroups.filter(lg => lg.service === 'lambda'), appsync: allLogGroups.filter(lg => lg.service === 'appsync'), apigateway: allLogGroups.filter(lg => lg.service === 'apigateway'), cognito: allLogGroups.filter(lg => lg.service === 'cognito'), stepfunctions: allLogGroups.filter(lg => lg.service === 'stepfunctions'), other: allLogGroups.filter(lg => !['lambda', 'appsync', 'apigateway', 'cognito', 'stepfunctions'].includes(lg.service)) }; // Filter by resource type if specified let filteredResources = {}; if (resourceType === 'all') { filteredResources = allResources; } else if (resourceType === 'log-groups') { filteredResources = { logGroupsByService }; } else { switch (resourceType) { case 'lambda': filteredResources = { lambdaFunctions: allResources.lambdaFunctions }; break; case 'dynamodb': filteredResources = { dynamoDBTables: allResources.dynamoDBTables }; break; case 'cognito': filteredResources = { cognitoUserPools: allResources.cognitoUserPools, cognitoIdentityPools: allResources.cognitoIdentityPools }; break; case 'appsync': filteredResources = { appSyncApis: allResources.appSyncApis }; break; default: filteredResources = allResources; } } return { success: true, appId, branchName: targetBranch, mainStackName, stackArn, discoveryMethod: 'recursive-nested-stack-discovery', ...filteredResources, logGroups: allLogGroups.filter(lg => lg.exists), logGroupsByService, potentialLogGroups: allLogGroups.filter(lg => !lg.exists), summary: { totalLambdaFunctions: allResources.lambdaFunctions.length, totalDynamoDBTables: allResources.dynamoDBTables.length, totalAppSyncApis: allResources.appSyncApis.length, totalApiGatewayApis: allResources.apiGatewayRestApis.length, totalCognitoUserPools: allResources.cognitoUserPools.length, totalCognitoIdentityPools: allResources.cognitoIdentityPools.length, totalS3Buckets: allResources.s3Buckets.length, totalNestedStacks: allResources.nestedStacks.length, verifiedLogGroups: allLogGroups.filter(lg => lg.exists).length, totalLogGroups: allLogGroups.length, logGroupsByType: { lambda: logGroupsByService.lambda.filter(lg => lg.exists).length, appsync: logGroupsByService.appsync.filter(lg => lg.exists).length, apigateway: logGroupsByService.apigateway.filter(lg => lg.exists).length, cognito: logGroupsByService.cognito.filter(lg => lg.exists).length, stepfunctions: logGroupsByService.stepfunctions.filter(lg => lg.exists).length } } }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { success: false, error: `Failed to discover resources: ${errorMessage}`, appId, branchName }; } } //# sourceMappingURL=resource-discovery-handlers.js.map