@chinchillaenterprises/mcp-amplify
Version:
AWS Amplify MCP server with intelligent deployment automation, specialized logging suite, and recursive resource discovery
351 lines • 15.8 kB
JavaScript
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